update evidence bundle to include new evidence types and implement ProofSpine integration
Some checks failed
Lighthouse CI / Lighthouse Audit (push) Has been cancelled
Lighthouse CI / Axe Accessibility Audit (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
sm-remote-ci / build-and-test (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-12-15 09:15:30 +02:00
parent 8c8f0c632d
commit 505fe7a885
49 changed files with 4756 additions and 551 deletions

View File

@@ -1,41 +0,0 @@
using System.Threading;
using System.Threading.Tasks;
namespace StellaOps.Scanner.Reachability.ProofSpine;
/// <summary>
/// Service for DSSE (Dead Simple Signing Envelope) signing operations.
/// </summary>
public interface IDsseSigningService
{
/// <summary>
/// Signs a payload and returns a DSSE envelope.
/// </summary>
Task<DsseEnvelope> SignAsync(
object payload,
ICryptoProfile cryptoProfile,
CancellationToken cancellationToken = default);
/// <summary>
/// Verifies a DSSE envelope signature.
/// </summary>
Task<bool> VerifyAsync(
DsseEnvelope envelope,
CancellationToken cancellationToken = default);
}
/// <summary>
/// Cryptographic profile for signing operations.
/// </summary>
public interface ICryptoProfile
{
/// <summary>
/// Key identifier.
/// </summary>
string KeyId { get; }
/// <summary>
/// Signing algorithm (e.g., "ed25519", "ecdsa-p256").
/// </summary>
string Algorithm { get; }
}

View File

@@ -1,53 +0,0 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace StellaOps.Scanner.Reachability.ProofSpine;
/// <summary>
/// Repository for ProofSpine persistence and queries.
/// </summary>
public interface IProofSpineRepository
{
/// <summary>
/// Gets a ProofSpine by its ID.
/// </summary>
Task<ProofSpine?> GetByIdAsync(string spineId, CancellationToken cancellationToken = default);
/// <summary>
/// Gets a ProofSpine by its decision criteria.
/// </summary>
Task<ProofSpine?> GetByDecisionAsync(
string artifactId,
string vulnId,
string policyProfileId,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets all ProofSpines for a scan run.
/// </summary>
Task<IReadOnlyList<ProofSpine>> GetByScanRunAsync(
string scanRunId,
CancellationToken cancellationToken = default);
/// <summary>
/// Saves a ProofSpine.
/// </summary>
Task<ProofSpine> SaveAsync(ProofSpine spine, CancellationToken cancellationToken = default);
/// <summary>
/// Supersedes an old spine with a new one.
/// </summary>
Task SupersedeAsync(
string oldSpineId,
string newSpineId,
string reason,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets all segments for a spine.
/// </summary>
Task<IReadOnlyList<ProofSegment>> GetSegmentsAsync(
string spineId,
CancellationToken cancellationToken = default);
}

View File

@@ -1,368 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
namespace StellaOps.Scanner.Reachability.ProofSpine;
/// <summary>
/// Builds ProofSpine chains from evidence segments.
/// Ensures deterministic ordering and cryptographic chaining.
/// </summary>
public sealed class ProofSpineBuilder
{
private readonly List<ProofSegmentInput> _segments = new();
private readonly IDsseSigningService _signer;
private readonly ICryptoProfile _cryptoProfile;
private readonly TimeProvider _timeProvider;
private string? _artifactId;
private string? _vulnerabilityId;
private string? _policyProfileId;
private string? _scanRunId;
public ProofSpineBuilder(
IDsseSigningService signer,
ICryptoProfile cryptoProfile,
TimeProvider timeProvider)
{
_signer = signer;
_cryptoProfile = cryptoProfile;
_timeProvider = timeProvider;
}
public ProofSpineBuilder ForArtifact(string artifactId)
{
_artifactId = artifactId;
return this;
}
public ProofSpineBuilder ForVulnerability(string vulnId)
{
_vulnerabilityId = vulnId;
return this;
}
public ProofSpineBuilder WithPolicyProfile(string profileId)
{
_policyProfileId = profileId;
return this;
}
public ProofSpineBuilder WithScanRun(string scanRunId)
{
_scanRunId = scanRunId;
return this;
}
/// <summary>
/// Adds an SBOM slice segment showing component relevance.
/// </summary>
public ProofSpineBuilder AddSbomSlice(
string sbomDigest,
IReadOnlyList<string> relevantPurls,
string toolId,
string toolVersion)
{
var input = new SbomSliceInput(sbomDigest, relevantPurls);
var inputHash = ComputeCanonicalHash(input);
var resultHash = ComputeCanonicalHash(relevantPurls);
_segments.Add(new ProofSegmentInput(
ProofSegmentType.SbomSlice,
inputHash,
resultHash,
input,
toolId,
toolVersion));
return this;
}
/// <summary>
/// Adds a vulnerability match segment.
/// </summary>
public ProofSpineBuilder AddMatch(
string vulnId,
string purl,
string matchedVersion,
string matchReason,
string toolId,
string toolVersion)
{
var input = new MatchInput(vulnId, purl, matchedVersion);
var result = new MatchResult(matchReason);
_segments.Add(new ProofSegmentInput(
ProofSegmentType.Match,
ComputeCanonicalHash(input),
ComputeCanonicalHash(result),
new { Input = input, Result = result },
toolId,
toolVersion));
return this;
}
/// <summary>
/// Adds a reachability analysis segment.
/// </summary>
public ProofSpineBuilder AddReachability(
string callgraphDigest,
string latticeState,
double confidence,
IReadOnlyList<string>? pathWitness,
string toolId,
string toolVersion)
{
var input = new ReachabilityInput(callgraphDigest);
var result = new ReachabilityResult(latticeState, confidence, pathWitness);
_segments.Add(new ProofSegmentInput(
ProofSegmentType.Reachability,
ComputeCanonicalHash(input),
ComputeCanonicalHash(result),
new { Input = input, Result = result },
toolId,
toolVersion));
return this;
}
/// <summary>
/// Adds a guard analysis segment (feature flags, config gates).
/// </summary>
public ProofSpineBuilder AddGuardAnalysis(
IReadOnlyList<GuardCondition> guards,
bool allGuardsPassed,
string toolId,
string toolVersion)
{
var input = new GuardAnalysisInput(guards);
var result = new GuardAnalysisResult(allGuardsPassed);
_segments.Add(new ProofSegmentInput(
ProofSegmentType.GuardAnalysis,
ComputeCanonicalHash(input),
ComputeCanonicalHash(result),
new { Input = input, Result = result },
toolId,
toolVersion));
return this;
}
/// <summary>
/// Adds runtime observation evidence.
/// </summary>
public ProofSpineBuilder AddRuntimeObservation(
string runtimeFactsDigest,
bool wasObserved,
int hitCount,
string toolId,
string toolVersion)
{
var input = new RuntimeObservationInput(runtimeFactsDigest);
var result = new RuntimeObservationResult(wasObserved, hitCount);
_segments.Add(new ProofSegmentInput(
ProofSegmentType.RuntimeObservation,
ComputeCanonicalHash(input),
ComputeCanonicalHash(result),
new { Input = input, Result = result },
toolId,
toolVersion));
return this;
}
/// <summary>
/// Adds policy evaluation segment with final verdict.
/// </summary>
public ProofSpineBuilder AddPolicyEval(
string policyDigest,
string verdict,
string verdictReason,
IReadOnlyDictionary<string, object> factors,
string toolId,
string toolVersion)
{
var input = new PolicyEvalInput(policyDigest, factors);
var result = new PolicyEvalResult(verdict, verdictReason);
_segments.Add(new ProofSegmentInput(
ProofSegmentType.PolicyEval,
ComputeCanonicalHash(input),
ComputeCanonicalHash(result),
new { Input = input, Result = result },
toolId,
toolVersion));
return this;
}
/// <summary>
/// Builds the final ProofSpine with chained, signed segments.
/// </summary>
public async Task<ProofSpine> BuildAsync(CancellationToken cancellationToken = default)
{
ValidateBuilder();
// Sort segments by type (predetermined order)
var orderedSegments = _segments
.OrderBy(s => (int)s.Type)
.ToList();
var builtSegments = new List<ProofSegment>();
string? prevHash = null;
for (var i = 0; i < orderedSegments.Count; i++)
{
var input = orderedSegments[i];
var createdAt = _timeProvider.GetUtcNow();
// Build payload for signing
var payload = new ProofSegmentPayload(
input.Type.ToString(),
i,
input.InputHash,
input.ResultHash,
prevHash,
input.Payload,
input.ToolId,
input.ToolVersion,
createdAt);
// Sign with DSSE
var envelope = await _signer.SignAsync(
payload,
_cryptoProfile,
cancellationToken);
var segmentId = ComputeSegmentId(input, i, prevHash);
var segment = new ProofSegment(
segmentId,
input.Type,
i,
input.InputHash,
input.ResultHash,
prevHash,
envelope,
input.ToolId,
input.ToolVersion,
ProofSegmentStatus.Verified,
createdAt);
builtSegments.Add(segment);
prevHash = segment.ResultHash;
}
// Compute root hash = hash(concat of all segment result hashes)
var rootHash = ComputeRootHash(builtSegments);
// Compute deterministic spine ID
var spineId = ComputeSpineId(_artifactId!, _vulnerabilityId!, _policyProfileId!, rootHash);
// Extract verdict from policy eval segment
var (verdict, verdictReason) = ExtractVerdict(builtSegments);
return new ProofSpine(
spineId,
_artifactId!,
_vulnerabilityId!,
_policyProfileId!,
builtSegments.ToImmutableArray(),
verdict,
verdictReason,
rootHash,
_scanRunId!,
_timeProvider.GetUtcNow(),
SupersededBySpineId: null);
}
private void ValidateBuilder()
{
if (string.IsNullOrWhiteSpace(_artifactId))
throw new InvalidOperationException("ArtifactId is required");
if (string.IsNullOrWhiteSpace(_vulnerabilityId))
throw new InvalidOperationException("VulnerabilityId is required");
if (string.IsNullOrWhiteSpace(_policyProfileId))
throw new InvalidOperationException("PolicyProfileId is required");
if (string.IsNullOrWhiteSpace(_scanRunId))
throw new InvalidOperationException("ScanRunId is required");
if (_segments.Count == 0)
throw new InvalidOperationException("At least one segment is required");
}
private static string ComputeCanonicalHash(object input)
{
var json = JsonSerializer.Serialize(input, CanonicalJsonOptions);
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(json));
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
}
private static string ComputeSegmentId(ProofSegmentInput input, int index, string? prevHash)
{
var data = $"{input.Type}:{index}:{input.InputHash}:{input.ResultHash}:{prevHash ?? "null"}";
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(data));
return Convert.ToHexString(hash).ToLowerInvariant()[..32];
}
private static string ComputeRootHash(IEnumerable<ProofSegment> segments)
{
var concat = string.Join(":", segments.Select(s => s.ResultHash));
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(concat));
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
}
private static string ComputeSpineId(string artifactId, string vulnId, string profileId, string rootHash)
{
var data = $"{artifactId}:{vulnId}:{profileId}:{rootHash}";
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(data));
return Convert.ToHexString(hash).ToLowerInvariant()[..32];
}
private static (string Verdict, string VerdictReason) ExtractVerdict(List<ProofSegment> segments)
{
// Default verdict if no policy eval segment
return ("under_investigation", "No policy evaluation completed");
}
private static readonly JsonSerializerOptions CanonicalJsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
}
// Supporting input types
internal sealed record ProofSegmentInput(
ProofSegmentType Type,
string InputHash,
string ResultHash,
object Payload,
string ToolId,
string ToolVersion);
internal sealed record SbomSliceInput(string SbomDigest, IReadOnlyList<string> RelevantPurls);
internal sealed record MatchInput(string VulnId, string Purl, string MatchedVersion);
internal sealed record MatchResult(string MatchReason);
internal sealed record ReachabilityInput(string CallgraphDigest);
internal sealed record ReachabilityResult(string LatticeState, double Confidence, IReadOnlyList<string>? PathWitness);
internal sealed record GuardAnalysisInput(IReadOnlyList<GuardCondition> Guards);
internal sealed record GuardAnalysisResult(bool AllGuardsPassed);
internal sealed record RuntimeObservationInput(string RuntimeFactsDigest);
internal sealed record RuntimeObservationResult(bool WasObserved, int HitCount);
internal sealed record PolicyEvalInput(string PolicyDigest, IReadOnlyDictionary<string, object> Factors);
internal sealed record PolicyEvalResult(string Verdict, string VerdictReason);
internal sealed record ProofSegmentPayload(
string SegmentType, int Index, string InputHash, string ResultHash,
string? PrevSegmentHash, object Payload, string ToolId, string ToolVersion,
DateTimeOffset CreatedAt);

View File

@@ -1,86 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
namespace StellaOps.Scanner.Reachability.ProofSpine;
/// <summary>
/// Represents a complete verifiable decision chain from SBOM to VEX verdict.
/// </summary>
public sealed record ProofSpine(
string SpineId,
string ArtifactId,
string VulnerabilityId,
string PolicyProfileId,
IReadOnlyList<ProofSegment> Segments,
string Verdict,
string VerdictReason,
string RootHash,
string ScanRunId,
DateTimeOffset CreatedAt,
string? SupersededBySpineId);
/// <summary>
/// A single evidence segment in the proof chain.
/// </summary>
public sealed record ProofSegment(
string SegmentId,
ProofSegmentType SegmentType,
int Index,
string InputHash,
string ResultHash,
string? PrevSegmentHash,
DsseEnvelope Envelope,
string ToolId,
string ToolVersion,
ProofSegmentStatus Status,
DateTimeOffset CreatedAt);
/// <summary>
/// Segment types in execution order.
/// </summary>
public enum ProofSegmentType
{
SbomSlice = 1, // Component relevance extraction
Match = 2, // SBOM-to-vulnerability mapping
Reachability = 3, // Symbol reachability analysis
GuardAnalysis = 4, // Config/feature flag gates
RuntimeObservation = 5, // Runtime evidence correlation
PolicyEval = 6 // Lattice decision computation
}
/// <summary>
/// Verification status of a segment.
/// </summary>
public enum ProofSegmentStatus
{
Pending = 0,
Verified = 1,
Partial = 2, // Some evidence missing but chain valid
Invalid = 3, // Signature verification failed
Untrusted = 4 // Key not in trust store
}
/// <summary>
/// DSSE envelope wrapper for signed content.
/// </summary>
public sealed record DsseEnvelope(
string PayloadType,
byte[] Payload,
IReadOnlyList<DsseSignature> Signatures);
/// <summary>
/// A signature in a DSSE envelope.
/// </summary>
public sealed record DsseSignature(
string KeyId,
byte[] Sig);
/// <summary>
/// Guard condition for feature flag or config gate analysis.
/// </summary>
public sealed record GuardCondition(
string Name,
string Type,
string Value,
bool Passed);