8.5 KiB
8.5 KiB
Verdict Rationale Template
Status: Implemented (SPRINT_20260106_001_001_LB) Library:
StellaOps.Policy.ExplainabilityAPI Endpoint:GET /api/v1/triage/findings/{findingId}/rationaleCLI Command:stella verdict rationale <finding-id>
Overview
Verdict Rationales provide human-readable explanations for policy verdicts using a standardized 4-line template. Each rationale explains:
- Evidence: What vulnerability was found and where
- Policy Clause: Which policy rule triggered the decision
- Attestations: What proofs support the verdict
- Decision: Final verdict with recommendation
Rationales are content-addressed (same inputs produce same rationale ID), enabling caching and deduplication.
4-Line Template
Every verdict rationale follows this structure:
Line 1 - Evidence: CVE-2024-XXXX in `libxyz` 1.2.3; symbol `foo_read` reachable from `/usr/bin/tool`.
Line 2 - Policy: Policy S2.1: reachable+EPSS>=0.2 => triage=P1.
Line 3 - Attestations: Build-ID match to vendor advisory; call-path: `main->parse->foo_read`.
Line 4 - Decision: Affected (score 0.72). Mitigation recommended: upgrade or backport KB-123.
Template Components
| Line | Purpose | Content |
|---|---|---|
| Evidence | What was found | CVE ID, component PURL, version, reachability info |
| Policy Clause | Why decision was made | Policy rule ID, expression, triage priority |
| Attestations | Supporting proofs | Build-ID matches, call paths, VEX statements, provenance |
| Decision | What to do | Verdict status, risk score, recommendation, mitigation |
API Usage
Get Rationale (JSON)
curl -H "Authorization: Bearer $TOKEN" \
"https://scanner.example.com/api/v1/triage/findings/12345/rationale?format=json"
Response:
{
"finding_id": "12345",
"rationale_id": "rationale:sha256:abc123...",
"schema_version": "1.0",
"evidence": {
"cve": "CVE-2024-1234",
"component_purl": "pkg:npm/lodash@4.17.20",
"component_version": "4.17.20",
"vulnerable_function": "template",
"entry_point": "/app/src/index.js",
"text": "CVE-2024-1234 in `pkg:npm/lodash@4.17.20` 4.17.20; symbol `template` reachable from `/app/src/index.js`."
},
"policy_clause": {
"clause_id": "S2.1",
"rule_description": "High severity with reachability",
"conditions": ["severity>=high", "reachable=true"],
"text": "Policy S2.1: severity>=high AND reachable=true => triage=P1."
},
"attestations": {
"path_witness": {
"id": "witness-789",
"type": "path-witness",
"digest": "sha256:def456...",
"summary": "Path witness from scanner"
},
"vex_statements": [
{
"id": "vex-001",
"type": "vex",
"digest": "sha256:ghi789...",
"summary": "Affected: from vendor.example.com"
}
],
"provenance": null,
"text": "Path witness from scanner; VEX statement: Affected from vendor.example.com."
},
"decision": {
"verdict": "Affected",
"score": 0.72,
"recommendation": "Upgrade to version 4.17.21",
"mitigation": {
"action": "upgrade",
"details": "Upgrade to 4.17.21 or later"
},
"text": "Affected (score 0.72). Mitigation recommended: Upgrade to version 4.17.21."
},
"generated_at": "2026-01-07T12:00:00Z",
"input_digests": {
"verdict_digest": "sha256:abc123...",
"policy_digest": "sha256:def456...",
"evidence_digest": "sha256:ghi789..."
}
}
Get Rationale (Plain Text)
curl -H "Authorization: Bearer $TOKEN" \
"https://scanner.example.com/api/v1/triage/findings/12345/rationale?format=plaintext"
Response:
{
"finding_id": "12345",
"rationale_id": "rationale:sha256:abc123...",
"format": "plaintext",
"content": "CVE-2024-1234 in `pkg:npm/lodash@4.17.20` 4.17.20; symbol `template` reachable from `/app/src/index.js`.\nPolicy S2.1: severity>=high AND reachable=true => triage=P1.\nPath witness from scanner; VEX statement: Affected from vendor.example.com.\nAffected (score 0.72). Mitigation recommended: Upgrade to version 4.17.21."
}
Get Rationale (Markdown)
curl -H "Authorization: Bearer $TOKEN" \
"https://scanner.example.com/api/v1/triage/findings/12345/rationale?format=markdown"
Response:
{
"finding_id": "12345",
"rationale_id": "rationale:sha256:abc123...",
"format": "markdown",
"content": "**Evidence:** CVE-2024-1234 in `pkg:npm/lodash@4.17.20` 4.17.20; symbol `template` reachable from `/app/src/index.js`.\n\n**Policy:** Policy S2.1: severity>=high AND reachable=true => triage=P1.\n\n**Attestations:** Path witness from scanner; VEX statement: Affected from vendor.example.com.\n\n**Decision:** Affected (score 0.72). Mitigation recommended: Upgrade to version 4.17.21."
}
CLI Usage
Table Output (Default)
stella verdict rationale 12345
Finding: 12345
Rationale ID: rationale:sha256:abc123...
Generated: 2026-01-07T12:00:00Z
+--------------------------------------+
| 1. Evidence |
+--------------------------------------+
| CVE-2024-1234 in `pkg:npm/lodash... |
+--------------------------------------+
+--------------------------------------+
| 2. Policy Clause |
+--------------------------------------+
| Policy S2.1: severity>=high AND... |
+--------------------------------------+
+--------------------------------------+
| 3. Attestations |
+--------------------------------------+
| Path witness from scanner; VEX... |
+--------------------------------------+
+--------------------------------------+
| 4. Decision |
+--------------------------------------+
| Affected (score 0.72). Mitigation... |
+--------------------------------------+
JSON Output
stella verdict rationale 12345 --output json
Markdown Output
stella verdict rationale 12345 --output markdown
Plain Text Output
stella verdict rationale 12345 --output text
With Tenant
stella verdict rationale 12345 --tenant acme-corp
Integration
Service Registration
// In Program.cs or service configuration
services.AddVerdictExplainability();
services.AddScoped<IFindingRationaleService, FindingRationaleService>();
Programmatic Usage
// Inject IVerdictRationaleRenderer
public class MyService
{
private readonly IVerdictRationaleRenderer _renderer;
public MyService(IVerdictRationaleRenderer renderer)
{
_renderer = renderer;
}
public string GetExplanation(VerdictRationaleInput input)
{
var rationale = _renderer.Render(input);
return _renderer.RenderPlainText(rationale);
}
}
Input Requirements
The VerdictRationaleInput requires:
| Field | Type | Required | Description |
|---|---|---|---|
VerdictRef |
VerdictReference |
Yes | Reference to verdict attestation |
Cve |
string |
Yes | CVE identifier |
Component |
ComponentIdentity |
Yes | Component PURL, name, version |
Reachability |
ReachabilityDetail |
No | Vulnerable function, entry point |
PolicyClauseId |
string |
Yes | Policy clause that triggered verdict |
PolicyRuleDescription |
string |
Yes | Human-readable rule description |
PolicyConditions |
List<string> |
No | Matched conditions |
PathWitness |
AttestationReference |
No | Path witness attestation |
VexStatements |
List<AttestationReference> |
No | VEX statement references |
Provenance |
AttestationReference |
No | Provenance attestation |
Verdict |
string |
Yes | Final verdict status |
Score |
double? |
No | Risk score (0-1) |
Recommendation |
string |
Yes | Recommended action |
Mitigation |
MitigationGuidance |
No | Specific mitigation guidance |
Determinism
Rationales are content-addressed: the same inputs always produce the same rationale_id. This enables:
- Caching: Store and retrieve rationales by ID
- Deduplication: Avoid regenerating identical rationales
- Verification: Confirm rationale wasn't modified after generation
The rationale ID is computed as:
sha256(canonical_json(verdict_id + witness_id + score_factors))
Related Documents
- Verdict Attestations - Cryptographic verdict proofs
- Policy DSL - Policy rule syntax
- Scoring Profiles - Risk score computation
- VEX Trust Model - VEX statement handling