new advisories work and features gaps work

This commit is contained in:
master
2026-01-14 18:39:19 +02:00
parent 95d5898650
commit 15aeac8e8b
148 changed files with 16731 additions and 554 deletions

View File

@@ -14,7 +14,7 @@ public sealed class BinaryDiffPredicateBuilderTests
public void Build_RequiresSubject()
{
var options = Options.Create(new BinaryDiffOptions { ToolVersion = "1.0.0" });
var builder = new BinaryDiffPredicateBuilder(options, BinaryDiffTestData.FixedTimeProvider);
var builder = new BinaryDiffPredicateBuilder(options, BinaryDiffTestData.TestTimeProvider);
builder.WithInputs(
new BinaryDiffImageReference { Digest = "sha256:base" },
@@ -30,7 +30,7 @@ public sealed class BinaryDiffPredicateBuilderTests
public void Build_RequiresInputs()
{
var options = Options.Create(new BinaryDiffOptions { ToolVersion = "1.0.0" });
var builder = new BinaryDiffPredicateBuilder(options, BinaryDiffTestData.FixedTimeProvider);
var builder = new BinaryDiffPredicateBuilder(options, BinaryDiffTestData.TestTimeProvider);
builder.WithSubject("docker://example/app@sha256:base", "sha256:aaaa");
@@ -44,7 +44,7 @@ public sealed class BinaryDiffPredicateBuilderTests
public void Build_SortsFindingsAndSections()
{
var options = Options.Create(new BinaryDiffOptions { ToolVersion = "1.0.0" });
var builder = new BinaryDiffPredicateBuilder(options, BinaryDiffTestData.FixedTimeProvider);
var builder = new BinaryDiffPredicateBuilder(options, BinaryDiffTestData.TestTimeProvider);
builder.WithSubject("docker://example/app@sha256:base", "sha256:aaaa")
.WithInputs(
@@ -106,7 +106,7 @@ public sealed class BinaryDiffPredicateBuilderTests
AnalyzedSections = [".z", ".a"]
});
var builder = new BinaryDiffPredicateBuilder(options, BinaryDiffTestData.FixedTimeProvider);
var builder = new BinaryDiffPredicateBuilder(options, BinaryDiffTestData.TestTimeProvider);
builder.WithSubject("docker://example/app@sha256:base", "sha256:aaaa")
.WithInputs(
new BinaryDiffImageReference { Digest = "sha256:base" },
@@ -116,7 +116,7 @@ public sealed class BinaryDiffPredicateBuilderTests
predicate.Metadata.ToolVersion.Should().Be("2.0.0");
predicate.Metadata.ConfigDigest.Should().Be("sha256:cfg");
predicate.Metadata.AnalysisTimestamp.Should().Be(BinaryDiffTestData.FixedTimeProvider.GetUtcNow());
predicate.Metadata.AnalysisTimestamp.Should().Be(BinaryDiffTestData.TestTimeProvider.GetUtcNow());
predicate.Metadata.AnalyzedSections.Should().Equal(".a", ".z");
}
}

View File

@@ -8,7 +8,7 @@ namespace StellaOps.Attestor.StandardPredicates.Tests.BinaryDiff;
internal static class BinaryDiffTestData
{
internal static readonly TimeProvider FixedTimeProvider =
internal static readonly TimeProvider TestTimeProvider =
new FixedTimeProvider(new DateTimeOffset(2026, 1, 13, 12, 0, 0, TimeSpan.Zero));
internal static BinaryDiffPredicate CreatePredicate()
@@ -20,7 +20,7 @@ internal static class BinaryDiffTestData
AnalyzedSections = [".text", ".rodata", ".data"]
});
var builder = new BinaryDiffPredicateBuilder(options, FixedTimeProvider);
var builder = new BinaryDiffPredicateBuilder(options, TestTimeProvider);
builder.WithSubject("docker://example/app@sha256:base", "sha256:aaaaaaaa")
.WithInputs(
new BinaryDiffImageReference

View File

@@ -0,0 +1,225 @@
// -----------------------------------------------------------------------------
// VexOverridePredicateBuilderTests.cs
// Sprint: SPRINT_20260112_004_ATTESTOR_vex_override_predicate (ATT-VEX-002)
// Description: Tests for VEX override predicate builder
// -----------------------------------------------------------------------------
using System.Text.Json;
using StellaOps.Attestor.StandardPredicates.VexOverride;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Attestor.StandardPredicates.Tests.VexOverride;
public sealed class VexOverridePredicateBuilderTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Build_WithRequiredFields_CreatesPredicate()
{
var decisionTime = new DateTimeOffset(2026, 1, 14, 10, 0, 0, TimeSpan.Zero);
var predicate = new VexOverridePredicateBuilder()
.WithArtifactDigest("sha256:abc123")
.WithVulnerabilityId("CVE-2024-12345")
.WithDecision(VexOverrideDecision.NotAffected)
.WithJustification("Component is not in use")
.WithDecisionTime(decisionTime)
.WithOperatorId("user@example.com")
.Build();
Assert.Equal("sha256:abc123", predicate.ArtifactDigest);
Assert.Equal("CVE-2024-12345", predicate.VulnerabilityId);
Assert.Equal(VexOverrideDecision.NotAffected, predicate.Decision);
Assert.Equal("Component is not in use", predicate.Justification);
Assert.Equal(decisionTime, predicate.DecisionTime);
Assert.Equal("user@example.com", predicate.OperatorId);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Build_MissingArtifactDigest_Throws()
{
var builder = new VexOverridePredicateBuilder()
.WithVulnerabilityId("CVE-2024-12345")
.WithDecision(VexOverrideDecision.NotAffected)
.WithJustification("Test")
.WithDecisionTime(DateTimeOffset.UtcNow)
.WithOperatorId("user@example.com");
Assert.Throws<InvalidOperationException>(() => builder.Build());
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Build_WithEvidenceRefs_AddsToList()
{
var predicate = new VexOverridePredicateBuilder()
.WithArtifactDigest("sha256:abc123")
.WithVulnerabilityId("CVE-2024-12345")
.WithDecision(VexOverrideDecision.Mitigated)
.WithJustification("Compensating control")
.WithDecisionTime(DateTimeOffset.UtcNow)
.WithOperatorId("user@example.com")
.AddEvidenceRef("document", "https://example.com/doc", "sha256:def456", "Design doc")
.AddEvidenceRef(new EvidenceReference
{
Type = "ticket",
Uri = "https://jira.example.com/PROJ-123"
})
.Build();
Assert.Equal(2, predicate.EvidenceRefs.Length);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Build_WithTool_SetsTool()
{
var predicate = new VexOverridePredicateBuilder()
.WithArtifactDigest("sha256:abc123")
.WithVulnerabilityId("CVE-2024-12345")
.WithDecision(VexOverrideDecision.Accepted)
.WithJustification("Accepted risk")
.WithDecisionTime(DateTimeOffset.UtcNow)
.WithOperatorId("user@example.com")
.WithTool("StellaOps", "1.0.0", "StellaOps Inc")
.Build();
Assert.NotNull(predicate.Tool);
Assert.Equal("StellaOps", predicate.Tool.Name);
Assert.Equal("1.0.0", predicate.Tool.Version);
Assert.Equal("StellaOps Inc", predicate.Tool.Vendor);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Build_WithMetadata_AddsMetadata()
{
var predicate = new VexOverridePredicateBuilder()
.WithArtifactDigest("sha256:abc123")
.WithVulnerabilityId("CVE-2024-12345")
.WithDecision(VexOverrideDecision.NotAffected)
.WithJustification("Test")
.WithDecisionTime(DateTimeOffset.UtcNow)
.WithOperatorId("user@example.com")
.WithMetadata("tenant", "acme-corp")
.WithMetadata("environment", "production")
.Build();
Assert.Equal(2, predicate.Metadata.Count);
Assert.Equal("acme-corp", predicate.Metadata["tenant"]);
Assert.Equal("production", predicate.Metadata["environment"]);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void BuildCanonicalJson_ProducesDeterministicOutput()
{
var decisionTime = new DateTimeOffset(2026, 1, 14, 10, 0, 0, TimeSpan.Zero);
var json1 = new VexOverridePredicateBuilder()
.WithArtifactDigest("sha256:abc123")
.WithVulnerabilityId("CVE-2024-12345")
.WithDecision(VexOverrideDecision.NotAffected)
.WithJustification("Test")
.WithDecisionTime(decisionTime)
.WithOperatorId("user@example.com")
.BuildCanonicalJson();
var json2 = new VexOverridePredicateBuilder()
.WithOperatorId("user@example.com") // Different order
.WithDecisionTime(decisionTime)
.WithJustification("Test")
.WithDecision(VexOverrideDecision.NotAffected)
.WithVulnerabilityId("CVE-2024-12345")
.WithArtifactDigest("sha256:abc123")
.BuildCanonicalJson();
Assert.Equal(json1, json2);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void BuildCanonicalJson_HasSortedKeys()
{
var decisionTime = new DateTimeOffset(2026, 1, 14, 10, 0, 0, TimeSpan.Zero);
var json = new VexOverridePredicateBuilder()
.WithArtifactDigest("sha256:abc123")
.WithVulnerabilityId("CVE-2024-12345")
.WithDecision(VexOverrideDecision.NotAffected)
.WithJustification("Test")
.WithDecisionTime(decisionTime)
.WithOperatorId("user@example.com")
.BuildCanonicalJson();
using var document = JsonDocument.Parse(json);
var keys = document.RootElement.EnumerateObject().Select(p => p.Name).ToList();
// Verify keys are alphabetically sorted
var sortedKeys = keys.OrderBy(k => k, StringComparer.Ordinal).ToList();
Assert.Equal(sortedKeys, keys);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void BuildJsonBytes_ReturnsUtf8Bytes()
{
var decisionTime = new DateTimeOffset(2026, 1, 14, 10, 0, 0, TimeSpan.Zero);
var bytes = new VexOverridePredicateBuilder()
.WithArtifactDigest("sha256:abc123")
.WithVulnerabilityId("CVE-2024-12345")
.WithDecision(VexOverrideDecision.NotAffected)
.WithJustification("Test")
.WithDecisionTime(decisionTime)
.WithOperatorId("user@example.com")
.BuildJsonBytes();
Assert.NotEmpty(bytes);
var json = System.Text.Encoding.UTF8.GetString(bytes);
using var document = JsonDocument.Parse(json);
Assert.Equal(JsonValueKind.Object, document.RootElement.ValueKind);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Build_WithExpiresAt_SetsExpiration()
{
var decisionTime = new DateTimeOffset(2026, 1, 14, 10, 0, 0, TimeSpan.Zero);
var expiresAt = new DateTimeOffset(2026, 4, 14, 10, 0, 0, TimeSpan.Zero);
var predicate = new VexOverridePredicateBuilder()
.WithArtifactDigest("sha256:abc123")
.WithVulnerabilityId("CVE-2024-12345")
.WithDecision(VexOverrideDecision.Accepted)
.WithJustification("Temporary acceptance")
.WithDecisionTime(decisionTime)
.WithOperatorId("user@example.com")
.WithExpiresAt(expiresAt)
.Build();
Assert.Equal(expiresAt, predicate.ExpiresAt);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Build_WithRuleDigestAndTraceHash_SetsValues()
{
var predicate = new VexOverridePredicateBuilder()
.WithArtifactDigest("sha256:abc123")
.WithVulnerabilityId("CVE-2024-12345")
.WithDecision(VexOverrideDecision.NotAffected)
.WithJustification("Test")
.WithDecisionTime(DateTimeOffset.UtcNow)
.WithOperatorId("user@example.com")
.WithRuleDigest("sha256:rule123")
.WithTraceHash("sha256:trace456")
.Build();
Assert.Equal("sha256:rule123", predicate.RuleDigest);
Assert.Equal("sha256:trace456", predicate.TraceHash);
}
}

View File

@@ -0,0 +1,255 @@
// -----------------------------------------------------------------------------
// VexOverridePredicateParserTests.cs
// Sprint: SPRINT_20260112_004_ATTESTOR_vex_override_predicate (ATT-VEX-002)
// Description: Tests for VEX override predicate parsing
// -----------------------------------------------------------------------------
using System.Text.Json;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Attestor.StandardPredicates.VexOverride;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Attestor.StandardPredicates.Tests.VexOverride;
public sealed class VexOverridePredicateParserTests
{
private readonly VexOverridePredicateParser _parser;
public VexOverridePredicateParserTests()
{
_parser = new VexOverridePredicateParser(NullLogger<VexOverridePredicateParser>.Instance);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void PredicateType_ReturnsCorrectUri()
{
Assert.Equal(VexOverridePredicateTypes.PredicateTypeUri, _parser.PredicateType);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_ValidPredicate_ReturnsValid()
{
var json = """
{
"artifactDigest": "sha256:abc123",
"vulnerabilityId": "CVE-2024-12345",
"decision": "not_affected",
"justification": "Component is not in use",
"decisionTime": "2026-01-14T10:00:00Z",
"operatorId": "user@example.com"
}
""";
using var document = JsonDocument.Parse(json);
var result = _parser.Parse(document.RootElement);
Assert.True(result.IsValid);
Assert.Empty(result.Errors);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_MissingArtifactDigest_ReturnsError()
{
var json = """
{
"vulnerabilityId": "CVE-2024-12345",
"decision": "not_affected",
"justification": "Component is not in use",
"decisionTime": "2026-01-14T10:00:00Z",
"operatorId": "user@example.com"
}
""";
using var document = JsonDocument.Parse(json);
var result = _parser.Parse(document.RootElement);
Assert.False(result.IsValid);
Assert.Contains(result.Errors, e => e.Code == "VEX_MISSING_ARTIFACT_DIGEST");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_MissingVulnerabilityId_ReturnsError()
{
var json = """
{
"artifactDigest": "sha256:abc123",
"decision": "not_affected",
"justification": "Component is not in use",
"decisionTime": "2026-01-14T10:00:00Z",
"operatorId": "user@example.com"
}
""";
using var document = JsonDocument.Parse(json);
var result = _parser.Parse(document.RootElement);
Assert.False(result.IsValid);
Assert.Contains(result.Errors, e => e.Code == "VEX_MISSING_VULN_ID");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_InvalidDecision_ReturnsError()
{
var json = """
{
"artifactDigest": "sha256:abc123",
"vulnerabilityId": "CVE-2024-12345",
"decision": "invalid_decision",
"justification": "Component is not in use",
"decisionTime": "2026-01-14T10:00:00Z",
"operatorId": "user@example.com"
}
""";
using var document = JsonDocument.Parse(json);
var result = _parser.Parse(document.RootElement);
Assert.False(result.IsValid);
Assert.Contains(result.Errors, e => e.Code == "VEX_INVALID_DECISION");
}
[Trait("Category", TestCategories.Unit)]
[Theory]
[InlineData("not_affected", VexOverrideDecision.NotAffected)]
[InlineData("mitigated", VexOverrideDecision.Mitigated)]
[InlineData("accepted", VexOverrideDecision.Accepted)]
[InlineData("under_investigation", VexOverrideDecision.UnderInvestigation)]
public void Parse_AllDecisionValues_Accepted(string decisionValue, VexOverrideDecision expected)
{
var json = $$"""
{
"artifactDigest": "sha256:abc123",
"vulnerabilityId": "CVE-2024-12345",
"decision": "{{decisionValue}}",
"justification": "Test",
"decisionTime": "2026-01-14T10:00:00Z",
"operatorId": "user@example.com"
}
""";
using var document = JsonDocument.Parse(json);
var result = _parser.Parse(document.RootElement);
Assert.True(result.IsValid);
var predicate = _parser.ParsePredicate(document.RootElement);
Assert.NotNull(predicate);
Assert.Equal(expected, predicate.Decision);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_NumericDecision_Accepted()
{
var json = """
{
"artifactDigest": "sha256:abc123",
"vulnerabilityId": "CVE-2024-12345",
"decision": 1,
"justification": "Test",
"decisionTime": "2026-01-14T10:00:00Z",
"operatorId": "user@example.com"
}
""";
using var document = JsonDocument.Parse(json);
var result = _parser.Parse(document.RootElement);
Assert.True(result.IsValid);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_WithEvidenceRefs_ParsesCorrectly()
{
var json = """
{
"artifactDigest": "sha256:abc123",
"vulnerabilityId": "CVE-2024-12345",
"decision": "not_affected",
"justification": "Test",
"decisionTime": "2026-01-14T10:00:00Z",
"operatorId": "user@example.com",
"evidenceRefs": [
{
"type": "document",
"uri": "https://example.com/doc",
"digest": "sha256:def456",
"description": "Design document"
}
]
}
""";
using var document = JsonDocument.Parse(json);
var result = _parser.Parse(document.RootElement);
Assert.True(result.IsValid);
var predicate = _parser.ParsePredicate(document.RootElement);
Assert.NotNull(predicate);
Assert.Single(predicate.EvidenceRefs);
Assert.Equal("document", predicate.EvidenceRefs[0].Type);
Assert.Equal("https://example.com/doc", predicate.EvidenceRefs[0].Uri);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_WithTool_ParsesCorrectly()
{
var json = """
{
"artifactDigest": "sha256:abc123",
"vulnerabilityId": "CVE-2024-12345",
"decision": "mitigated",
"justification": "Compensating control applied",
"decisionTime": "2026-01-14T10:00:00Z",
"operatorId": "user@example.com",
"tool": {
"name": "StellaOps",
"version": "1.0.0",
"vendor": "StellaOps Inc"
}
}
""";
using var document = JsonDocument.Parse(json);
var result = _parser.Parse(document.RootElement);
Assert.True(result.IsValid);
var predicate = _parser.ParsePredicate(document.RootElement);
Assert.NotNull(predicate);
Assert.NotNull(predicate.Tool);
Assert.Equal("StellaOps", predicate.Tool.Name);
Assert.Equal("1.0.0", predicate.Tool.Version);
Assert.Equal("StellaOps Inc", predicate.Tool.Vendor);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ExtractSbom_ReturnsNull()
{
var json = """
{
"artifactDigest": "sha256:abc123",
"vulnerabilityId": "CVE-2024-12345",
"decision": "not_affected",
"justification": "Test",
"decisionTime": "2026-01-14T10:00:00Z",
"operatorId": "user@example.com"
}
""";
using var document = JsonDocument.Parse(json);
var result = _parser.ExtractSbom(document.RootElement);
Assert.Null(result);
}
}