feat: Implement DefaultCryptoHmac for compliance-aware HMAC operations
- Added DefaultCryptoHmac class implementing ICryptoHmac interface. - Introduced purpose-based HMAC computation methods. - Implemented verification methods for HMACs with constant-time comparison. - Created HmacAlgorithms and HmacPurpose classes for well-known identifiers. - Added compliance profile support for HMAC algorithms. - Included asynchronous methods for HMAC computation from streams.
This commit is contained in:
@@ -0,0 +1,308 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Aoc;
|
||||
using StellaOps.Concelier.Core.Aoc;
|
||||
using StellaOps.Concelier.RawModels;
|
||||
|
||||
namespace StellaOps.Concelier.Core.Tests.Aoc;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="AdvisorySchemaValidator"/> per WEB-AOC-19-002.
|
||||
/// Covers ERR_AOC_001 (forbidden), ERR_AOC_002 (merge), ERR_AOC_006 (derived), ERR_AOC_007 (unknown).
|
||||
/// </summary>
|
||||
public sealed class AdvisorySchemaValidatorTests
|
||||
{
|
||||
private static readonly AocGuardOptions GuardOptions = AocGuardOptions.Default;
|
||||
|
||||
private static AdvisoryRawDocument CreateValidDocument(string tenant = "tenant-a")
|
||||
{
|
||||
using var rawDocument = JsonDocument.Parse("""{"id":"demo"}""");
|
||||
return new AdvisoryRawDocument(
|
||||
Tenant: tenant,
|
||||
Source: new RawSourceMetadata("vendor-x", "connector-y", "1.0.0"),
|
||||
Upstream: new RawUpstreamMetadata(
|
||||
UpstreamId: "GHSA-xxxx",
|
||||
DocumentVersion: "1",
|
||||
RetrievedAt: DateTimeOffset.UtcNow,
|
||||
ContentHash: "sha256:abc",
|
||||
Signature: new RawSignatureMetadata(false),
|
||||
Provenance: ImmutableDictionary<string, string>.Empty),
|
||||
Content: new RawContent(
|
||||
Format: "OSV",
|
||||
SpecVersion: "1.0",
|
||||
Raw: rawDocument.RootElement.Clone()),
|
||||
Identifiers: new RawIdentifiers(
|
||||
Aliases: ImmutableArray.Create("GHSA-xxxx"),
|
||||
PrimaryId: "GHSA-xxxx"),
|
||||
Linkset: new RawLinkset
|
||||
{
|
||||
Aliases = ImmutableArray<string>.Empty,
|
||||
PackageUrls = ImmutableArray<string>.Empty,
|
||||
Cpes = ImmutableArray<string>.Empty,
|
||||
References = ImmutableArray<RawReference>.Empty,
|
||||
ReconciledFrom = ImmutableArray<string>.Empty,
|
||||
Notes = ImmutableDictionary<string, string>.Empty
|
||||
},
|
||||
Links: ImmutableArray<RawLink>.Empty);
|
||||
}
|
||||
|
||||
private static AdvisorySchemaValidator CreateValidator()
|
||||
=> new(new AocWriteGuard(), Options.Create(GuardOptions));
|
||||
|
||||
[Fact]
|
||||
public void ValidateSchema_AllowsValidDocument()
|
||||
{
|
||||
var validator = CreateValidator();
|
||||
var document = CreateValidDocument();
|
||||
|
||||
var result = validator.ValidateSchema(document);
|
||||
|
||||
Assert.True(result.IsValid);
|
||||
Assert.Empty(result.Violations);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateForbiddenFields_ReturnsSuccessForValidDocument()
|
||||
{
|
||||
var validator = CreateValidator();
|
||||
var document = CreateValidDocument();
|
||||
|
||||
var result = validator.ValidateForbiddenFields(document);
|
||||
|
||||
Assert.True(result.IsValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateDerivedFields_ReturnsSuccessForValidDocument()
|
||||
{
|
||||
var validator = CreateValidator();
|
||||
var document = CreateValidDocument();
|
||||
|
||||
var result = validator.ValidateDerivedFields(document);
|
||||
|
||||
Assert.True(result.IsValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateAllowedFields_ReturnsSuccessForValidDocument()
|
||||
{
|
||||
var validator = CreateValidator();
|
||||
var document = CreateValidDocument();
|
||||
|
||||
var result = validator.ValidateAllowedFields(document);
|
||||
|
||||
Assert.True(result.IsValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateMergeAttempt_ReturnsSuccessForValidDocument()
|
||||
{
|
||||
var validator = CreateValidator();
|
||||
var document = CreateValidDocument();
|
||||
|
||||
var result = validator.ValidateMergeAttempt(document);
|
||||
|
||||
Assert.True(result.IsValid);
|
||||
}
|
||||
|
||||
// Direct IAocGuard tests for ERR_AOC_001, ERR_AOC_002, ERR_AOC_006, ERR_AOC_007
|
||||
// These test the underlying guard behavior with arbitrary JSON
|
||||
|
||||
[Fact]
|
||||
public void AocGuard_DetectsForbiddenField_ERR_AOC_001()
|
||||
{
|
||||
var guard = new AocWriteGuard();
|
||||
using var jsonDoc = JsonDocument.Parse("""
|
||||
{
|
||||
"tenant": "test",
|
||||
"severity": "high",
|
||||
"source": {"vendor": "test", "connector": "test", "version": "1.0"},
|
||||
"upstream": {
|
||||
"upstream_id": "CVE-2024-0001",
|
||||
"content_hash": "sha256:abc",
|
||||
"retrieved_at": "2024-01-01T00:00:00Z",
|
||||
"signature": {"present": false},
|
||||
"provenance": {}
|
||||
},
|
||||
"content": {"format": "OSV", "raw": {}},
|
||||
"identifiers": {"aliases": [], "primary": "CVE-2024-0001"},
|
||||
"linkset": {}
|
||||
}
|
||||
""");
|
||||
|
||||
var result = guard.Validate(jsonDoc.RootElement, GuardOptions);
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Contains(result.Violations, v =>
|
||||
v.Code == AocViolationCode.ForbiddenField &&
|
||||
v.ErrorCode == "ERR_AOC_001" &&
|
||||
v.Path == "/severity");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AocGuard_DetectsMergedFromField_ERR_AOC_001()
|
||||
{
|
||||
var guard = new AocWriteGuard();
|
||||
using var jsonDoc = JsonDocument.Parse("""
|
||||
{
|
||||
"tenant": "test",
|
||||
"merged_from": ["obs-1", "obs-2"],
|
||||
"source": {"vendor": "test", "connector": "test", "version": "1.0"},
|
||||
"upstream": {
|
||||
"upstream_id": "CVE-2024-0001",
|
||||
"content_hash": "sha256:abc",
|
||||
"retrieved_at": "2024-01-01T00:00:00Z",
|
||||
"signature": {"present": false},
|
||||
"provenance": {}
|
||||
},
|
||||
"content": {"format": "OSV", "raw": {}},
|
||||
"identifiers": {"aliases": [], "primary": "CVE-2024-0001"},
|
||||
"linkset": {}
|
||||
}
|
||||
""");
|
||||
|
||||
var result = guard.Validate(jsonDoc.RootElement, GuardOptions);
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Contains(result.Violations, v =>
|
||||
v.Code == AocViolationCode.ForbiddenField &&
|
||||
v.ErrorCode == "ERR_AOC_001" &&
|
||||
v.Path == "/merged_from");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AocGuard_DetectsDerivedField_ERR_AOC_006()
|
||||
{
|
||||
var guard = new AocWriteGuard();
|
||||
using var jsonDoc = JsonDocument.Parse("""
|
||||
{
|
||||
"tenant": "test",
|
||||
"effective_status": "affected",
|
||||
"source": {"vendor": "test", "connector": "test", "version": "1.0"},
|
||||
"upstream": {
|
||||
"upstream_id": "CVE-2024-0001",
|
||||
"content_hash": "sha256:abc",
|
||||
"retrieved_at": "2024-01-01T00:00:00Z",
|
||||
"signature": {"present": false},
|
||||
"provenance": {}
|
||||
},
|
||||
"content": {"format": "OSV", "raw": {}},
|
||||
"identifiers": {"aliases": [], "primary": "CVE-2024-0001"},
|
||||
"linkset": {}
|
||||
}
|
||||
""");
|
||||
|
||||
var result = guard.Validate(jsonDoc.RootElement, GuardOptions);
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Contains(result.Violations, v =>
|
||||
v.Code == AocViolationCode.DerivedFindingDetected &&
|
||||
v.ErrorCode == "ERR_AOC_006" &&
|
||||
v.Path == "/effective_status");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AocGuard_DetectsUnknownField_ERR_AOC_007()
|
||||
{
|
||||
var guard = new AocWriteGuard();
|
||||
using var jsonDoc = JsonDocument.Parse("""
|
||||
{
|
||||
"tenant": "test",
|
||||
"unknown_custom_field": "value",
|
||||
"source": {"vendor": "test", "connector": "test", "version": "1.0"},
|
||||
"upstream": {
|
||||
"upstream_id": "CVE-2024-0001",
|
||||
"content_hash": "sha256:abc",
|
||||
"retrieved_at": "2024-01-01T00:00:00Z",
|
||||
"signature": {"present": false},
|
||||
"provenance": {}
|
||||
},
|
||||
"content": {"format": "OSV", "raw": {}},
|
||||
"identifiers": {"aliases": [], "primary": "CVE-2024-0001"},
|
||||
"linkset": {}
|
||||
}
|
||||
""");
|
||||
|
||||
var result = guard.Validate(jsonDoc.RootElement, GuardOptions);
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Contains(result.Violations, v =>
|
||||
v.Code == AocViolationCode.UnknownField &&
|
||||
v.ErrorCode == "ERR_AOC_007" &&
|
||||
v.Path == "/unknown_custom_field");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("cvss")]
|
||||
[InlineData("cvss_vector")]
|
||||
[InlineData("consensus_provider")]
|
||||
[InlineData("reachability")]
|
||||
[InlineData("asset_criticality")]
|
||||
[InlineData("risk_score")]
|
||||
public void AocGuard_DetectsAllForbiddenFields(string forbiddenField)
|
||||
{
|
||||
var guard = new AocWriteGuard();
|
||||
var json = $$"""
|
||||
{
|
||||
"tenant": "test",
|
||||
"{{forbiddenField}}": "forbidden_value",
|
||||
"source": {"vendor": "test", "connector": "test", "version": "1.0"},
|
||||
"upstream": {
|
||||
"upstream_id": "CVE-2024-0001",
|
||||
"content_hash": "sha256:abc",
|
||||
"retrieved_at": "2024-01-01T00:00:00Z",
|
||||
"signature": {"present": false},
|
||||
"provenance": {}
|
||||
},
|
||||
"content": {"format": "OSV", "raw": {}},
|
||||
"identifiers": {"aliases": [], "primary": "CVE-2024-0001"},
|
||||
"linkset": {}
|
||||
}
|
||||
""";
|
||||
using var jsonDoc = JsonDocument.Parse(json);
|
||||
|
||||
var result = guard.Validate(jsonDoc.RootElement, GuardOptions);
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Contains(result.Violations, v =>
|
||||
v.Code == AocViolationCode.ForbiddenField &&
|
||||
v.ErrorCode == "ERR_AOC_001");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("effective_range")]
|
||||
[InlineData("effective_severity")]
|
||||
[InlineData("effective_cvss")]
|
||||
public void AocGuard_DetectsAllDerivedFields(string derivedField)
|
||||
{
|
||||
var guard = new AocWriteGuard();
|
||||
var json = $$"""
|
||||
{
|
||||
"tenant": "test",
|
||||
"{{derivedField}}": "derived_value",
|
||||
"source": {"vendor": "test", "connector": "test", "version": "1.0"},
|
||||
"upstream": {
|
||||
"upstream_id": "CVE-2024-0001",
|
||||
"content_hash": "sha256:abc",
|
||||
"retrieved_at": "2024-01-01T00:00:00Z",
|
||||
"signature": {"present": false},
|
||||
"provenance": {}
|
||||
},
|
||||
"content": {"format": "OSV", "raw": {}},
|
||||
"identifiers": {"aliases": [], "primary": "CVE-2024-0001"},
|
||||
"linkset": {}
|
||||
}
|
||||
""";
|
||||
using var jsonDoc = JsonDocument.Parse(json);
|
||||
|
||||
var result = guard.Validate(jsonDoc.RootElement, GuardOptions);
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
// Derived fields (effective_*) trigger both ForbiddenField and DerivedFindingDetected
|
||||
// if they're in the forbidden list, otherwise just DerivedFindingDetected
|
||||
Assert.Contains(result.Violations, v =>
|
||||
v.Code == AocViolationCode.DerivedFindingDetected &&
|
||||
v.ErrorCode == "ERR_AOC_006");
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,8 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<!-- Disable Concelier Testing infra which requires Storage.Mongo -->
|
||||
<UseConcelierTestInfra>false</UseConcelierTestInfra>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.Core/StellaOps.Concelier.Core.csproj" />
|
||||
@@ -15,6 +17,6 @@
|
||||
<ProjectReference Include="../../../Aoc/__Libraries/StellaOps.Aoc/StellaOps.Aoc.csproj" />
|
||||
<!-- Test packages inherited from Directory.Build.props -->
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0-rc.2.25502.107" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user