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>
|
||||
|
||||
Reference in New Issue
Block a user