Files
git.stella-ops.org/src/Policy/StellaOps.Policy.Engine/Attestation/VerdictPredicateBuilder.cs
master b444284be5 docs: Archive Sprint 3500 (PoE), Sprint 7100 (Proof Moats), and additional sprints
Archive completed sprint documentation and deliverables:

## SPRINT_3500 - Proof of Exposure (PoE) Implementation (COMPLETE )
- Windows filesystem hash sanitization (colon → underscore)
- Namespace conflict resolution (Subgraph → PoESubgraph)
- Mock test improvements with It.IsAny<>()
- Direct orchestrator unit tests
- 8/8 PoE tests passing (100% success)
- Archived to: docs/implplan/archived/2025-12-23-sprint-3500-poe/

## SPRINT_7100.0001 - Proof-Driven Moats Core (COMPLETE )
- Four-tier backport detection system
- 9 production modules (4,044 LOC)
- Binary fingerprinting (TLSH + instruction hashing)
- VEX integration with proof-carrying verdicts
- 42+ unit tests passing (100% success)
- Archived to: docs/implplan/archived/2025-12-23-sprint-7100-proof-moats/

## SPRINT_7100.0002 - Proof Moats Storage Layer (COMPLETE )
- PostgreSQL repository implementations
- Database migrations (4 evidence tables + audit)
- Test data seed scripts (12 evidence records, 3 CVEs)
- Integration tests with Testcontainers
- <100ms proof generation performance
- Archived to: docs/implplan/archived/2025-12-23-sprint-7100-proof-moats/

## SPRINT_3000_0200 - Authority Admin & Branding (COMPLETE )
- Console admin RBAC UI components
- Branding editor with tenant isolation
- Authority backend endpoints
- Archived to: docs/implplan/archived/

## Additional Documentation
- CLI command reference and compliance guides
- Module architecture docs (26 modules documented)
- Data schemas and contracts
- Operations runbooks
- Security risk models
- Product roadmap

All archived sprints achieved 100% completion of planned deliverables.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-23 15:02:38 +02:00

260 lines
8.3 KiB
C#

using System.Collections.Immutable;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using StellaOps.Canonical.Json;
using StellaOps.Policy;
using StellaOps.Policy.Engine.Materialization;
namespace StellaOps.Policy.Engine.Attestation;
/// <summary>
/// Builds DSSE verdict predicates from policy explain traces.
/// </summary>
public sealed class VerdictPredicateBuilder
{
/// <summary>
/// Initializes a new instance of VerdictPredicateBuilder.
/// </summary>
public VerdictPredicateBuilder()
{
}
/// <summary>
/// Builds a verdict predicate from a policy explain trace.
/// </summary>
public VerdictPredicate Build(PolicyExplainTrace trace)
{
if (trace is null)
{
throw new ArgumentNullException(nameof(trace));
}
// Map verdict
var verdict = new VerdictInfo(
status: MapVerdictStatus(trace.Verdict.Status),
severity: MapSeverity(trace.Verdict.Severity),
score: trace.Verdict.Score ?? 0.0,
rationale: trace.Verdict.Rationale
);
// Map rule chain
var ruleChain = trace.RuleChain
.Select(r => new VerdictRuleExecution(
ruleId: r.RuleId,
action: r.Action,
decision: r.Decision,
score: r.Score != 0 ? r.Score : null
))
.ToList();
// Map evidence
var evidence = trace.Evidence
.Select(e => new VerdictEvidence(
type: e.Type,
reference: e.Reference,
source: e.Source,
status: e.Status,
digest: ComputeEvidenceDigest(e),
weight: e.Weight != 0 ? e.Weight : null,
metadata: e.Metadata.Any() ? e.Metadata.ToImmutableSortedDictionary() : null
))
.ToList();
// Map VEX impacts
var vexImpacts = trace.VexImpacts
.Select(v => new VerdictVexImpact(
statementId: v.StatementId,
provider: v.Provider,
status: v.Status,
accepted: v.Accepted,
justification: v.Justification
))
.ToList();
// Extract reachability (if present in metadata)
var reachability = ExtractReachability(trace);
// Build metadata with determinism hash
var metadata = BuildMetadata(trace, evidence);
return new VerdictPredicate(
tenantId: trace.TenantId,
policyId: trace.PolicyId,
policyVersion: trace.PolicyVersion,
runId: trace.RunId,
findingId: trace.FindingId,
evaluatedAt: trace.EvaluatedAt,
verdict: verdict,
ruleChain: ruleChain,
evidence: evidence,
vexImpacts: vexImpacts,
reachability: reachability,
metadata: metadata
);
}
/// <summary>
/// Serializes a verdict predicate to canonical JSON.
/// </summary>
public string Serialize(VerdictPredicate predicate)
{
if (predicate is null)
{
throw new ArgumentNullException(nameof(predicate));
}
var canonical = CanonJson.Canonicalize(predicate);
return Encoding.UTF8.GetString(canonical);
}
/// <summary>
/// Computes the determinism hash for a verdict predicate.
/// </summary>
public string ComputeDeterminismHash(VerdictPredicate predicate)
{
if (predicate is null)
{
throw new ArgumentNullException(nameof(predicate));
}
// Sort and concatenate all evidence digests
var evidenceDigests = predicate.Evidence
.Where(e => e.Digest is not null)
.Select(e => e.Digest!)
.OrderBy(d => d, StringComparer.Ordinal)
.ToList();
// Add verdict status, severity, score
var components = new List<string>
{
predicate.Verdict.Status,
predicate.Verdict.Severity,
predicate.Verdict.Score.ToString("F2", CultureInfo.InvariantCulture),
};
components.AddRange(evidenceDigests);
// Compute SHA256 hash
var combined = string.Join(":", components);
var bytes = Encoding.UTF8.GetBytes(combined);
var hash = SHA256.HashData(bytes);
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
}
private static string MapVerdictStatus(PolicyVerdictStatus status)
{
return status switch
{
PolicyVerdictStatus.Pass => "passed",
PolicyVerdictStatus.Warned => "warned",
PolicyVerdictStatus.Blocked => "blocked",
PolicyVerdictStatus.Ignored => "ignored",
PolicyVerdictStatus.Deferred => "deferred",
PolicyVerdictStatus.Escalated => "escalated",
PolicyVerdictStatus.RequiresVex => "requires_vex",
_ => throw new ArgumentOutOfRangeException(nameof(status), status, "Unknown verdict status.")
};
}
private static string MapSeverity(SeverityRank? severity)
{
if (severity is null)
{
return "none";
}
return severity.Value switch
{
SeverityRank.Critical => "critical",
SeverityRank.High => "high",
SeverityRank.Medium => "medium",
SeverityRank.Low => "low",
SeverityRank.Info => "info",
SeverityRank.None => "none",
_ => "none"
};
}
private static string? ComputeEvidenceDigest(PolicyExplainEvidence evidence)
{
// If evidence has a reference that looks like a digest, use it
if (evidence.Reference.StartsWith("sha256:", StringComparison.Ordinal))
{
return evidence.Reference;
}
// Otherwise, compute digest from reference + source + status
var content = $"{evidence.Type}:{evidence.Reference}:{evidence.Source}:{evidence.Status}";
var bytes = Encoding.UTF8.GetBytes(content);
var hash = SHA256.HashData(bytes);
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
}
private static VerdictReachability? ExtractReachability(PolicyExplainTrace trace)
{
// Check if metadata contains reachability status
if (!trace.Metadata.TryGetValue("reachabilitystatus", out var reachabilityStatus))
{
return null;
}
// TODO: Extract full reachability paths from trace or evidence
// For now, return basic reachability status
return new VerdictReachability(
status: reachabilityStatus,
paths: null
);
}
private ImmutableSortedDictionary<string, string> BuildMetadata(
PolicyExplainTrace trace,
List<VerdictEvidence> evidence)
{
var builder = ImmutableSortedDictionary.CreateBuilder<string, string>(StringComparer.Ordinal);
// Add component PURL if present
if (trace.Metadata.TryGetValue("componentpurl", out var componentPurl))
{
builder["componentpurl"] = componentPurl;
}
// Add SBOM ID if present
if (trace.Metadata.TryGetValue("sbomid", out var sbomId))
{
builder["sbomid"] = sbomId;
}
// Add trace ID if present
if (trace.Metadata.TryGetValue("traceid", out var traceId))
{
builder["traceid"] = traceId;
}
// Compute and add determinism hash
// Temporarily create predicate to compute hash
var tempPredicate = new VerdictPredicate(
tenantId: trace.TenantId,
policyId: trace.PolicyId,
policyVersion: trace.PolicyVersion,
runId: trace.RunId,
findingId: trace.FindingId,
evaluatedAt: trace.EvaluatedAt,
verdict: new VerdictInfo(
status: MapVerdictStatus(trace.Verdict.Status),
severity: MapSeverity(trace.Verdict.Severity),
score: trace.Verdict.Score ?? 0.0
),
ruleChain: null,
evidence: evidence,
vexImpacts: null,
reachability: null,
metadata: null
);
builder["determinismhash"] = ComputeDeterminismHash(tempPredicate);
return builder.ToImmutable();
}
}