Files
git.stella-ops.org/docs/modules/policy/guides/verdict-rationale.md
2026-01-07 09:43:12 +02:00

8.5 KiB

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

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)

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))