---
name: transilienceai/risk-prioritiser
source: https://app.decimal.ai/s/transilienceai-risk-prioritiser@1/SKILL.md
source_sha256: ac7770e2dd2d
---

# 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.csv` → `projects/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

| Tier | Weight |
|---|---|
| `crown_jewel` | 1.00 |
| `revenue` | 0.70 |
| `support` | 0.40 |
| `dev` | 0.20 |
| `unknown` | 0.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

```
{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.