risk prioritiser

Skill

Risk-based prioritisation of confirmed attack paths. Combines exploit feasibility, technical CVSS severity, and asset business impact into a single ranked list driving remediation roadmaps.

Files2
  • @skills/risk-prioritiser/SKILL.md
  • @skills/risk-prioritiser/reference/scoring-formula.md

Risk Prioritiser

Consume the attack-path graph + the client-supplied business-tier map and emit a single ranked list of paths the org should remediate first. Mounted onto cloud-agent task #7.

Trigger

Cron daily after attack-path-stitcher (task #6) completes. Also event-fires when a new validated/*.json is written with nvd.score >= 9.0.

Workflow

  1. Load inputs.
    • artifacts/attack-paths.json — graph from task #6. Split into confirmed_paths (RFP-grade) and inferred_paths (topology/supply-chain).
    • business-tier-map.csv — client-supplied asset → tier. Search order: {OUTPUT_DIR}/business-tier-map.csvprojects/rfp-3.3/schemas/business-tier-map.csv.
  2. Score each confirmed_paths entry.
    • feasibility = product of every edge's feasibility along the path. For confirmed paths this is always 1.0.
    • technical_severity = max_cvss_along_path / 10.
    • business_impact = tier weight of the destination (crown_jewel = 1.0 by definition).
    • entry_exposure = 1.0 if entry node is external: true, else 0.5.
    • Final score = feasibility × technical_severity × business_impact × entry_exposure.
  3. Score each single-asset validated finding that does not sit on a confirmed path. Same formula; feasibility = 1.0, business_impact = asset's own tier weight, hop count = 1.
  4. Score each inferred_paths entry, but cap their bucket placement at theoretical regardless of numeric score. Inferred paths NEVER reach immediate / short_term / medium_term — RFP §3.3 requires "confirmed", and inferred paths are evidence-deficient by construction.
  5. Sort descending. Stable tie-break order: max CVSS desc → hop count asc → finding age desc (newer first).
  6. Bucket into roadmap tiers.
    • immediate (0-7 days): confirmed AND score ≥ 0.6
    • short_term (7-30 days): confirmed AND 0.3 ≤ score < 0.6
    • medium_term (30-90 days): confirmed AND 0.1 ≤ score < 0.3
    • monitor: confirmed AND score < 0.1
    • theoretical (track-only): any path with path_class != "confirmed". Surfaced in board reports but excluded from the remediation SLA roadmap.
  7. Write outputs to artifacts/attack-paths-ranked.json + attack-paths-ranked.md. Each row carries path_class so downstream consumers can filter.

Tier weights

TierWeight
crown_jewel1.00
revenue0.70
support0.40
dev0.20
unknown0.30
The unknown weight is deliberately above support to bias toward investigating un-mapped assets — they often turn out to be high-tier once discovered.

Output

text
{OUTPUT_DIR}/
  artifacts/
    attack-paths-ranked.json
    attack-paths-ranked.md
attack-paths-ranked.json schema:
json
{
  "generated_at": "2026-05-13T03:30:00Z",
  "tier_weights_used": {"crown_jewel": 1.0, "revenue": 0.7, "support": 0.4, "dev": 0.2, "unknown": 0.3},
  "ranked": [
    {
      "rank": 1,
      "kind": "path",
      "path_class": "confirmed",
      "path_id": "asset05->asset42->asset99",
      "hops": ["asset05", "asset42", "asset99"],
      "feasibility": 1.0,
      "max_cvss": 9.8,
      "business_impact": 1.0,
      "entry_exposure": 1.0,
      "score": 0.98,
      "bucket": "immediate",
      "remediation_focus": "asset05"
    },
    {
      "rank": 2,
      "kind": "finding",
      "path_class": "confirmed",
      "finding_id": "finding-018",
      "asset": "asset42",
      "feasibility": 1.0,
      "max_cvss": 9.1,
      "business_impact": 0.7,
      "entry_exposure": 1.0,
      "score": 0.637,
      "bucket": "immediate"
    }
  ],
  "buckets": {"immediate": 4, "short_term": 11, "medium_term": 22, "monitor": 8, "theoretical": 14}
}

Remediation focus

For each ranked path, pick a single remediation_focus asset — the one whose patch breaks the chain at the lowest cost:
  • If any edge has feasibility < 1.0 → focus on that edge's source asset. The weakest pivot is the attacker's cheapest hop; hardening it eliminates the easiest entry.
  • If every edge is feasibility 1.0 (fully confirmed chain) → focus on the path's entry asset (hops[0]). Patching the externally-reachable foothold breaks every downstream hop and is the cheapest patch surface operationally.
This rule yields a single deterministic asset id per path. The previous "edge contribution formula" was deprecated — on uniformly-confirmed chains every edge has identical contribution, which gives no useful signal.

Rules

  1. Always derive from attack-paths.json. Do not re-derive feasibility / CVSS — those are already validated by upstream tasks.
  2. business-tier-map.csv is authoritative. If an asset is missing from the map, use unknown tier weight + flag unmapped_assets[] at the top of the JSON output.
  3. Stable rank for stable input. Re-running on identical input must produce byte-identical output.
  4. No new findings. Prioritisation never creates findings; it only re-orders.
  5. Bucket thresholds are config. The 0.6/0.3/0.1 cuts can be overridden via --thresholds argument when running on a client with different remediation cadences. Default values match the Transilience report's "Immediate / Short-term / Medium-term" labels.

Implementation

The scoring is implemented deterministically by tools/risk-prioritise.py (not delegated to an LLM agent), to satisfy Rule 3 (stable rank for stable input). The tool reads artifacts/attack-paths.json and writes the two output files. Override the tier weights or bucket thresholds with --tier-weights / --thresholds JSON arguments — recorded in the output for audit.

References

  • reference/scoring-formula.md — worked example + edge-case behaviour.
  • projects/rfp-3.3/schemas/business-tier-map.csv — input schema and example rows.
  • tools/risk-prioritise.py — deterministic implementation.
risk-prioritiser — Kortix Marketplace | Kortix