# Verdict Rationale Template > **Status:** Implemented (SPRINT_20260106_001_001_LB) > **Library:** `StellaOps.Policy.Explainability` > **API Endpoint:** `GET /api/v1/triage/findings/{findingId}/rationale` > **CLI Command:** `stella verdict rationale ` --- ## Overview **Verdict Rationales** provide human-readable explanations for policy verdicts using a standardized 4-line template. Each rationale explains: 1. **Evidence:** What vulnerability was found and where 2. **Policy Clause:** Which policy rule triggered the decision 3. **Attestations:** What proofs support the verdict 4. **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) ```bash curl -H "Authorization: Bearer $TOKEN" \ "https://scanner.example.com/api/v1/triage/findings/12345/rationale?format=json" ``` **Response:** ```json { "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) ```bash curl -H "Authorization: Bearer $TOKEN" \ "https://scanner.example.com/api/v1/triage/findings/12345/rationale?format=plaintext" ``` **Response:** ```json { "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) ```bash curl -H "Authorization: Bearer $TOKEN" \ "https://scanner.example.com/api/v1/triage/findings/12345/rationale?format=markdown" ``` **Response:** ```json { "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) ```bash 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 ```bash stella verdict rationale 12345 --output json ``` ### Markdown Output ```bash stella verdict rationale 12345 --output markdown ``` ### Plain Text Output ```bash stella verdict rationale 12345 --output text ``` ### With Tenant ```bash stella verdict rationale 12345 --tenant acme-corp ``` --- ## Integration ### Service Registration ```csharp // In Program.cs or service configuration services.AddVerdictExplainability(); services.AddScoped(); ``` ### Programmatic Usage ```csharp // 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` | No | Matched conditions | | `PathWitness` | `AttestationReference` | No | Path witness attestation | | `VexStatements` | `List` | 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](verdict-attestations.md) - Cryptographic verdict proofs - [Policy DSL](dsl.md) - Policy rule syntax - [Scoring Profiles](scoring-profiles.md) - Risk score computation - [VEX Trust Model](vex-trust-model.md) - VEX statement handling