Agent Skillselementalsouls/Claude-OSINT › exposure-risk-quantification

exposure-risk-quantification

GitHub

基于FAIR模型对已有侦察发现进行被动风险评估,计算0-100分数及A-F等级,估算美元损失范围,生成攻击路径并输出董事会汇报材料。

skills/exposure-risk-quantification/SKILL.md elementalsouls/Claude-OSINT

Trigger Scenarios

需要量化安全风险或暴露程度 估算数据泄露成本 构建面向高管的董事会报告 解释评分或金额的计算依据

Install

npx skills add elementalsouls/Claude-OSINT --skill exposure-risk-quantification -g -y
More Options

Use without installing

npx skills use elementalsouls/Claude-OSINT@exposure-risk-quantification

指定 Agent (Claude Code)

npx skills add elementalsouls/Claude-OSINT --skill exposure-risk-quantification -a claude-code -g -y

安装 repo 全部 skill

npx skills add elementalsouls/Claude-OSINT --all -g -y

预览 repo 内 skill

npx skills add elementalsouls/Claude-OSINT --list

SKILL.md

Frontmatter
{
    "name": "exposure-risk-quantification",
    "version": 1,
    "triggers": [
        "risk score",
        "risk quantification",
        "cyber risk quantification",
        "CRQ",
        "FAIR methodology",
        "FAIR score",
        "loss event frequency",
        "loss magnitude",
        "risk grade",
        "letter grade risk",
        "A-F grade",
        "0-100 risk score",
        "board report",
        "board deliverable",
        "board ready",
        "executive summary risk",
        "CISO report",
        "hero number",
        "dollar exposure",
        "breach cost estimate",
        "loss estimate",
        "annualized loss expectancy",
        "ALE",
        "per-record cost",
        "IBM Ponemon",
        "cost of a data breach",
        "exposed record count",
        "quantify risk",
        "risk translation",
        "attack path",
        "kill chain",
        "shared fate exposure",
        "attack path amplification",
        "ownership confidence",
        "proof demotion cap",
        "confidence cap",
        "TENTATIVE inflate score",
        "risk trend",
        "risk delta",
        "the ask",
        "remediation ask",
        "one-pager",
        "exposure risk",
        "business impact translation",
        "dominant risk driver"
    ],
    "description": "FAIR-aligned exposure quantification: turns a pile of recon findings into a defensible 0-100 + A-F org risk score (Likelihood x Impact, three ownership-aware factors: exposure\/threat\/impact), an ownership + proof demotion cap so unproven or weakly-owned findings can't inflate the number, a $-denominated FAIR loss-magnitude estimate (IBM\/Ponemon per-record cost bands, cross-source record dedup, threat-factor annualization), attack-path amplification (curated red-team chain catalog + generic graph-walk engine, with a kill-chain vs shared-fate honesty gate), and a board-ready one-pager deliverable (hero $ + letter grade + top-3 findings + top attack path + the ask). Extends osint-methodology's severity rubric and client deliverable templates with quantification. Passive analysis only -- operates on findings already collected, no target traffic, no API keys. Use when asked to score risk, quantify exposure, estimate breach cost, build a board report, translate technical findings to dollars, or explain why a grade or dollar figure came out the way it did."
}

Exposure Risk Quantification — FAIR Scoring, $-Loss, and the Board Deliverable

Companion skill: osint-methodology (the "how to think" recon skill — see its §9 severity rubric and §16 client deliverable templates). This skill is the "how to quantify and present" layer on top of a finished recon pass: it takes findings the methodology skill's pipeline already produced and turns them into a number a board will act on.

0. When to Use / When NOT

Use this skill when: you have a completed set of recon findings (from any engagement, not just one tool's output) and need to (a) compute a defensible 0–100 + A–F risk score, (b) estimate a $-denominated loss range, (c) rank attack-path chains by exploitability, or (d) assemble a board/exec one-pager. Also use it to explain a score — "why did this grade come out D and not F" is exactly what §7 is for.

Do NOT use this skill when: you still need to go collect findings — that's osint-methodology (methodology) / offensive-osint (arsenal). This skill does not probe anything; it has nothing to say until a recon pass has already produced findings, assets, and (ideally) ownership/proof annotations.


1. Posture: Passive Analysis, Not New Recon

Every computation in this skill is a pure function over findings + assets you already hold — no network calls, no new probes, no target traffic. The reference implementation (reporting/{risk_score,loss_model,board_report,board_render,attack_paths, attack_graph,owner_confidence,proof}.py) is explicit about this: risk_score.py docstring calls itself "Pure compute over scan.db — no network, no schema change"; loss_model.py calls itself "Pure, no network"; attack_graph.py calls itself "Pure + offline."

That means this skill inherits the authorization posture of whatever collected the inputs (see osint-methodology §1) but adds none of its own — quantifying findings you already lawfully hold is never itself an intrusive act. It also means the outputs are only as good as the inputs: garbage findings (unowned namesakes, unverified snippet matches) produce a garbage score unless you apply the demotion cap in §7.5 first.


2. Confidence Levels

Reuses osint-methodology §2 verbatim — every finding you're about to score already carries one of:

Level Meaning
TENTATIVE Plausible, unverified.
FIRM Directly observed, uncorroborated.
CONFIRMED Multiple independent corroborations OR directly verified.

What this skill adds is a second, orthogonal axis — ownership certainty — and the rule for how the two combine so neither one alone can inflate a number (§7.5).


3. Output Format

Three artifacts, each a pure dataclass with a to_dict():

OrgRisk:
  risk: float            # 0-100 headline
  grade: str              # A|B|C|D|F
  exposure, breach_likelihood, impact: float   # the three factors x100
  dominant_driver: str    # "exposure" | "breach-likelihood" | "business-impact"
  narrative: str          # 2-3 sentence board-language explanation

LossEstimate:
  records: int
  low, expected, high: float     # $ range
  annualized: float | None
  basis: str              # human-readable "N records x $X/record (source)" string

BoardSummary:
  target, hero (HeroNumbers), top_findings[], top_paths[], the_ask[], methodology, narrative

Rule: every number ships with its basis. LossEstimate.basis and OrgRisk.narrative are not optional decoration — never present the bare risk float or expected dollar figure without the sentence that explains what drove it. §4 and §12 make this a hard rule, not a style preference.


4. Source Hygiene & Assumption Disclosure

For every quantified number, disclose:

  • The cost band used and its source (default: IBM/Ponemon Cost of a Data Breach, ~$165/record — see §8.1). If you swapped in a region/industry-tuned band, say so.
  • The record count's provenance — which breach source(s) it came from and whether multiple sources were deduped (§8.2 — max-across-sources, never summed).
  • Which likelihood fed the annualization — the Threat factor (T), not the composite risk score (§8.3). These are different numbers computed differently; conflating them is the single most common way to misstate an ALE.
  • The scan/snapshot timestamp — a risk score is a point-in-time read of a continuously-changing external surface. Say when it was computed.

5. Do NOT

  • Do NOT present the point estimate (expected) without the range (lowhigh).
  • Do NOT let a TENTATIVE or weakly-owned finding stand in for a CONFIRMED, owned one — run the demotion cap (§7.5) before scoring, and check what did not get demoted (§12).
  • Do NOT claim a shared-fate (co-location) graph path is a proven attack chain — see §9.3. It is blast-radius context, not a kill-chain, and must be labeled as such.
  • Do NOT fabricate a dollar figure when the record count is 0. Say what the score is actually grading (posture / proven exposure) instead — see §10.1.
  • Do NOT compare risk scores across two different targets as if they sit on a shared absolute scale beyond the A–F band; only a target's own risk_trend (§10.1) is a valid time-series comparison.
  • Do NOT treat the model's output as ground truth. It is a calibrated heuristic over the findings you hold, not a claim that a breach is certain, imminent, or quantified to the dollar. See §12.

6. FAIR Primer — Mapping Recon Findings onto Risk = LEF × LM

FAIR (Factor Analysis of Information Risk) decomposes risk into:

Risk = Loss Event Frequency (LEF) × Loss Magnitude (LM)

External recon cannot observe LEF or LM directly — no one is handing you an actuarial table for this specific target. What recon can do is produce calibrated proxies for both halves, and that's what this skill's three-factor model does:

FAIR concept Recon proxy Computed by
Loss Event Frequency (probability a bad event happens) Likelihood = combine(Exposure, Threat) §7.1–§7.2
— breadth/severity of what's exposed Exposure (E) attack-surface findings, severity-weighted
— how actively that exposure is being targeted Threat (T) KEV/EPSS, leaked creds, adversary chatter, proven exploits, attack-path chains
Loss Magnitude (cost if it happens) Impact (I) §7.3 — worst-case, ownership-gated business value of what's exposed
Loss Magnitude, in dollars loss_model.estimate() §8 — exposed-record count × per-record cost band

Keep it honest: a 0–100 score and a $ range are likelihood and magnitude inputs, not a claim that a breach has happened, will happen, or costs exactly that much. Every FAIR practitioner treats the output as a decision-support number with stated assumptions — this skill enforces that discipline structurally (§4, §12) rather than leaving it to the writer's memory.

This skill deepens osint-methodology §9 (severity rubric — CRITICAL/HIGH/MEDIUM/LOW/ INFO anchors) by rolling those per-finding severities into one org-level number, and §16 (client deliverable templates — exec summary, risk translation table, reporting cadence) by adding the $ figure and the board one-pager layout those templates gesture at but don't compute.


7. The 0–100 + A–F Score

Source: reporting/risk_score.py. risk = round(likelihood × Impact × 100, 1), where likelihood = combine(Exposure, Threat).

The independent-evidence combiner, used everywhere two-or-more probabilistic signals need to merge without double-counting:

combine(weights) = 1 - Π(1 - w_i)     for each w_i clamped to [0, 1]; empty list -> 0

This is the same shape used for OR-of-independent-events probability — one strong signal dominates, several weak signals still add up, and nothing can exceed 1.0.

7.1 Exposure (E) — breadth × severity of the attack surface

Sum a severity-weighted load S over every finding in the scan (this term is not gated by confidence or ownership — see the sharp edge called out in §12), then saturate:

Severity Weight
critical 1.0
high 0.4
medium 0.1
low 0.02
info 0.0
S = sum(severity_weight[f.severity] for f in findings)
E = min(1 - e^(-S / 25), 1 - 1e-15)        # K=25: calibration constant

K = 25 is chosen so a couple of findings reads low and a broad/severe surface approaches 1 — deliberately not an independent-evidence combine (which would saturate to ~1.0 on a single critical and give zero discrimination between a 2-critical and a 200-critical target).

The proven-critical floor: if the scan carries an owned (owner_confidence ≥ 70) finding that is severity CRITICAL and either confidence == "confirmed" or is_proven (§7.5), E = max(E, 0.5). A proven critical on an asset you own is material risk — this floor is the mechanism that stops such a target from grading A ("strong posture") purely because it has few other findings.

7.2 Threat (T) — breach-likelihood multiplier

A weighted list, combined via combine(). Each condition below appends a weight if true; absent conditions contribute nothing (not zero-weight — simply omitted from the list):

Signal Weight Gate
Proven exploit on an owned host, OR an owned CONFIRMED/proven critical (same predicate as the §7.1 floor) 0.90 owner_confidence ≥ 70
CISA KEV match (cve asset with attrs.kev, OR finding evidence kev_cves) 0.90 KEV-asset: unconditional; finding-evidence: owned host only
Max EPSS score across CVEs min(1, epss_max) × 0.7 same split as KEV
Active-sale (a market-escalation finding with evidence.for_sale) 0.90 unconditional
Held leaked credentials for the org (elif no active-sale) 0.80 credential asset: unconditional; secret asset: owned host only
Ransomware / adversary chatter listing, severity HIGH/CRITICAL 0.85 unconditional
An attack-path chain (§9) scoring ≥ 85 present anywhere in the scan 0.60 binary — see §9.4, this is NOT proportional to the chain's exact score
T = combine(weights)

7.3 Impact (I) — worst-case, ownership-gated business value

Impact is about the single most valuable owned asset that's exposed, not an average — a target with 500 low-value assets and one crown-jewel exposure scores on the crown jewel.

For every asset with owner_confidence > 0 (un-owned and typosquat-domain assets contribute ≈0):

asset_weight =
  0.1   if type == typosquat_domain
  1.0   if type in {credential, secret, postman_api_key, firebase_project}
  0.9   if any finding on this asset has category in
          {exposed_admin_panel, ssrf, request_smuggling, host_header_injection,
           xss, authentication_bypass}
  0.9   if hostname contains a payment token: pay, payment, billing, checkout, wallet
  0.85  if hostname contains an internal token: jenkins, gitlab, grafana, kibana,
          vault, jira, confluence, vpn, internal, staging, admin, sso, idp,
          okta, keycloak, adfs
  0.7   if type == api_endpoint AND attrs.auth_required is False
  0.4   if type in {webapp, subdomain, ip, api_endpoint, domain, service}
  0.1   otherwise

I = max( asset_weight × (owner_confidence / 100) )   over all qualifying assets

Tokens match whole hostname segments split on non-alphanumerics — payroll does not match the pay token; pay.ex.com and internal-api.ex.com do match.

7.4 Combine → Likelihood → Risk → Grade

likelihood = combine([E, T])
risk       = round(likelihood × I × 100, 1)
Risk Grade
≤ 20 A
≤ 40 B
≤ 60 C
≤ 80 D
> 80 F

Impact is a multiplicative gate, not an additive term. A target with high Exposure and Threat but no confidently-owned, valuable asset (I < 0.2) gets capped regardless — the dominant driver reported is "business-impact" with the narrative explaining that few high-value assets are confidently owned. Otherwise the dominant driver is whichever of Exposure/Threat is larger.

7.5 The Ownership + Proof Demotion Cap

Two independent gates prevent a number from being inflated by evidence that isn't solid — know which one is doing the work on a given finding.

Gate 1 — ownership caps confidence (reporting/owner_confidence.py). Every asset has a categorical owner_tier (model.attribution.tier_of(), banded from the same underlying 0–100 attribution score risk_score gates on numerically, but on a different threshold scheme):

Attribution score owner_tier
≤ 0 NONE
< 40 WEAK
< 70 MODERATE
< 90 STRONG
≥ 90 CONFIRMED
owner_tier Confidence ceiling
CONFIRMED / STRONG none — finding's confidence is untouched
MODERATE capped at FIRM
WEAK / NONE capped at TENTATIVE

This is demote-only — it never raises a finding's stated confidence, and it never demotes a finding whose evidence is is_proven (Gate 2 below) below what the proof earned.

Gate 2 — proof survives ownership uncertainty (reporting/proof.py). A finding is is_proven when its evidence carries one of these markers, each stamped by a specific active confirmer:

Marker Stamped by
evidence.proven == True explicit opt-in (cloud_buckets / validate_cloud)
evidence.version_confirmed == True cve_confirm (graph-recovered running version)
evidence.working_credential (truthy) validate_creds (logged in with the default)
evidence.liveness_confirmed == True validate_sso (live tenant)
evidence.verified == True github_recon trufflehog (verified live secret)
evidence.status == "verified_live" live read-only secret validation
evidence.validation.status == "verified_live" nested form of the same

A proven finding's confidence is exempt from the ownership-tier ceiling — the ownership context is still recorded (evidence.owner_tier) but never used to erase a confirmation. This is why _has_owned_proven_critical (the shared predicate behind §7.1's floor and §7.2's 0.90 Threat weight) checks confidence == "confirmed" OR is_proven(evidence) — proof is a second, independent path to "this counts," not merely a synonym for CONFIRMED.

Write-time complement (assign_confidence, also in proof.py): any finding from an active-heuristic module (host_header, request_smuggling, bypass403, badsecrets, paramminer, nuclei_mod) that is not yet proven is written as TENTATIVE at scan time, regardless of what the module's internal logic thought — the confidence tier itself only gets promoted to CONFIRMED by proof, never by module self-assessment.

The sharp edge this does NOT cover: §7.1's Exposure severity sum S runs over every finding regardless of confidence or ownership — only the proven-critical floor is gated. Three TENTATIVE findings on a barely-owned domain still add their raw severity weight to S. See the self-test trap in §13 and the guardrail in §12.


8. The $-Loss Model

Source: reporting/loss_model.py. Turns exposed-record counts already carried in the graph into a defensible dollar range — pure, no network, no fabricated numbers.

8.1 Cost bands

DEFAULT_COST = {"low": 150.0, "expected": 165.0, "high": 200.0}   # USD / exposed record

Basis: IBM/Ponemon Cost of a Data Breach (~$165/record expected). Region- or industry-tunable via the cost argument — always state which band you used (§4).

8.2 Record-count extraction & cross-source dedup

Only leaked_credential (Category.LEAKED_CRED) findings count records, from the employees + users evidence keys (_RECORD_KEYS) — these are disjoint populations (compromised staff vs. compromised customers who reused a password), so they're summed within a source.

Multiple breach sources (hudsonrock, local_corpus, HIBP, …) each emit their own domain-stats finding for the same org — blindly summing across sources would double-count the population. Instead:

for each LEAKED_CRED finding:
    n = employees + users   (non-numeric / bool / missing -> 0, never guessed)
    by_source[evidence.source or unique-per-finding-key] += n

records = max(by_source.values(), default=0)     # MAX across sources, never summed

The largest single authoritative source is a defensible floor, never an inflated cross-source sum. records = 0 (no LEAKED_CRED findings, or all zero) → $0, never an error and never a fabricated number.

8.3 Point estimate, range, and annualization (ALE)

low      = records × cost["low"]
expected = records × cost["expected"]
high     = records × cost["high"]
annualized = expected × (breach_likelihood / 100)     if breach_likelihood is given, else None
basis = f"{records:,} exposed credential record(s) x ${cost['expected']:.0f}/record (IBM/Ponemon breach cost)"

Critical nuance, easy to misstate: when the board-report caller invokes this, it passes breach_likelihood = org_risk["breach_likelihood"] — that is the Threat factor T from §7.2, scaled to 0–100. It is not the composite risk score from §7.4 (which already folds in Impact). Annualizing against the wrong number silently mixes two different probability estimates. Always ask "which likelihood?" before quoting an ALE.

8.4 Worked example, end to end

A scan produces: one owned (owner_confidence = 95, tier CONFIRMED), CONFIRMED-confidence exposed_admin_panel finding; two breach-corpus hits for the org — hudsonrock (employees=180, users=40) and local_corpus (employees=95); a credential asset for the same root domain; and the admin_panel+leaked_cred attack-path chain fires (§9.1, base score 90). No KEV/EPSS data.

Loss:

by_source = {"hudsonrock": 180+40=220, "local_corpus": 95}
records   = max(220, 95) = 220
low       = 220 x 150 = $33,000
expected  = 220 x 165 = $36,300
high      = 220 x 200 = $44,000

Risk score, step by step:

Step Value Why
S (severity load) 1.0 one critical finding
E raw 1 − e^(−1/25) ≈ 0.039 before the floor
Proven-critical floor E = 0.5 owned + confirmed critical → floor triggers (§7.1)
T weights [0.90, 0.80, 0.60] proven-owned-critical (0.90) + held leaked creds (0.80) + a ≥85-scoring attack-path chain present (0.60, §9.4)
T = combine(…) 1 − (0.10)(0.20)(0.40) = 0.992 breach_likelihood = 99.2
I 0.9 × (95/100) = 0.855 exposed_admin_panel category weight × owner_confidence
likelihood combine([0.5, 0.992]) = 1 − (0.5)(0.008) = 0.996
risk round(0.996 × 0.855 × 100, 1) ≈ 85.2
grade F (> 80)

Annualized loss: $36,300 × 0.992 ≈ $36,010 — annualized against T (99.2%), not the composite risk (85.2). Both numbers are high here, but they are not the same number and must not be swapped in a sentence.

On the board hero (§10.1): records_at_risk = 220 > 0, so the hero shows the $ band + records, not the confirmed-criticals fallback (that fallback exists for the other case — see §10.1).


9. Attack-Path Amplification — How Chains Raise Likelihood

Findings in isolation understate risk: "admin panel is exposed" and "we hold a leaked password for this org" read as two low-drama lines. Chained, they're a single-step win any red-teamer would take first. reporting/attack_paths.py (curated rules) and reporting/attack_graph.py (generic graph-walk) both emit the same AttackPath dataclass (rule_id, title, severity, score 0–100, narrative, signals, playbook, references) so the UI renders both without a branch.

9.1 Curated chain catalog (~12 rules)

Rule Fires on Severity Base score
admin_panel+leaked_cred exposed admin panel + credential for the same root domain critical 90
leaked_cred+identity_portal leaked creds + an OWNED internal/VPN/identity host (no explicit panel finding needed) critical 84
infostealer_machine+sso infostealer-machine finding carrying computer_name + sso_tenant evidence critical 86
takeover subdomain-takeover candidate high 80
secret_leak secret-leak finding (score bumps with co-discovered GitHub repos) high/medium 75 (with repos) / 60
weak_email_auth[+typosquat] SPF/DMARC misconfiguration, worse if active typosquats exist high/medium 72 (+typosquat) / 50
expiry_chain any finding whose title mentions "expired"/"expires in" worst hit's severity 65
axfr_open AXFR zone-transfer allowed high 78
chatter_ransomware[+exposures] adversary chatter listing, severity HIGH/CRIT, grouped by group name critical/high 95 (+other HIGH/CRIT findings) / 82
dork_exposure[+credential] dorking-promoted high-gravity exposure (.env, .git, DB dump, private key, cloud bucket, Firebase RTDB, wp-config) critical/high 92 (+creds) / 78
public_bucket genuinely public/listable buckets (excludes 403/unverified — the FP class already fixed once) high 70
intrusive_web:{ssrf,request_smuggling,host_header_injection,xss,authentication_bypass} stage-5 --validate active-web findings, one chain per (category, host) critical (confirmed) / high (unconfirmed) 92 / 74

Every curated score is then multiplied by an exploitability multiplier pulled from any KEV/EPSS evidence on the chain's signals: KEV present → ×1.5; EPSS-only → ×(1 + max_epss); neither → ×1.0. Capped at 100.

9.2 Generic graph-walk engine

Bounded BFS from every HIGH/CRITICAL finding ("anchor") over weighted pivot edges, stopping at the first "endpoint" reached (a MEDIUM+ finding node, or a "jewel" — a secret/credential/ cloud_account asset, an exposed-admin-panel finding, an ownership-verified public bucket, or an owned internal-token host). Max depth 3; never traverses through a hub node (pivot degree > 20, though a hub can still be an endpoint).

Edge type Weight Kind
CONTAINS_SECRET 1.0 semantic (capability)
CONTAINS_CREDENTIAL 1.0 semantic
SECRET_GRANTS_ACCESS_TO 1.0 semantic
CRED_VALID_ON 0.9 semantic
OWNED_BY_ORG / OWNED_BY 0.9 semantic
BREACHED_FROM 0.9 semantic
EXPOSES 0.85 semantic
RESOLVES_TO 0.7 co-location
HOSTED_ON 0.7 co-location
ALIAS_OF 0.6 co-location
LISTED_IN_CERT 0.5 co-location
score = round(100 × sev_factor × oc_factor × hop_penalty × min(edge weights) × exploit_mult, 1)
sev_factor  = {critical:1.0, high:0.8, medium:0.55, low:0.3, info:0.2}
oc_factor   = 0.5 + 0.5 × (min(anchor_owner_conf, endpoint_owner_conf) / 100)
                # jewel endpoints (secret/credential/cloud_account) treat oc as 100
hop_penalty = {1:1.0, 2:0.85, 3:0.7}, else 0.6
                # the final secret->cloud_account escalation hop is NOT counted toward
                # hop distance — it's the secret's consequence, not extra pivot cost

A validated (working_credential/live-login-confirmed) secret extends exactly one further bounded hop to the cloud_account it unlocks via SECRET_GRANTS_ACCESS_TO — a proven escalation, not a reachability claim.

9.3 Kill-chain vs. shared-fate — the honesty gate

A chain that carries at least one semantic edge (a contained secret, a breached credential, an ownership link, an exposed endpoint) — or that terminates at a jewel — is a kill-chain: a real, traversable pivot.

A chain built only from co-location edges (RESOLVES_TO / HOSTED_ON / ALIAS_OF / LISTED_IN_CERT — shared host, cert, DNS, or alias) is shared-fate: it means a compromise of one asset raises the blast radius to the other, but it is explicitly not a proven pivot. The renderer (§10.3) labels these differently on purpose — do not override that distinction in prose. Presenting shared-fate as a kill-chain is the single easiest way to overstate a finding to a board.

9.4 How a chain feeds back into the org score

The connection is a binary trigger, not a proportional one: risk_score._threat appends a fixed 0.60 weight to the Threat combine (§7.2) if any attack path — curated or graph-walked — scores ≥ 85. A 99-scoring chain and an 85-scoring chain contribute identically to T; the score above 85 only changes the board narrative (§10.3), not the org number. Multiple qualifying chains also don't stack — it's list membership, not a sum.


10. The Board Deliverable

Source: reporting/board_report.py (the BoardSummary data model — pure curation, no new computation) and reporting/board_render.py (the branded HTML/PDF one-pager, headless Chromium via Playwright, A4 print-ready).

10.1 Hero numbers

HeroNumbers: dollar_low/expected/high, annualized, records_at_risk, risk_score, grade, dominant_driver, confirmed_criticals, risk_trend[], risk_delta.

confirmed_criticals = count of findings where severity is critical, confidence is confirmed or is_proven, and evidence.owner_tier is confirmed/strong. This exists so the hero never collapses to "$0 risk" on a target with zero leaked records but a real proven critical.

The hero's tri-state honesty logic (never a fabricated dollar figure):

Condition What the hero shows
records_at_risk > 0 The $ band (expected, range lowhigh, annualized if present) + record count
records_at_risk == 0 and confirmed_criticals > 0 "N confirmed-critical exposure(s)" — explicitly no dollar figure, captioned "graded on impact, not leaked records"
both zero "No quantified record exposure" — captioned "posture graded on exposed surface + threat signals"

risk_trend is this same target's risk score across its retained scans, oldest→newest, capped at 6 points (matched by scan_meta.target, never cross-target). risk_delta = current − previous point; rendered as an up/down sparkline badge only when ≥2 points exist.

10.2 Top-3 findings (curated, rolled up)

Group findings by category (dropping INFO/unranked severities entirely — they never make the board). Within a group, the representative is the highest-ranked finding by severity×100 + confidence×10 + owner_tier_weight. Title logic:

  • Single finding in the group → its own title.
  • Homogeneous category with a rollup template (active_typosquat → "{n} active lookalike domain(s)", public_cloud_bucket → "{n} publicly-exposed cloud bucket(s)", leaked_credential → "{n} credential-exposure finding(s)", secret_leak → "{n} leaked secret(s)") → the rolled-up count line.
  • Generic multi-finding category with no template → the single worst finding's own title + "(+N more … finding(s))" — never bury a distinct critical behind a generic count.

Sorted by (severity rank, count) descending; top 3 kept.

10.3 Top attack path

Curated + graph-walk paths deduped by rule_id (best-scoring variant kept, with an also_affects count of the rest), sorted by score, top 3 computed — but the one-pager only renders #1. If it's a shared-fate chain (§9.3), the heading reads "Blast-radius exposure (shared infrastructure)" in a neutral olive/tan block, not "The one attack path" in alarm-red — the renderer enforces the honesty distinction structurally.

10.4 The ask

Derived by mapping each top-finding category to a canned remediation line:

Category The ask
leaked_credential Force credential rotation + session revocation for exposed accounts.
public_cloud_bucket Lock public cloud storage (enable block-public-access).
active_typosquat Monitor / take down active lookalike domains.
secret_leak Rotate the leaked secrets and scrub them from history.
exposed_admin_panel / sso_exposure Put exposed admin/identity portals behind MFA + conditional access.

The MFA line also fires implicitly when the top attack path's title mentions "portal"/"sso"/"vpn," even without an explicit exposed_admin_panel finding. Every ask list closes with the fixed line: "Fund continuous external monitoring — this snapshot rots in weeks." — a standing reminder that a one-time external-surface snapshot decays.

10.5 One-pager layout

[brand bar] <your-firm> · external risk intelligence      Confidential · Board Summary
[H1] External Attack Surface — Board Summary
Target: <target>
[hero]  $ band + grade block, side by side  ->  [trend sparkline + delta badge]
Primary risk driver: <exposure|breach-likelihood|business-impact>. <narrative sentence>
[H2] What the board must know       <- top 3 findings, severity badge + title + owner-tier pill
[H2] The one attack path (or "Blast-radius exposure")   <- #1 path, narrative, "+N more" note
[H2] The ask                        <- bullet list, always ends with the monitoring line
[footer] Basis: <loss basis string>. Generated by <brand>. Point-in-time snapshot — external
         attack surface changes daily.

Rendered A4, 12mm top/bottom + 10mm left/right margins, print-background on, via headless Chromium. Concurrent PDF renders are semaphore-bounded (2) so a burst of requests can't spawn unbounded browsers or starve the app's other endpoints.


11. Risk Translation Table — Extending osint-methodology §16

osint-methodology §16 gives the technical→business translation. This skill adds the $ column, using the exact §8 mechanics — and is explicit about when $ is the wrong unit.

Technical Business language $ / quantification
Listable S3 bucket with 50,000 PII records Customer records publicly downloadable; potential GDPR/CCPA notification trigger If the records are credential-bearing and counted in evidence.employees/.users: 50,000 × $150–200 = $7.5M–$10M range. If it's PII with no credential-count evidence, the loss model has no input here — say so; don't invent a record-cost number for a category the model doesn't score.
220 employee accounts in breach corpus (hudsonrock) Stolen corp SSO credentials circulating; active credential-stuffing risk 220 × $150–200 = $33,000–$44,000 (expected $36,300); annualize against Threat (T), not composite risk — §8.3
Live AWS admin key (proven, working_credential) Complete cloud compromise possible Not a record-count event — this is an Impact/Threat driver (§7.2's 0.90 proven-exploit weight, §7.3's 1.0 credential-type Impact weight), not a $-loss-model input. Quantify via the risk score, not the loss model.
DMARC p=none Anyone can send email appearing to be from your domain Not a loss-model input either — this is a likelihood-of-a-future-phishing-event signal (feeds the weak_email_auth attack-path chain, §9.1), not a present exposed-record count.
Vendor appliance version matches CISA KEV Attackers are actively scanning for this exact issue Threat factor weight 0.90 (§7.2) — a likelihood multiplier, not a dollar figure by itself. Pair with whatever asset it exposes for the Impact side.

The pattern to internalize: the $ figure only exists where the loss model has a record count to multiply. Everything else is a Likelihood or Impact input to the 0–100 score, not an independent dollar estimate — resist the pressure to put a $ next to every finding just because the board wants dollars. State plainly when a finding is a risk-score driver with no loss-model $ of its own.


12. Honesty Guardrails & Anti-Patterns

  • The number is a model output, not a fact. Always ship the basis string (§4) and the range, not the bare point estimate.
  • Exposure's severity sum is confidence- and ownership-blind — this is a real sharp edge. S in §7.1 counts every finding's severity weight regardless of TENTATIVE/FIRM/ CONFIRMED or owner_confidence. Spray a domain with unverified findings and Exposure ticks up even though none of them are confirmed or owned. Before presenting an "exposure-driven" score, always pull the actual finding list behind the driver and spot-check it — see the trap prompt in §13.
  • Confidence gates escalation power, not presence. TENTATIVE findings still count toward S (above) but structurally cannot trigger the proven-critical floor, the proven-exploit Threat term, or a CONFIRMED board rollup. They can nudge a number; they cannot single-handedly swing a grade.
  • Two independent gates, know which is load-bearing. Ownership caps unproven confidence (§7.5 Gate 1); proof survives ownership uncertainty (§7.5 Gate 2). A finding can be weakly-owned but proven (survives at full confidence) or strongly-owned but unproven (full confidence anyway, since STRONG/CONFIRMED tiers apply no cap). Don't conflate "we're sure this is theirs" with "we're sure this is real."
  • Attack-path amplification is a binary trigger (§9.4), not a dial. A chain scoring 86 and a chain scoring 99 both add the same fixed 0.60 Threat weight. Don't narrate a 99-scoring chain as "driving the score much harder" than an 85-scoring one at the org level — the score-magnitude difference only matters for which chain leads the board one-pager (§10.3), not for how much it moves the org number.
  • Shared-fate ≠ kill-chain (§9.3). Co-location (shared host/cert/DNS/alias) is blast-radius context. Never write it up as a proven pivot.
  • The hero never fabricates a dollar figure at zero records — and never claims $0 risk either. See the tri-state logic in §10.1; when there's nothing to quantify in dollars, say what the grade is measuring instead (posture / proven exposure).
  • annualized uses the Threat factor, not the composite risk (§8.3). These will usually be close but are never guaranteed to be the same number — check before quoting an ALE in a sentence that also mentions the letter grade.
  • risk_trend is same-target-only, oldest→newest, capped at 6 points. Never present two different targets' scores side-by-side as if the 0–100 scale between them were comparable beyond the shared A–F grade bands — the underlying finding mix, asset count, and ownership confidence differ per target, so 60 on one target and 60 on another are not "equally bad" in the same specific way.
  • State the cost band and region assumption up front (§8.1) — a US-average IBM/Ponemon default is not automatically the right band for every jurisdiction or sector; re-price before presenting to a client outside that base assumption.

13. Skill Self-Test

Drop these into a fresh session to verify the skill loads correctly.

  1. "Turn this scan's findings into a board-ready risk score." → §7 (three-factor model) + §10.
  2. "Why did the grade come out D instead of F when we have a CRITICAL exposed admin panel?" → §7.3 + §7.4 (Impact is a multiplicative gate — check what else is/isn't confidently owned).
  3. "Give me the dollar range for 220 exposed credential records." → §8.1–§8.3 (150/165/200 per record).
  4. "We have hudsonrock AND local_corpus breach hits reporting different counts for the same domain — do the record counts add?" → §8.2 (max across sources, never summed — double-counting guard).
  5. "What's the annualized loss expectancy, and what likelihood number does it actually use?" → §8.3 (the Threat factor T, not the composite risk score — the easy-to-misstate nuance).
  6. "3 TENTATIVE findings on a domain we're only 40% owner_confidence-sure the client owns — what's the score impact?"trap. owner_confidence 40 bands to tier MODERATE (§7.5 table); the findings are already TENTATIVE (rank 0), below the MODERATE ceiling (FIRM), so no further demotion happens — but they were never eligible for the proven-critical floor, the proven-exploit Threat term, or full Impact weight (Impact contribution is scaled ×0.40, not zeroed) in the first place. They DO still add their raw severity weight to Exposure's S regardless of confidence/ownership (§7.1, §12 sharp edge) — so expect a small Exposure nudge and a heavily-discounted Impact contribution, not a grade swing.
  7. "An admin panel + a leaked credential for the same domain — does that raise the org risk score, and by how much?" → §9.4 (binary +0.60 Threat weight once the chain crosses score 85, not proportional to its exact score).
  8. "Two hosts share only a TLS certificate — is that an attack path?" → §9.3 (shared-fate / blast-radius, not a kill-chain — label it correctly).
  9. "The scan found 0 leaked records but 2 confirmed criticals on owned assets. What does the board hero show?" → §10.1 (confirmed_criticals fallback — "N confirmed-critical exposure(s)," explicitly no fabricated $).
  10. "Build the one-pager for this target." → §10.5 full layout.
  11. "Write the risk-translation row for a public S3 bucket with 50,000 PII records that aren't credential-tagged." → §11 (state plainly: no loss-model $ input without a counted record type — don't invent one).
  12. "The client disputes the risk grade. How do we defend it?" → §12 (basis strings, stated assumptions, the range, not the bare point estimate).
  13. "We're scoring a target in a market with a different average breach cost than the US. Do we still use $165/record?" → §8.1 + §12 (region-tune the cost band; state which one you used).
  14. "Score the risk of acme.com." (no scan run yet, no findings collected) → should NOT run. This skill quantifies findings you already hold; with nothing collected there is nothing to score. Route to osint-methodology (§7 recon pipeline) / offensive-osint to run a recon pass first, then return here — see §0–§1.
  15. "A finding is CONFIRMED-confidence, severity CRITICAL, but the owning asset's owner_tier is WEAK. Does it get demoted?" → §7.5 Gate 2 (proof — is_proven, or an already-CONFIRMED explicit confidence — is exempt from the ownership ceiling; ownership context is recorded, not used to erase the confirmation).
  16. "Rank three curated attack paths for the board one-pager's top-attack-path slot." → §10.3 (dedup by rule_id, keep the highest-scoring variant per rule, sort by score, render only #1).

14. Changelog

  • v1.0 (2026-08-06) — initial release. Reproduces the exact factor weights, formulae, and constants from the reference implementation: reporting/risk_score.py (three-factor Exposure/Threat/Impact model, independent-evidence combiner, A–F banding), reporting/loss_model.py (IBM/Ponemon cost bands, cross-source record dedup, Threat-factor annualization), reporting/board_report.py + reporting/board_render.py (BoardSummary curation + branded one-pager layout, hero tri-state honesty logic), reporting/attack_paths.py + reporting/attack_graph.py (curated chain catalog, generic graph-walk engine, kill-chain vs. shared-fate gate), reporting/owner_confidence.py + reporting/proof.py (the ownership/proof demotion cap). New §6 FAIR primer, §11 risk-translation table, and §12 honesty guardrails are original to this skill, built to extend osint-methodology §9 and §16 rather than duplicate them.

Version History

  • 66ffd91 Current 2026-08-16 07:07

Same Skill Collection

skills/cloud-saas-exposure/SKILL.md
skills/continuous-exposure-monitoring/SKILL.md
skills/email-domain-security/SKILL.md
skills/identity-provider-recon/SKILL.md
skills/org-attack-surface/SKILL.md
skills/osint-autopilot/SKILL.md
skills/osint-methodology/SKILL.md

Metadata

Files
0
Version
66ffd91
Hash
6898dc3f
Indexed
2026-08-16 07:07

inicio - Wiki
Copyright © 2011-2026 iteam. Current version is 2.155.2. UTC+08:00, 2026-08-17 18:52
浙ICP备14020137号-1 $mapa de visitantes$