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 <finding-id>


Overview

This guide describes Stella Ops verdict rationales for triage engineers, auditors, and tooling authors who need a consistent, human-readable explanation of why a policy verdict was reached.

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

LinePurposeContent
EvidenceWhat was foundCVE ID, component PURL, version, reachability info
Policy ClauseWhy decision was madePolicy rule ID, expression, triage priority
AttestationsSupporting proofsBuild-ID matches, call paths, VEX statements, provenance
DecisionWhat to doVerdict 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:

FieldTypeRequiredDescription
VerdictRefVerdictReferenceYesReference to verdict attestation
CvestringYesCVE identifier
ComponentComponentIdentityYesComponent PURL, name, version
ReachabilityReachabilityDetailNoVulnerable function, entry point
PolicyClauseIdstringYesPolicy clause that triggered verdict
PolicyRuleDescriptionstringYesHuman-readable rule description
PolicyConditionsList<string>NoMatched conditions
PathWitnessAttestationReferenceNoPath witness attestation
VexStatementsList<AttestationReference>NoVEX statement references
ProvenanceAttestationReferenceNoProvenance attestation
VerdictstringYesFinal verdict status
Scoredouble?NoRisk score (0-1)
RecommendationstringYesRecommended action
MitigationMitigationGuidanceNoSpecific mitigation guidance

Determinism

Rationales are content-addressed: the same inputs always produce the same rationale_id. This enables:

The rationale ID is computed as:

sha256(canonical_json(verdict_id + witness_id + score_factors))