AI agent skill · by @transilienceai
Risk Prioritiser
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.
Archived 2026-10-23 · Source updated 2026-07-29 · License: MIT. Check the original repository for the latest version.
SKILL.md instructions
# 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.