save progress

This commit is contained in:
StellaOps Bot
2026-01-03 00:47:24 +02:00
parent 3f197814c5
commit ca578801fd
319 changed files with 32478 additions and 2202 deletions

View File

@@ -0,0 +1,429 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Sprint: SPRINT_20260102_001_BE
// Task: DS-041 - VEX evidence emission for backport detection
using System.Collections.Immutable;
using StellaOps.Scanner.Evidence.Models;
namespace StellaOps.Scanner.Evidence.Tests;
[Trait("Category", "Unit")]
public sealed class DeltaSigVexEmitterTests
{
private readonly FakeTimeProvider _timeProvider = new();
private readonly DeltaSigVexEmitter _emitter;
public DeltaSigVexEmitterTests()
{
_timeProvider.SetUtcNow(new DateTimeOffset(2026, 1, 3, 12, 0, 0, TimeSpan.Zero));
_emitter = new DeltaSigVexEmitter(timeProvider: _timeProvider);
}
[Fact]
public void EmitCandidates_PatchedBinary_EmitsCandidate()
{
// Arrange
var evidence = CreatePatchedEvidence(
cveId: "CVE-2025-12345",
confidence: 0.95m);
var context = CreateContext([evidence]);
// Act
var result = _emitter.EmitCandidates(context);
// Assert
Assert.Equal(1, result.CandidatesEmitted);
Assert.Single(result.Candidates);
var candidate = result.Candidates[0];
Assert.Equal(DeltaSigVexStatus.NotAffected, candidate.SuggestedStatus);
Assert.Equal(DeltaSigVexJustification.VulnerableCodeNotPresent, candidate.Justification);
Assert.Contains("CVE-2025-12345", candidate.CveIds);
Assert.Equal(0.95m, candidate.Confidence);
}
[Fact]
public void EmitCandidates_VulnerableBinary_DoesNotEmitCandidate()
{
// Arrange
var evidence = CreateVulnerableEvidence("CVE-2025-12345");
var context = CreateContext([evidence]);
// Act
var result = _emitter.EmitCandidates(context);
// Assert
Assert.Equal(0, result.CandidatesEmitted);
Assert.Empty(result.Candidates);
}
[Fact]
public void EmitCandidates_InconclusiveAnalysis_DoesNotEmitCandidate()
{
// Arrange
var evidence = CreateInconclusiveEvidence("CVE-2025-12345");
var context = CreateContext([evidence]);
// Act
var result = _emitter.EmitCandidates(context);
// Assert
Assert.Equal(0, result.CandidatesEmitted);
Assert.Empty(result.Candidates);
}
[Fact]
public void EmitCandidates_LowConfidence_DoesNotEmitCandidate()
{
// Arrange
var evidence = CreatePatchedEvidence(
cveId: "CVE-2025-12345",
confidence: 0.50m); // Below default threshold of 0.75
var context = CreateContext([evidence]);
// Act
var result = _emitter.EmitCandidates(context);
// Assert
Assert.Equal(0, result.CandidatesEmitted);
Assert.Empty(result.Candidates);
}
[Fact]
public void EmitCandidates_CustomMinConfidence_RespectsThreshold()
{
// Arrange
var options = new DeltaSigVexEmitterOptions { MinConfidence = 0.40m };
var emitter = new DeltaSigVexEmitter(options, _timeProvider);
var evidence = CreatePatchedEvidence(
cveId: "CVE-2025-12345",
confidence: 0.50m);
var context = CreateContext([evidence]);
// Act
var result = emitter.EmitCandidates(context);
// Assert
Assert.Equal(1, result.CandidatesEmitted);
}
[Fact]
public void EmitCandidates_HighConfidence_DoesNotRequireReview()
{
// Arrange
var evidence = CreatePatchedEvidence(
cveId: "CVE-2025-12345",
confidence: 0.99m); // Above auto-approval threshold of 0.95
var context = CreateContext([evidence]);
// Act
var result = _emitter.EmitCandidates(context);
// Assert
Assert.Single(result.Candidates);
Assert.False(result.Candidates[0].RequiresReview);
}
[Fact]
public void EmitCandidates_MediumConfidence_RequiresReview()
{
// Arrange
var evidence = CreatePatchedEvidence(
cveId: "CVE-2025-12345",
confidence: 0.85m); // Between min and auto-approval thresholds
var context = CreateContext([evidence]);
// Act
var result = _emitter.EmitCandidates(context);
// Assert
Assert.Single(result.Candidates);
Assert.True(result.Candidates[0].RequiresReview);
}
[Fact]
public void EmitCandidates_MultiplePatchedBinaries_EmitsMultipleCandidates()
{
// Arrange
var evidence1 = CreatePatchedEvidence("CVE-2025-0001", confidence: 0.90m);
var evidence2 = CreatePatchedEvidence("CVE-2025-0002", confidence: 0.85m);
var evidence3 = CreateVulnerableEvidence("CVE-2025-0003"); // Should be skipped
var context = CreateContext([evidence1, evidence2, evidence3]);
// Act
var result = _emitter.EmitCandidates(context);
// Assert
Assert.Equal(2, result.CandidatesEmitted);
Assert.All(result.Candidates, c => Assert.Equal(DeltaSigVexStatus.NotAffected, c.SuggestedStatus));
}
[Fact]
public void EmitCandidates_RespectsMaxCandidatesLimit()
{
// Arrange
var options = new DeltaSigVexEmitterOptions { MaxCandidatesPerBatch = 2 };
var emitter = new DeltaSigVexEmitter(options, _timeProvider);
var evidenceList = Enumerable.Range(1, 10)
.Select(i => CreatePatchedEvidence($"CVE-2025-{i:D4}", confidence: 0.90m))
.ToList();
var context = CreateContext(evidenceList);
// Act
var result = emitter.EmitCandidates(context);
// Assert
Assert.Equal(2, result.CandidatesEmitted);
Assert.Equal(2, result.Candidates.Length);
}
[Fact]
public void EmitCandidates_CandidateContainsSymbolMatchEvidence()
{
// Arrange
var symbolMatches = ImmutableArray.Create(
new SymbolMatchEvidence
{
SymbolName = "vulnerable_function",
HashHex = "abc123def456",
State = SignatureState.Patched,
Confidence = 1.0m,
ExactMatch = true
},
new SymbolMatchEvidence
{
SymbolName = "another_function",
HashHex = "789xyz012",
State = SignatureState.Patched,
Confidence = 0.85m,
ExactMatch = false,
ChunksMatched = 17,
ChunksTotal = 20
});
var evidence = DeltaSignatureEvidence.CreatePatched(
cveIds: ["CVE-2025-12345"],
packagePurl: "pkg:rpm/openssl@1.0.1e-30.el6_6?arch=x86_64",
binaryId: "build-id:abc123",
architecture: "x86_64",
symbolMatches: symbolMatches,
confidence: 0.92m,
recipe: new NormalizationRecipeRef
{
RecipeId = "stellaops.normalize.x64.v1",
Version = "1.0.0"
},
timeProvider: _timeProvider);
var context = CreateContext([evidence]);
// Act
var result = _emitter.EmitCandidates(context);
// Assert
Assert.Single(result.Candidates);
var candidate = result.Candidates[0];
// Should have evidence links for patched symbols
Assert.Contains(candidate.EvidenceLinks, e => e.Type == "patched_symbol");
Assert.Equal(2, candidate.EvidenceLinks.Count(e => e.Type == "patched_symbol"));
}
[Fact]
public void EmitCandidates_CandidateHasCorrectExpiration()
{
// Arrange
var options = new DeltaSigVexEmitterOptions { CandidateTtl = TimeSpan.FromDays(14) };
var emitter = new DeltaSigVexEmitter(options, _timeProvider);
var evidence = CreatePatchedEvidence("CVE-2025-12345", confidence: 0.90m);
var context = CreateContext([evidence]);
// Act
var result = emitter.EmitCandidates(context);
// Assert
var candidate = result.Candidates[0];
var expectedExpiry = _timeProvider.GetUtcNow().AddDays(14);
Assert.Equal(expectedExpiry, candidate.ExpiresAt);
}
[Fact]
public void EmitCandidates_GeneratesUniqueCandidateIds()
{
// Arrange
var evidence1 = CreatePatchedEvidence("CVE-2025-0001", confidence: 0.90m);
var evidence2 = CreatePatchedEvidence("CVE-2025-0002", confidence: 0.85m);
var context = CreateContext([evidence1, evidence2]);
// Act
var result = _emitter.EmitCandidates(context);
// Assert
var ids = result.Candidates.Select(c => c.CandidateId).ToList();
Assert.Equal(2, ids.Distinct().Count());
Assert.All(ids, id => Assert.StartsWith("vexds-", id));
}
[Fact]
public void EmitCandidates_RationaleIncludesMatchDetails()
{
// Arrange
var evidence = CreatePatchedEvidence("CVE-2025-12345", confidence: 0.95m);
var context = CreateContext([evidence]);
// Act
var result = _emitter.EmitCandidates(context);
// Assert
var candidate = result.Candidates[0];
Assert.Contains("delta signature analysis", candidate.Rationale.ToLowerInvariant());
Assert.Contains("95", candidate.Rationale); // Confidence percentage
}
[Fact]
public void EmitCandidates_IncludesAttestationLinkWhenPresent()
{
// Arrange
var symbolMatches = ImmutableArray.Create(
new SymbolMatchEvidence
{
SymbolName = "vuln_func",
HashHex = "abc123",
State = SignatureState.Patched,
Confidence = 1.0m,
ExactMatch = true
});
var evidence = DeltaSignatureEvidence.CreatePatched(
cveIds: ["CVE-2025-12345"],
packagePurl: "pkg:rpm/test@1.0.0",
binaryId: "build-id:xyz",
architecture: "x86_64",
symbolMatches: symbolMatches,
confidence: 0.95m,
recipe: new NormalizationRecipeRef
{
RecipeId = "test.recipe",
Version = "1.0.0"
},
timeProvider: _timeProvider) with
{
AttestationUri = "dsse://rekor.example.com/entries/abc123"
};
var context = CreateContext([evidence]);
// Act
var result = _emitter.EmitCandidates(context);
// Assert
var candidate = result.Candidates[0];
Assert.Contains(candidate.EvidenceLinks, e => e.Type == "dsse_attestation");
}
#region Helpers
private DeltaSigVexEmissionContext CreateContext(IReadOnlyList<DeltaSignatureEvidence> evidence)
{
return new DeltaSigVexEmissionContext(
ImageDigest: "sha256:abc123def456",
EvidenceItems: evidence);
}
private DeltaSignatureEvidence CreatePatchedEvidence(string cveId, decimal confidence)
{
var symbolMatches = ImmutableArray.Create(
new SymbolMatchEvidence
{
SymbolName = "vulnerable_function",
HashHex = "abc123def456",
State = SignatureState.Patched,
Confidence = confidence,
ExactMatch = confidence >= 0.90m
});
return DeltaSignatureEvidence.CreatePatched(
cveIds: [cveId],
packagePurl: "pkg:rpm/openssl@1.0.1e-30.el6_6?arch=x86_64",
binaryId: "build-id:abc123",
architecture: "x86_64",
symbolMatches: symbolMatches,
confidence: confidence,
recipe: new NormalizationRecipeRef
{
RecipeId = "stellaops.normalize.x64.v1",
Version = "1.0.0",
Steps = ["zero_addresses", "canonicalize_nops", "normalize_plt"]
},
timeProvider: _timeProvider);
}
private DeltaSignatureEvidence CreateVulnerableEvidence(string cveId)
{
var symbolMatches = ImmutableArray.Create(
new SymbolMatchEvidence
{
SymbolName = "vulnerable_function",
HashHex = "vuln_hash_123",
State = SignatureState.Vulnerable,
Confidence = 0.95m,
ExactMatch = true
});
return DeltaSignatureEvidence.CreateVulnerable(
cveIds: [cveId],
packagePurl: "pkg:rpm/openssl@1.0.1e-30.el6_6?arch=x86_64",
binaryId: "build-id:abc123",
architecture: "x86_64",
symbolMatches: symbolMatches,
confidence: 0.95m,
recipe: new NormalizationRecipeRef
{
RecipeId = "stellaops.normalize.x64.v1",
Version = "1.0.0"
},
timeProvider: _timeProvider);
}
private DeltaSignatureEvidence CreateInconclusiveEvidence(string cveId)
{
return DeltaSignatureEvidence.CreateInconclusive(
cveIds: [cveId],
packagePurl: "pkg:rpm/openssl@1.0.1e-30.el6_6?arch=x86_64",
binaryId: "build-id:abc123",
architecture: "x86_64",
reason: "Target symbols not found in binary",
symbolMatches: [],
recipe: new NormalizationRecipeRef
{
RecipeId = "stellaops.normalize.x64.v1",
Version = "1.0.0"
},
timeProvider: _timeProvider);
}
#endregion
}
/// <summary>
/// Fake TimeProvider for deterministic testing.
/// </summary>
internal sealed class FakeTimeProvider : TimeProvider
{
private DateTimeOffset _utcNow = DateTimeOffset.UtcNow;
public void SetUtcNow(DateTimeOffset value) => _utcNow = value;
public override DateTimeOffset GetUtcNow() => _utcNow;
}

View File

@@ -0,0 +1,306 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Sprint: SPRINT_20260102_001_BE
// Task: DS-041 - VEX evidence emission for backport detection
using System.Collections.Immutable;
using System.Text.Json;
using StellaOps.Scanner.Evidence.Models;
namespace StellaOps.Scanner.Evidence.Tests;
[Trait("Category", "Unit")]
public sealed class DeltaSignatureEvidenceTests
{
private readonly FakeTimeProvider _timeProvider = new();
public DeltaSignatureEvidenceTests()
{
_timeProvider.SetUtcNow(new DateTimeOffset(2026, 1, 3, 12, 0, 0, TimeSpan.Zero));
}
[Fact]
public void CreatePatched_ReturnsCorrectResult()
{
// Arrange
var symbolMatches = CreateSymbolMatches(SignatureState.Patched);
// Act
var evidence = DeltaSignatureEvidence.CreatePatched(
cveIds: ["CVE-2025-12345"],
packagePurl: "pkg:rpm/openssl@1.0.1e",
binaryId: "build-id:abc123",
architecture: "x86_64",
symbolMatches: symbolMatches,
confidence: 0.95m,
recipe: CreateRecipe(),
timeProvider: _timeProvider);
// Assert
Assert.Equal(DeltaSigResult.Patched, evidence.Result);
Assert.Contains("PATCHED", evidence.Summary);
Assert.Contains("95", evidence.Summary);
Assert.Equal(0.95m, evidence.Confidence);
}
[Fact]
public void CreateVulnerable_ReturnsCorrectResult()
{
// Arrange
var symbolMatches = CreateSymbolMatches(SignatureState.Vulnerable);
// Act
var evidence = DeltaSignatureEvidence.CreateVulnerable(
cveIds: ["CVE-2025-12345"],
packagePurl: "pkg:rpm/openssl@1.0.1e",
binaryId: "build-id:abc123",
architecture: "x86_64",
symbolMatches: symbolMatches,
confidence: 0.90m,
recipe: CreateRecipe(),
timeProvider: _timeProvider);
// Assert
Assert.Equal(DeltaSigResult.Vulnerable, evidence.Result);
Assert.Contains("VULNERABLE", evidence.Summary);
Assert.Contains("90", evidence.Summary);
}
[Fact]
public void CreateInconclusive_ReturnsCorrectResult()
{
// Arrange & Act
var evidence = DeltaSignatureEvidence.CreateInconclusive(
cveIds: ["CVE-2025-12345"],
packagePurl: "pkg:rpm/openssl@1.0.1e",
binaryId: "build-id:abc123",
architecture: "x86_64",
reason: "Symbol not found",
symbolMatches: [],
recipe: CreateRecipe(),
timeProvider: _timeProvider);
// Assert
Assert.Equal(DeltaSigResult.Inconclusive, evidence.Result);
Assert.Contains("INCONCLUSIVE", evidence.Summary);
Assert.Contains("Symbol not found", evidence.Summary);
Assert.Equal(0m, evidence.Confidence);
}
[Fact]
public void Evidence_SerializesToJson()
{
// Arrange
var evidence = DeltaSignatureEvidence.CreatePatched(
cveIds: ["CVE-2025-12345", "CVE-2025-67890"],
packagePurl: "pkg:rpm/openssl@1.0.1e",
binaryId: "build-id:abc123",
architecture: "x86_64",
symbolMatches: CreateSymbolMatches(SignatureState.Patched),
confidence: 0.95m,
recipe: CreateRecipe(),
timeProvider: _timeProvider);
// Act
var json = JsonSerializer.Serialize(evidence);
var deserialized = JsonSerializer.Deserialize<DeltaSignatureEvidence>(json);
// Assert
Assert.NotNull(deserialized);
Assert.Equal(evidence.Result, deserialized.Result);
Assert.Equal(evidence.CveIds, deserialized.CveIds);
Assert.Equal(evidence.PackagePurl, deserialized.PackagePurl);
Assert.Equal(evidence.Confidence, deserialized.Confidence);
}
[Fact]
public void DeltaSigResult_SerializesAsString()
{
// Arrange
var evidence = DeltaSignatureEvidence.CreatePatched(
cveIds: ["CVE-2025-12345"],
packagePurl: "pkg:rpm/test@1.0.0",
binaryId: "test",
architecture: "x86_64",
symbolMatches: [],
confidence: 0.95m,
recipe: CreateRecipe(),
timeProvider: _timeProvider);
// Act
var json = JsonSerializer.Serialize(evidence);
// Assert
Assert.Contains("\"patched\"", json);
}
[Fact]
public void SignatureState_SerializesAsString()
{
// Arrange
var match = new SymbolMatchEvidence
{
SymbolName = "test_func",
HashHex = "abc123",
State = SignatureState.Patched,
Confidence = 1.0m,
ExactMatch = true
};
// Act
var json = JsonSerializer.Serialize(match);
// Assert
Assert.Contains("\"patched\"", json);
}
[Fact]
public void SymbolMatchEvidence_SerializesChunkInfo()
{
// Arrange
var match = new SymbolMatchEvidence
{
SymbolName = "partial_match_func",
HashHex = "xyz789",
State = SignatureState.Patched,
Confidence = 0.85m,
ExactMatch = false,
ChunksMatched = 17,
ChunksTotal = 20
};
// Act
var json = JsonSerializer.Serialize(match);
var deserialized = JsonSerializer.Deserialize<SymbolMatchEvidence>(json);
// Assert
Assert.NotNull(deserialized);
Assert.Equal(17, deserialized.ChunksMatched);
Assert.Equal(20, deserialized.ChunksTotal);
Assert.False(deserialized.ExactMatch);
}
[Fact]
public void NormalizationRecipeRef_SerializesSteps()
{
// Arrange
var recipe = new NormalizationRecipeRef
{
RecipeId = "stellaops.normalize.x64.v1",
Version = "1.0.0",
Steps = ["zero_addresses", "canonicalize_nops", "normalize_plt_got"]
};
// Act
var json = JsonSerializer.Serialize(recipe);
var deserialized = JsonSerializer.Deserialize<NormalizationRecipeRef>(json);
// Assert
Assert.NotNull(deserialized);
Assert.Equal(3, deserialized.Steps.Length);
Assert.Contains("zero_addresses", deserialized.Steps);
}
[Fact]
public void Evidence_WithAttestationUri_SerializesCorrectly()
{
// Arrange
var evidence = DeltaSignatureEvidence.CreatePatched(
cveIds: ["CVE-2025-12345"],
packagePurl: "pkg:rpm/test@1.0.0",
binaryId: "test",
architecture: "x86_64",
symbolMatches: [],
confidence: 0.95m,
recipe: CreateRecipe(),
timeProvider: _timeProvider) with
{
AttestationUri = "dsse://rekor.sigstore.dev/entries/12345"
};
// Act
var json = JsonSerializer.Serialize(evidence);
// Assert
Assert.Contains("attestationUri", json);
Assert.Contains("rekor.sigstore.dev", json);
}
[Fact]
public void Evidence_SchemaVersionIsSet()
{
// Arrange & Act
var evidence = DeltaSignatureEvidence.CreatePatched(
cveIds: ["CVE-2025-12345"],
packagePurl: "pkg:rpm/test@1.0.0",
binaryId: "test",
architecture: "x86_64",
symbolMatches: [],
confidence: 0.95m,
recipe: CreateRecipe(),
timeProvider: _timeProvider);
// Assert
Assert.Equal("stellaops.evidence.deltasig.v1", evidence.Schema);
}
[Fact]
public void Evidence_GeneratedAtIsSet()
{
// Arrange
var expectedTime = new DateTimeOffset(2026, 1, 3, 12, 0, 0, TimeSpan.Zero);
_timeProvider.SetUtcNow(expectedTime);
// Act
var evidence = DeltaSignatureEvidence.CreatePatched(
cveIds: ["CVE-2025-12345"],
packagePurl: "pkg:rpm/test@1.0.0",
binaryId: "test",
architecture: "x86_64",
symbolMatches: [],
confidence: 0.95m,
recipe: CreateRecipe(),
timeProvider: _timeProvider);
// Assert
Assert.Equal(expectedTime, evidence.GeneratedAt);
}
#region Helpers
private static ImmutableArray<SymbolMatchEvidence> CreateSymbolMatches(SignatureState state)
{
return
[
new SymbolMatchEvidence
{
SymbolName = "vulnerable_function",
HashHex = "abc123def456",
State = state,
Confidence = 0.95m,
ExactMatch = true
},
new SymbolMatchEvidence
{
SymbolName = "another_function",
HashHex = "789xyz012",
State = state,
Confidence = 0.85m,
ExactMatch = false,
ChunksMatched = 17,
ChunksTotal = 20
}
];
}
private static NormalizationRecipeRef CreateRecipe()
{
return new NormalizationRecipeRef
{
RecipeId = "stellaops.normalize.x64.v1",
Version = "1.0.0",
Steps = ["zero_addresses", "canonicalize_nops"]
};
}
#endregion
}