release orchestrator v1 draft and build fixes
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// ChangeTraceAttestationService.cs
|
||||
// Sprint: SPRINT_20260112_200_005_ATTEST_predicate
|
||||
// Description: Service for generating change trace DSSE attestations.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Attestor.ProofChain.Predicates;
|
||||
using StellaOps.Attestor.ProofChain.Signing;
|
||||
using StellaOps.Attestor.ProofChain.Statements;
|
||||
using StellaOps.Scanner.ChangeTrace.Models;
|
||||
|
||||
using ChangeTraceModel = StellaOps.Scanner.ChangeTrace.Models.ChangeTrace;
|
||||
using DsseEnvelope = StellaOps.Attestor.ProofChain.Signing.DsseEnvelope;
|
||||
|
||||
namespace StellaOps.Attestor.ProofChain.ChangeTrace;
|
||||
|
||||
/// <summary>
|
||||
/// Service for generating change trace DSSE attestations.
|
||||
/// </summary>
|
||||
public sealed class ChangeTraceAttestationService : IChangeTraceAttestationService
|
||||
{
|
||||
private readonly IProofChainSigner _signer;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new change trace attestation service.
|
||||
/// </summary>
|
||||
/// <param name="signer">Proof chain signer for envelope generation.</param>
|
||||
/// <param name="timeProvider">Time provider for timestamp generation.</param>
|
||||
public ChangeTraceAttestationService(
|
||||
IProofChainSigner signer,
|
||||
TimeProvider timeProvider)
|
||||
{
|
||||
_signer = signer ?? throw new ArgumentNullException(nameof(signer));
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<DsseEnvelope> GenerateAttestationAsync(
|
||||
ChangeTraceModel trace,
|
||||
ChangeTraceAttestationOptions? options = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(trace);
|
||||
options ??= ChangeTraceAttestationOptions.Default;
|
||||
|
||||
var predicate = MapToPredicate(trace, options);
|
||||
var statement = CreateStatement(trace, predicate);
|
||||
|
||||
return await _signer.SignStatementAsync(
|
||||
statement,
|
||||
SigningKeyProfile.Evidence,
|
||||
ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Map a change trace model to its attestation predicate.
|
||||
/// </summary>
|
||||
private ChangeTracePredicate MapToPredicate(
|
||||
ChangeTraceModel trace,
|
||||
ChangeTraceAttestationOptions options)
|
||||
{
|
||||
var deltas = trace.Deltas
|
||||
.Take(options.MaxDeltas)
|
||||
.Select(d => new ChangeTraceDeltaEntry
|
||||
{
|
||||
Purl = d.Purl,
|
||||
FromVersion = d.FromVersion,
|
||||
ToVersion = d.ToVersion,
|
||||
ChangeType = d.ChangeType.ToString(),
|
||||
Explain = d.Explain.ToString(),
|
||||
SymbolsChanged = d.Evidence.SymbolsChanged,
|
||||
BytesChanged = d.Evidence.BytesChanged,
|
||||
Confidence = d.Evidence.Confidence,
|
||||
TrustDeltaScore = d.TrustDelta?.Score ?? 0,
|
||||
CveIds = d.Evidence.CveIds,
|
||||
Functions = d.Evidence.Functions
|
||||
})
|
||||
.ToImmutableArray();
|
||||
|
||||
var proofSteps = trace.Deltas
|
||||
.Where(d => d.TrustDelta is not null)
|
||||
.SelectMany(d => d.TrustDelta!.ProofSteps)
|
||||
.Distinct()
|
||||
.Take(options.MaxProofSteps)
|
||||
.ToImmutableArray();
|
||||
|
||||
var aggregateReachability = AggregateReachabilityImpact(trace.Deltas);
|
||||
var aggregateExploitability = DetermineExploitabilityFromScore(trace.Summary.RiskDelta);
|
||||
|
||||
return new ChangeTracePredicate
|
||||
{
|
||||
FromDigest = trace.Basis.FromScanId ?? trace.Subject.Digest,
|
||||
ToDigest = trace.Basis.ToScanId ?? trace.Subject.Digest,
|
||||
TenantId = options.TenantId,
|
||||
Deltas = deltas,
|
||||
Summary = new ChangeTracePredicateSummary
|
||||
{
|
||||
ChangedPackages = trace.Summary.ChangedPackages,
|
||||
ChangedSymbols = trace.Summary.ChangedSymbols,
|
||||
ChangedBytes = trace.Summary.ChangedBytes,
|
||||
RiskDelta = trace.Summary.RiskDelta,
|
||||
Verdict = trace.Summary.Verdict.ToString().ToLowerInvariant()
|
||||
},
|
||||
TrustDelta = new TrustDeltaRecord
|
||||
{
|
||||
Score = trace.Summary.RiskDelta,
|
||||
BeforeScore = trace.Summary.BeforeRiskScore,
|
||||
AfterScore = trace.Summary.AfterRiskScore,
|
||||
ReachabilityImpact = aggregateReachability.ToString().ToLowerInvariant(),
|
||||
ExploitabilityImpact = aggregateExploitability.ToString().ToLowerInvariant()
|
||||
},
|
||||
ProofSteps = proofSteps,
|
||||
DiffMethods = trace.Basis.DiffMethod,
|
||||
Policies = trace.Basis.Policies,
|
||||
AnalyzedAt = trace.Basis.AnalyzedAt,
|
||||
AlgorithmVersion = trace.Basis.EngineVersion,
|
||||
CommitmentHash = trace.Commitment?.Sha256
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create an in-toto statement from the change trace and predicate.
|
||||
/// </summary>
|
||||
private ChangeTraceStatement CreateStatement(
|
||||
ChangeTraceModel trace,
|
||||
ChangeTracePredicate predicate)
|
||||
{
|
||||
var subjectName = trace.Subject.Purl ?? trace.Subject.Name ?? trace.Subject.Digest;
|
||||
var digest = ParseDigest(trace.Subject.Digest);
|
||||
|
||||
return new ChangeTraceStatement
|
||||
{
|
||||
Subject =
|
||||
[
|
||||
new Subject
|
||||
{
|
||||
Name = subjectName,
|
||||
Digest = digest
|
||||
}
|
||||
],
|
||||
Predicate = predicate
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a digest string into a dictionary of algorithm:value pairs.
|
||||
/// </summary>
|
||||
private static IReadOnlyDictionary<string, string> ParseDigest(string digestString)
|
||||
{
|
||||
var result = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
|
||||
if (string.IsNullOrEmpty(digestString))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
// Handle "algorithm:value" format
|
||||
var colonIndex = digestString.IndexOf(':', StringComparison.Ordinal);
|
||||
if (colonIndex > 0)
|
||||
{
|
||||
var algorithm = digestString[..colonIndex];
|
||||
var value = digestString[(colonIndex + 1)..];
|
||||
result[algorithm] = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Assume SHA-256 if no algorithm prefix
|
||||
result["sha256"] = digestString;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Aggregate reachability impact from multiple deltas.
|
||||
/// </summary>
|
||||
private static ReachabilityImpact AggregateReachabilityImpact(
|
||||
ImmutableArray<PackageDelta> deltas)
|
||||
{
|
||||
// Priority: Introduced > Increased > Reduced > Eliminated > Unchanged
|
||||
if (deltas.Any(d => d.TrustDelta?.ReachabilityImpact == ReachabilityImpact.Introduced))
|
||||
return ReachabilityImpact.Introduced;
|
||||
if (deltas.Any(d => d.TrustDelta?.ReachabilityImpact == ReachabilityImpact.Increased))
|
||||
return ReachabilityImpact.Increased;
|
||||
if (deltas.Any(d => d.TrustDelta?.ReachabilityImpact == ReachabilityImpact.Reduced))
|
||||
return ReachabilityImpact.Reduced;
|
||||
if (deltas.Any(d => d.TrustDelta?.ReachabilityImpact == ReachabilityImpact.Eliminated))
|
||||
return ReachabilityImpact.Eliminated;
|
||||
return ReachabilityImpact.Unchanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determine exploitability impact from overall risk delta score.
|
||||
/// </summary>
|
||||
private static ExploitabilityImpact DetermineExploitabilityFromScore(double riskDelta)
|
||||
{
|
||||
return riskDelta switch
|
||||
{
|
||||
<= -0.5 => ExploitabilityImpact.Eliminated,
|
||||
< -0.3 => ExploitabilityImpact.Down,
|
||||
>= 0.5 => ExploitabilityImpact.Introduced,
|
||||
> 0.3 => ExploitabilityImpact.Up,
|
||||
_ => ExploitabilityImpact.Unchanged
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// IChangeTraceAttestationService.cs
|
||||
// Sprint: SPRINT_20260112_200_005_ATTEST_predicate
|
||||
// Description: Interface for generating change trace attestations.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using StellaOps.Attestor.ProofChain.Signing;
|
||||
|
||||
namespace StellaOps.Attestor.ProofChain.ChangeTrace;
|
||||
|
||||
/// <summary>
|
||||
/// Service for generating change trace DSSE attestations.
|
||||
/// </summary>
|
||||
public interface IChangeTraceAttestationService
|
||||
{
|
||||
/// <summary>
|
||||
/// Generate DSSE envelope for a change trace.
|
||||
/// </summary>
|
||||
/// <param name="trace">The change trace to attest.</param>
|
||||
/// <param name="options">Optional attestation options.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>DSSE envelope containing the change trace attestation.</returns>
|
||||
Task<DsseEnvelope> GenerateAttestationAsync(
|
||||
Scanner.ChangeTrace.Models.ChangeTrace trace,
|
||||
ChangeTraceAttestationOptions? options = null,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for change trace attestation generation.
|
||||
/// </summary>
|
||||
public sealed record ChangeTraceAttestationOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Default attestation options.
|
||||
/// </summary>
|
||||
public static readonly ChangeTraceAttestationOptions Default = new();
|
||||
|
||||
/// <summary>
|
||||
/// Tenant ID for multi-tenant isolation.
|
||||
/// </summary>
|
||||
public string TenantId { get; init; } = "default";
|
||||
|
||||
/// <summary>
|
||||
/// Whether to include raw trace in attestation metadata.
|
||||
/// </summary>
|
||||
public bool IncludeRawTrace { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum number of proof steps to include.
|
||||
/// </summary>
|
||||
public int MaxProofSteps { get; init; } = 50;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum number of deltas to include in the predicate.
|
||||
/// </summary>
|
||||
public int MaxDeltas { get; init; } = 1000;
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// ChangeTracePredicate.cs
|
||||
// Sprint: SPRINT_20260112_200_005_ATTEST_predicate
|
||||
// Description: DSSE predicate for change trace attestations.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Attestor.ProofChain.Predicates;
|
||||
|
||||
/// <summary>
|
||||
/// DSSE predicate for change trace attestations.
|
||||
/// predicateType: stella.ops/changetrace@v1
|
||||
/// </summary>
|
||||
public sealed record ChangeTracePredicate
|
||||
{
|
||||
/// <summary>
|
||||
/// Predicate type URI for change trace attestations.
|
||||
/// </summary>
|
||||
public const string PredicateTypeUri = "stella.ops/changetrace@v1";
|
||||
|
||||
/// <summary>
|
||||
/// Digest of the "from" artifact.
|
||||
/// </summary>
|
||||
[JsonPropertyName("fromDigest")]
|
||||
public required string FromDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Digest of the "to" artifact.
|
||||
/// </summary>
|
||||
[JsonPropertyName("toDigest")]
|
||||
public required string ToDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Tenant ID for multi-tenant isolation.
|
||||
/// </summary>
|
||||
[JsonPropertyName("tenantId")]
|
||||
public required string TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Package-level deltas.
|
||||
/// </summary>
|
||||
[JsonPropertyName("deltas")]
|
||||
public ImmutableArray<ChangeTraceDeltaEntry> Deltas { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Summary of all changes.
|
||||
/// </summary>
|
||||
[JsonPropertyName("summary")]
|
||||
public required ChangeTracePredicateSummary Summary { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Trust delta with proof steps.
|
||||
/// </summary>
|
||||
[JsonPropertyName("trustDelta")]
|
||||
public required TrustDeltaRecord TrustDelta { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Human-readable proof steps explaining the verdict.
|
||||
/// </summary>
|
||||
[JsonPropertyName("proofSteps")]
|
||||
public ImmutableArray<string> ProofSteps { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Diff methods used (pkg, symbol, byte).
|
||||
/// </summary>
|
||||
[JsonPropertyName("diffMethods")]
|
||||
public ImmutableArray<string> DiffMethods { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Lattice policies applied.
|
||||
/// </summary>
|
||||
[JsonPropertyName("policies")]
|
||||
public ImmutableArray<string> Policies { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// When the analysis was performed.
|
||||
/// </summary>
|
||||
[JsonPropertyName("analyzedAt")]
|
||||
public required DateTimeOffset AnalyzedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Algorithm/engine version for reproducibility.
|
||||
/// </summary>
|
||||
[JsonPropertyName("algorithmVersion")]
|
||||
public string AlgorithmVersion { get; init; } = "1.0";
|
||||
|
||||
/// <summary>
|
||||
/// Commitment hash for deterministic verification.
|
||||
/// </summary>
|
||||
[JsonPropertyName("commitmentHash")]
|
||||
public string? CommitmentHash { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delta entry within the change trace predicate.
|
||||
/// </summary>
|
||||
public sealed record ChangeTraceDeltaEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Package URL (PURL) of the changed package.
|
||||
/// </summary>
|
||||
[JsonPropertyName("purl")]
|
||||
public required string Purl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Version before the change.
|
||||
/// </summary>
|
||||
[JsonPropertyName("fromVersion")]
|
||||
public required string FromVersion { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Version after the change.
|
||||
/// </summary>
|
||||
[JsonPropertyName("toVersion")]
|
||||
public required string ToVersion { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Type of change (Added, Removed, Modified, Upgraded, Downgraded, Rebuilt).
|
||||
/// </summary>
|
||||
[JsonPropertyName("changeType")]
|
||||
public required string ChangeType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Explanation of the change reason.
|
||||
/// </summary>
|
||||
[JsonPropertyName("explain")]
|
||||
public required string Explain { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of symbols changed in this package.
|
||||
/// </summary>
|
||||
[JsonPropertyName("symbolsChanged")]
|
||||
public int SymbolsChanged { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Total bytes changed in this package.
|
||||
/// </summary>
|
||||
[JsonPropertyName("bytesChanged")]
|
||||
public long BytesChanged { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Confidence score for the change classification (0.0-1.0).
|
||||
/// </summary>
|
||||
[JsonPropertyName("confidence")]
|
||||
public double Confidence { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Trust delta score for this specific package change.
|
||||
/// </summary>
|
||||
[JsonPropertyName("trustDeltaScore")]
|
||||
public double TrustDeltaScore { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// CVE identifiers addressed by this change.
|
||||
/// </summary>
|
||||
[JsonPropertyName("cveIds")]
|
||||
public ImmutableArray<string> CveIds { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Function names affected by this change.
|
||||
/// </summary>
|
||||
[JsonPropertyName("functions")]
|
||||
public ImmutableArray<string> Functions { get; init; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Summary within the change trace predicate.
|
||||
/// </summary>
|
||||
public sealed record ChangeTracePredicateSummary
|
||||
{
|
||||
/// <summary>
|
||||
/// Total number of packages with changes.
|
||||
/// </summary>
|
||||
[JsonPropertyName("changedPackages")]
|
||||
public required int ChangedPackages { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Total number of symbols with changes.
|
||||
/// </summary>
|
||||
[JsonPropertyName("changedSymbols")]
|
||||
public required int ChangedSymbols { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Total bytes changed across all packages.
|
||||
/// </summary>
|
||||
[JsonPropertyName("changedBytes")]
|
||||
public required long ChangedBytes { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Aggregated risk delta score.
|
||||
/// </summary>
|
||||
[JsonPropertyName("riskDelta")]
|
||||
public required double RiskDelta { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Overall verdict (risk_down, neutral, risk_up, inconclusive).
|
||||
/// </summary>
|
||||
[JsonPropertyName("verdict")]
|
||||
public required string Verdict { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Trust delta record within the predicate.
|
||||
/// </summary>
|
||||
public sealed record TrustDeltaRecord
|
||||
{
|
||||
/// <summary>
|
||||
/// Overall trust delta score (-1.0 to +1.0).
|
||||
/// </summary>
|
||||
[JsonPropertyName("score")]
|
||||
public required double Score { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Trust score before the change.
|
||||
/// </summary>
|
||||
[JsonPropertyName("beforeScore")]
|
||||
public double? BeforeScore { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Trust score after the change.
|
||||
/// </summary>
|
||||
[JsonPropertyName("afterScore")]
|
||||
public double? AfterScore { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Impact on code reachability (unchanged, reduced, increased, eliminated, introduced).
|
||||
/// </summary>
|
||||
[JsonPropertyName("reachabilityImpact")]
|
||||
public required string ReachabilityImpact { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Impact on exploitability (unchanged, down, up, eliminated, introduced).
|
||||
/// </summary>
|
||||
[JsonPropertyName("exploitabilityImpact")]
|
||||
public required string ExploitabilityImpact { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// ChangeTraceStatement.cs
|
||||
// Sprint: SPRINT_20260112_200_005_ATTEST_predicate
|
||||
// Description: In-toto statement for change trace attestations.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
using StellaOps.Attestor.ProofChain.Predicates;
|
||||
|
||||
namespace StellaOps.Attestor.ProofChain.Statements;
|
||||
|
||||
/// <summary>
|
||||
/// In-toto statement for change trace attestations.
|
||||
/// Predicate type: stella.ops/changetrace@v1
|
||||
/// </summary>
|
||||
public sealed record ChangeTraceStatement : InTotoStatement
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[JsonPropertyName("predicateType")]
|
||||
public override string PredicateType => ChangeTracePredicate.PredicateTypeUri;
|
||||
|
||||
/// <summary>
|
||||
/// The change trace predicate payload.
|
||||
/// </summary>
|
||||
[JsonPropertyName("predicate")]
|
||||
public required ChangeTracePredicate Predicate { get; init; }
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
<ProjectReference Include="..\..\..\Feedser\StellaOps.Feedser.Core\StellaOps.Feedser.Core.csproj" />
|
||||
<ProjectReference Include="..\..\..\Feedser\StellaOps.Feedser.BinaryAnalysis\StellaOps.Feedser.BinaryAnalysis.csproj" />
|
||||
<ProjectReference Include="..\..\..\Concelier\__Libraries\StellaOps.Concelier.SourceIntel\StellaOps.Concelier.SourceIntel.csproj" />
|
||||
<ProjectReference Include="..\..\..\Scanner\__Libraries\StellaOps.Scanner.ChangeTrace\StellaOps.Scanner.ChangeTrace.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// ChangeTracePredicateTests.cs
|
||||
// Sprint: SPRINT_20260112_200_005_ATTEST_predicate
|
||||
// Description: Tests for ChangeTracePredicate serialization.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Attestor.ProofChain.Predicates;
|
||||
|
||||
namespace StellaOps.Attestor.ProofChain.Tests.ChangeTrace;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for ChangeTracePredicate serialization and deserialization.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class ChangeTracePredicateTests
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void PredicateTypeUri_IsCorrect()
|
||||
{
|
||||
// Assert
|
||||
ChangeTracePredicate.PredicateTypeUri.Should().Be("stella.ops/changetrace@v1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serialize_MinimalPredicate_ProducesValidJson()
|
||||
{
|
||||
// Arrange
|
||||
var predicate = new ChangeTracePredicate
|
||||
{
|
||||
FromDigest = "sha256:abc123",
|
||||
ToDigest = "sha256:def456",
|
||||
TenantId = "tenant-1",
|
||||
Summary = new ChangeTracePredicateSummary
|
||||
{
|
||||
ChangedPackages = 5,
|
||||
ChangedSymbols = 20,
|
||||
ChangedBytes = 1024,
|
||||
RiskDelta = -0.25,
|
||||
Verdict = "risk_down"
|
||||
},
|
||||
TrustDelta = new TrustDeltaRecord
|
||||
{
|
||||
Score = -0.25,
|
||||
ReachabilityImpact = "reduced",
|
||||
ExploitabilityImpact = "down"
|
||||
},
|
||||
AnalyzedAt = new DateTimeOffset(2026, 1, 12, 14, 30, 0, TimeSpan.Zero)
|
||||
};
|
||||
|
||||
// Act
|
||||
var json = JsonSerializer.Serialize(predicate, JsonOptions);
|
||||
|
||||
// Assert
|
||||
json.Should().NotBeNullOrEmpty();
|
||||
json.Should().Contain("\"fromDigest\"");
|
||||
json.Should().Contain("\"toDigest\"");
|
||||
json.Should().Contain("\"tenantId\"");
|
||||
json.Should().Contain("\"summary\"");
|
||||
json.Should().Contain("\"trustDelta\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RoundTrip_FullPredicate_PreservesAllData()
|
||||
{
|
||||
// Arrange
|
||||
var predicate = new ChangeTracePredicate
|
||||
{
|
||||
FromDigest = "sha256:abc123",
|
||||
ToDigest = "sha256:def456",
|
||||
TenantId = "tenant-1",
|
||||
Deltas =
|
||||
[
|
||||
new ChangeTraceDeltaEntry
|
||||
{
|
||||
Purl = "pkg:deb/debian/openssl@3.0.9",
|
||||
FromVersion = "3.0.9",
|
||||
ToVersion = "3.0.9-1+deb12u3",
|
||||
ChangeType = "Modified",
|
||||
Explain = "VendorBackport",
|
||||
SymbolsChanged = 10,
|
||||
BytesChanged = 2048,
|
||||
Confidence = 0.95,
|
||||
TrustDeltaScore = -0.3,
|
||||
CveIds = ["CVE-2026-12345"],
|
||||
Functions = ["ssl3_get_record"]
|
||||
}
|
||||
],
|
||||
Summary = new ChangeTracePredicateSummary
|
||||
{
|
||||
ChangedPackages = 1,
|
||||
ChangedSymbols = 10,
|
||||
ChangedBytes = 2048,
|
||||
RiskDelta = -0.3,
|
||||
Verdict = "risk_down"
|
||||
},
|
||||
TrustDelta = new TrustDeltaRecord
|
||||
{
|
||||
Score = -0.3,
|
||||
BeforeScore = 0.5,
|
||||
AfterScore = 0.8,
|
||||
ReachabilityImpact = "reduced",
|
||||
ExploitabilityImpact = "down"
|
||||
},
|
||||
ProofSteps = ["CVE patched", "Function verified"],
|
||||
DiffMethods = ["pkg", "symbol"],
|
||||
Policies = ["lattice:default@v3"],
|
||||
AnalyzedAt = new DateTimeOffset(2026, 1, 12, 14, 30, 0, TimeSpan.Zero),
|
||||
AlgorithmVersion = "1.0.0",
|
||||
CommitmentHash = "a1b2c3d4e5f6"
|
||||
};
|
||||
|
||||
// Act
|
||||
var json = JsonSerializer.Serialize(predicate, JsonOptions);
|
||||
var deserialized = JsonSerializer.Deserialize<ChangeTracePredicate>(json, JsonOptions);
|
||||
|
||||
// Assert
|
||||
deserialized.Should().NotBeNull();
|
||||
deserialized!.FromDigest.Should().Be(predicate.FromDigest);
|
||||
deserialized.ToDigest.Should().Be(predicate.ToDigest);
|
||||
deserialized.TenantId.Should().Be(predicate.TenantId);
|
||||
deserialized.Deltas.Should().HaveCount(1);
|
||||
deserialized.Deltas[0].Purl.Should().Be("pkg:deb/debian/openssl@3.0.9");
|
||||
deserialized.Summary.ChangedPackages.Should().Be(1);
|
||||
deserialized.TrustDelta.Score.Should().Be(-0.3);
|
||||
deserialized.ProofSteps.Should().HaveCount(2);
|
||||
deserialized.DiffMethods.Should().HaveCount(2);
|
||||
deserialized.Policies.Should().HaveCount(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serialize_DeltaEntry_ContainsAllFields()
|
||||
{
|
||||
// Arrange
|
||||
var entry = new ChangeTraceDeltaEntry
|
||||
{
|
||||
Purl = "pkg:npm/lodash@4.17.21",
|
||||
FromVersion = "4.17.20",
|
||||
ToVersion = "4.17.21",
|
||||
ChangeType = "Upgraded",
|
||||
Explain = "UpstreamUpgrade",
|
||||
SymbolsChanged = 5,
|
||||
BytesChanged = 512,
|
||||
Confidence = 0.88,
|
||||
TrustDeltaScore = -0.1,
|
||||
CveIds = ["CVE-2026-00001"],
|
||||
Functions = ["merge", "clone"]
|
||||
};
|
||||
|
||||
// Act
|
||||
var json = JsonSerializer.Serialize(entry, JsonOptions);
|
||||
|
||||
// Assert
|
||||
json.Should().Contain("\"purl\"");
|
||||
json.Should().Contain("\"fromVersion\"");
|
||||
json.Should().Contain("\"toVersion\"");
|
||||
json.Should().Contain("\"changeType\"");
|
||||
json.Should().Contain("\"explain\"");
|
||||
json.Should().Contain("\"symbolsChanged\"");
|
||||
json.Should().Contain("\"bytesChanged\"");
|
||||
json.Should().Contain("\"confidence\"");
|
||||
json.Should().Contain("\"trustDeltaScore\"");
|
||||
json.Should().Contain("\"cveIds\"");
|
||||
json.Should().Contain("\"functions\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serialize_Summary_ContainsAllFields()
|
||||
{
|
||||
// Arrange
|
||||
var summary = new ChangeTracePredicateSummary
|
||||
{
|
||||
ChangedPackages = 3,
|
||||
ChangedSymbols = 25,
|
||||
ChangedBytes = 8192,
|
||||
RiskDelta = 0.15,
|
||||
Verdict = "neutral"
|
||||
};
|
||||
|
||||
// Act
|
||||
var json = JsonSerializer.Serialize(summary, JsonOptions);
|
||||
|
||||
// Assert
|
||||
json.Should().Contain("\"changedPackages\"");
|
||||
json.Should().Contain("\"changedSymbols\"");
|
||||
json.Should().Contain("\"changedBytes\"");
|
||||
json.Should().Contain("\"riskDelta\"");
|
||||
json.Should().Contain("\"verdict\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serialize_TrustDeltaRecord_ContainsAllFields()
|
||||
{
|
||||
// Arrange
|
||||
var trustDelta = new TrustDeltaRecord
|
||||
{
|
||||
Score = -0.45,
|
||||
BeforeScore = 0.4,
|
||||
AfterScore = 0.85,
|
||||
ReachabilityImpact = "eliminated",
|
||||
ExploitabilityImpact = "eliminated"
|
||||
};
|
||||
|
||||
// Act
|
||||
var json = JsonSerializer.Serialize(trustDelta, JsonOptions);
|
||||
|
||||
// Assert
|
||||
json.Should().Contain("\"score\"");
|
||||
json.Should().Contain("\"beforeScore\"");
|
||||
json.Should().Contain("\"afterScore\"");
|
||||
json.Should().Contain("\"reachabilityImpact\"");
|
||||
json.Should().Contain("\"exploitabilityImpact\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serialize_EmptyDeltas_ProducesEmptyArray()
|
||||
{
|
||||
// Arrange
|
||||
var predicate = new ChangeTracePredicate
|
||||
{
|
||||
FromDigest = "sha256:abc",
|
||||
ToDigest = "sha256:def",
|
||||
TenantId = "test",
|
||||
Deltas = ImmutableArray<ChangeTraceDeltaEntry>.Empty,
|
||||
Summary = new ChangeTracePredicateSummary
|
||||
{
|
||||
ChangedPackages = 0,
|
||||
ChangedSymbols = 0,
|
||||
ChangedBytes = 0,
|
||||
RiskDelta = 0,
|
||||
Verdict = "neutral"
|
||||
},
|
||||
TrustDelta = new TrustDeltaRecord
|
||||
{
|
||||
Score = 0,
|
||||
ReachabilityImpact = "unchanged",
|
||||
ExploitabilityImpact = "unchanged"
|
||||
},
|
||||
AnalyzedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
// Act
|
||||
var json = JsonSerializer.Serialize(predicate, JsonOptions);
|
||||
|
||||
// Assert
|
||||
json.Should().Contain("\"deltas\": []");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Deserialize_JsonWithMissingOptionalFields_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
var json = """
|
||||
{
|
||||
"fromDigest": "sha256:abc",
|
||||
"toDigest": "sha256:def",
|
||||
"tenantId": "test",
|
||||
"summary": {
|
||||
"changedPackages": 1,
|
||||
"changedSymbols": 5,
|
||||
"changedBytes": 256,
|
||||
"riskDelta": -0.1,
|
||||
"verdict": "risk_down"
|
||||
},
|
||||
"trustDelta": {
|
||||
"score": -0.1,
|
||||
"reachabilityImpact": "reduced",
|
||||
"exploitabilityImpact": "down"
|
||||
},
|
||||
"analyzedAt": "2026-01-12T14:30:00Z"
|
||||
}
|
||||
""";
|
||||
|
||||
// Act
|
||||
var predicate = JsonSerializer.Deserialize<ChangeTracePredicate>(json, JsonOptions);
|
||||
|
||||
// Assert
|
||||
predicate.Should().NotBeNull();
|
||||
predicate!.Deltas.Should().BeEmpty();
|
||||
predicate.ProofSteps.Should().BeEmpty();
|
||||
predicate.DiffMethods.Should().BeEmpty();
|
||||
predicate.Policies.Should().BeEmpty();
|
||||
predicate.CommitmentHash.Should().BeNull();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user