release orchestrator v1 draft and build fixes

This commit is contained in:
master
2026-01-12 12:24:17 +02:00
parent f3de858c59
commit 9873f80830
1598 changed files with 240385 additions and 5944 deletions

View File

@@ -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
};
}
}

View File

@@ -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;
}

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -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>

View File

@@ -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();
}
}