sprints and audit work

This commit is contained in:
StellaOps Bot
2026-01-07 09:36:16 +02:00
parent 05833e0af2
commit ab364c6032
377 changed files with 64534 additions and 1627 deletions

View File

@@ -0,0 +1,8 @@
global using System;
global using System.Collections.Generic;
global using System.Collections.Immutable;
global using System.Linq;
global using System.Text;
global using System.Text.Json;
global using System.Text.Json.Serialization;
global using Microsoft.Extensions.Logging;

View File

@@ -0,0 +1,52 @@
namespace StellaOps.Policy.Explainability;
/// <summary>
/// Renders verdict rationales in multiple formats.
/// </summary>
public interface IVerdictRationaleRenderer
{
/// <summary>
/// Renders a complete verdict rationale from verdict components.
/// </summary>
VerdictRationale Render(VerdictRationaleInput input);
/// <summary>
/// Renders rationale as plain text (4-line format).
/// </summary>
string RenderPlainText(VerdictRationale rationale);
/// <summary>
/// Renders rationale as Markdown.
/// </summary>
string RenderMarkdown(VerdictRationale rationale);
/// <summary>
/// Renders rationale as canonical JSON (RFC 8785).
/// </summary>
string RenderJson(VerdictRationale rationale);
}
/// <summary>
/// Input for verdict rationale rendering.
/// </summary>
public sealed record VerdictRationaleInput
{
public required VerdictReference VerdictRef { get; init; }
public required string Cve { get; init; }
public required ComponentIdentity Component { get; init; }
public ReachabilityDetail? Reachability { get; init; }
public required string PolicyClauseId { get; init; }
public required string PolicyRuleDescription { get; init; }
public required IReadOnlyList<string> PolicyConditions { get; init; }
public AttestationReference? PathWitness { get; init; }
public IReadOnlyList<AttestationReference>? VexStatements { get; init; }
public AttestationReference? Provenance { get; init; }
public required string Verdict { get; init; }
public double? Score { get; init; }
public required string Recommendation { get; init; }
public MitigationGuidance? Mitigation { get; init; }
public required DateTimeOffset GeneratedAt { get; init; }
public required string VerdictDigest { get; init; }
public string? PolicyDigest { get; init; }
public string? EvidenceDigest { get; init; }
}

View File

@@ -0,0 +1,12 @@
using Microsoft.Extensions.DependencyInjection;
namespace StellaOps.Policy.Explainability;
public static class ExplainabilityServiceCollectionExtensions
{
public static IServiceCollection AddVerdictExplainability(this IServiceCollection services)
{
services.AddSingleton<IVerdictRationaleRenderer, VerdictRationaleRenderer>();
return services;
}
}

View File

@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../../__Libraries/StellaOps.Canonical.Json/StellaOps.Canonical.Json.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,197 @@
namespace StellaOps.Policy.Explainability;
/// <summary>
/// Structured verdict rationale following the 4-line template.
/// Line 1: Evidence summary
/// Line 2: Policy clause that triggered the decision
/// Line 3: Attestations and proofs supporting the verdict
/// Line 4: Final decision with score and recommendation
/// </summary>
public sealed record VerdictRationale
{
/// <summary>Schema version for forward compatibility.</summary>
[JsonPropertyName("schema_version")]
public string SchemaVersion { get; init; } = "1.0";
/// <summary>Unique rationale ID (content-addressed).</summary>
[JsonPropertyName("rationale_id")]
public required string RationaleId { get; init; }
/// <summary>Reference to the verdict being explained.</summary>
[JsonPropertyName("verdict_ref")]
public required VerdictReference VerdictRef { get; init; }
/// <summary>Line 1: Evidence summary.</summary>
[JsonPropertyName("evidence")]
public required RationaleEvidence Evidence { get; init; }
/// <summary>Line 2: Policy clause that triggered the decision.</summary>
[JsonPropertyName("policy_clause")]
public required RationalePolicyClause PolicyClause { get; init; }
/// <summary>Line 3: Attestations and proofs supporting the verdict.</summary>
[JsonPropertyName("attestations")]
public required RationaleAttestations Attestations { get; init; }
/// <summary>Line 4: Final decision with score and recommendation.</summary>
[JsonPropertyName("decision")]
public required RationaleDecision Decision { get; init; }
/// <summary>Generation timestamp (UTC).</summary>
[JsonPropertyName("generated_at")]
public required DateTimeOffset GeneratedAt { get; init; }
/// <summary>Input digests for reproducibility.</summary>
[JsonPropertyName("input_digests")]
public required RationaleInputDigests InputDigests { get; init; }
}
/// <summary>Reference to the verdict being explained.</summary>
public sealed record VerdictReference
{
[JsonPropertyName("attestation_id")]
public required string AttestationId { get; init; }
[JsonPropertyName("artifact_digest")]
public required string ArtifactDigest { get; init; }
[JsonPropertyName("policy_id")]
public required string PolicyId { get; init; }
[JsonPropertyName("cve")]
public string? Cve { get; init; }
[JsonPropertyName("component_purl")]
public string? ComponentPurl { get; init; }
}
/// <summary>Line 1: Evidence summary.</summary>
public sealed record RationaleEvidence
{
[JsonPropertyName("cve")]
public required string Cve { get; init; }
[JsonPropertyName("component")]
public required ComponentIdentity Component { get; init; }
[JsonPropertyName("reachability")]
public ReachabilityDetail? Reachability { get; init; }
[JsonPropertyName("formatted_text")]
public required string FormattedText { get; init; }
}
public sealed record ComponentIdentity
{
[JsonPropertyName("purl")]
public required string Purl { get; init; }
[JsonPropertyName("name")]
public string? Name { get; init; }
[JsonPropertyName("version")]
public string? Version { get; init; }
[JsonPropertyName("ecosystem")]
public string? Ecosystem { get; init; }
}
public sealed record ReachabilityDetail
{
[JsonPropertyName("vulnerable_function")]
public string? VulnerableFunction { get; init; }
[JsonPropertyName("entry_point")]
public string? EntryPoint { get; init; }
[JsonPropertyName("path_summary")]
public string? PathSummary { get; init; }
}
/// <summary>Line 2: Policy clause reference.</summary>
public sealed record RationalePolicyClause
{
[JsonPropertyName("clause_id")]
public required string ClauseId { get; init; }
[JsonPropertyName("rule_description")]
public required string RuleDescription { get; init; }
[JsonPropertyName("conditions")]
public required IReadOnlyList<string> Conditions { get; init; }
[JsonPropertyName("formatted_text")]
public required string FormattedText { get; init; }
}
/// <summary>Line 3: Attestations and proofs.</summary>
public sealed record RationaleAttestations
{
[JsonPropertyName("path_witness")]
public AttestationReference? PathWitness { get; init; }
[JsonPropertyName("vex_statements")]
public IReadOnlyList<AttestationReference>? VexStatements { get; init; }
[JsonPropertyName("provenance")]
public AttestationReference? Provenance { get; init; }
[JsonPropertyName("formatted_text")]
public required string FormattedText { get; init; }
}
public sealed record AttestationReference
{
[JsonPropertyName("id")]
public required string Id { get; init; }
[JsonPropertyName("type")]
public required string Type { get; init; }
[JsonPropertyName("digest")]
public string? Digest { get; init; }
[JsonPropertyName("summary")]
public string? Summary { get; init; }
}
/// <summary>Line 4: Final decision.</summary>
public sealed record RationaleDecision
{
[JsonPropertyName("verdict")]
public required string Verdict { get; init; }
[JsonPropertyName("score")]
public double? Score { get; init; }
[JsonPropertyName("recommendation")]
public required string Recommendation { get; init; }
[JsonPropertyName("mitigation")]
public MitigationGuidance? Mitigation { get; init; }
[JsonPropertyName("formatted_text")]
public required string FormattedText { get; init; }
}
public sealed record MitigationGuidance
{
[JsonPropertyName("action")]
public required string Action { get; init; }
[JsonPropertyName("details")]
public string? Details { get; init; }
}
/// <summary>Input digests for reproducibility.</summary>
public sealed record RationaleInputDigests
{
[JsonPropertyName("verdict_digest")]
public required string VerdictDigest { get; init; }
[JsonPropertyName("policy_digest")]
public string? PolicyDigest { get; init; }
[JsonPropertyName("evidence_digest")]
public string? EvidenceDigest { get; init; }
}

View File

@@ -0,0 +1,200 @@
using System.Security.Cryptography;
using StellaOps.Canonical.Json;
namespace StellaOps.Policy.Explainability;
/// <summary>
/// Renders verdict rationales in multiple formats following the 4-line template.
/// </summary>
public sealed class VerdictRationaleRenderer : IVerdictRationaleRenderer
{
private readonly ILogger<VerdictRationaleRenderer> _logger;
public VerdictRationaleRenderer(ILogger<VerdictRationaleRenderer> logger)
{
_logger = logger;
}
public VerdictRationale Render(VerdictRationaleInput input)
{
var evidence = RenderEvidence(input);
var policyClause = RenderPolicyClause(input);
var attestations = RenderAttestations(input);
var decision = RenderDecision(input);
var inputDigests = new RationaleInputDigests
{
VerdictDigest = input.VerdictDigest,
PolicyDigest = input.PolicyDigest,
EvidenceDigest = input.EvidenceDigest
};
var rationale = new VerdictRationale
{
RationaleId = string.Empty, // Will be computed below
VerdictRef = input.VerdictRef,
Evidence = evidence,
PolicyClause = policyClause,
Attestations = attestations,
Decision = decision,
GeneratedAt = input.GeneratedAt,
InputDigests = inputDigests
};
// Compute content-addressed ID
var rationaleId = ComputeRationaleId(rationale);
return rationale with { RationaleId = rationaleId };
}
public string RenderPlainText(VerdictRationale rationale)
{
var sb = new StringBuilder();
sb.AppendLine(rationale.Evidence.FormattedText);
sb.AppendLine(rationale.PolicyClause.FormattedText);
sb.AppendLine(rationale.Attestations.FormattedText);
sb.AppendLine(rationale.Decision.FormattedText);
return sb.ToString();
}
public string RenderMarkdown(VerdictRationale rationale)
{
var sb = new StringBuilder();
sb.AppendLine($"## Verdict Rationale: {rationale.Evidence.Cve}");
sb.AppendLine();
sb.AppendLine("### Evidence");
sb.AppendLine(rationale.Evidence.FormattedText);
sb.AppendLine();
sb.AppendLine("### Policy Clause");
sb.AppendLine(rationale.PolicyClause.FormattedText);
sb.AppendLine();
sb.AppendLine("### Attestations");
sb.AppendLine(rationale.Attestations.FormattedText);
sb.AppendLine();
sb.AppendLine("### Decision");
sb.AppendLine(rationale.Decision.FormattedText);
sb.AppendLine();
sb.AppendLine($"*Rationale ID: `{rationale.RationaleId}`*");
return sb.ToString();
}
public string RenderJson(VerdictRationale rationale)
{
return CanonJson.Serialize(rationale);
}
private RationaleEvidence RenderEvidence(VerdictRationaleInput input)
{
var text = new StringBuilder();
text.Append($"CVE-{input.Cve.Replace("CVE-", "")} in `{input.Component.Name ?? input.Component.Purl}` {input.Component.Version}");
if (input.Reachability != null)
{
text.Append($"; symbol `{input.Reachability.VulnerableFunction}` reachable from `{input.Reachability.EntryPoint}`");
if (!string.IsNullOrEmpty(input.Reachability.PathSummary))
{
text.Append($" ({input.Reachability.PathSummary})");
}
}
text.Append('.');
return new RationaleEvidence
{
Cve = input.Cve,
Component = input.Component,
Reachability = input.Reachability,
FormattedText = text.ToString()
};
}
private RationalePolicyClause RenderPolicyClause(VerdictRationaleInput input)
{
var text = $"Policy {input.PolicyClauseId}: {input.PolicyRuleDescription}";
if (input.PolicyConditions.Any())
{
text += $" ({string.Join(", ", input.PolicyConditions)})";
}
text += ".";
return new RationalePolicyClause
{
ClauseId = input.PolicyClauseId,
RuleDescription = input.PolicyRuleDescription,
Conditions = input.PolicyConditions,
FormattedText = text
};
}
private RationaleAttestations RenderAttestations(VerdictRationaleInput input)
{
var parts = new List<string>();
if (input.PathWitness != null)
{
parts.Add($"Path witness: {input.PathWitness.Summary ?? input.PathWitness.Id}");
}
if (input.VexStatements?.Any() == true)
{
var vexSummary = string.Join(", ", input.VexStatements.Select(v => v.Summary ?? v.Id));
parts.Add($"VEX statements: {vexSummary}");
}
if (input.Provenance != null)
{
parts.Add($"Provenance: {input.Provenance.Summary ?? input.Provenance.Id}");
}
var text = parts.Any()
? string.Join("; ", parts) + "."
: "No attestations available.";
return new RationaleAttestations
{
PathWitness = input.PathWitness,
VexStatements = input.VexStatements,
Provenance = input.Provenance,
FormattedText = text
};
}
private RationaleDecision RenderDecision(VerdictRationaleInput input)
{
var text = new StringBuilder();
text.Append($"{input.Verdict}");
if (input.Score.HasValue)
{
text.Append($" (score {input.Score.Value:F2})");
}
text.Append($". {input.Recommendation}");
if (input.Mitigation != null)
{
text.Append($": {input.Mitigation.Action}");
if (!string.IsNullOrEmpty(input.Mitigation.Details))
{
text.Append($" ({input.Mitigation.Details})");
}
}
text.Append('.');
return new RationaleDecision
{
Verdict = input.Verdict,
Score = input.Score,
Recommendation = input.Recommendation,
Mitigation = input.Mitigation,
FormattedText = text.ToString()
};
}
private string ComputeRationaleId(VerdictRationale rationale)
{
var canonicalJson = CanonJson.Serialize(rationale with { RationaleId = string.Empty });
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(canonicalJson));
return $"rat:sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
}
}