Skip to main content

Risk Scoring: Threat vs Exposure

Every endpoint in the catalog is scored on two independent axes, not one. This is the model's core idea: "is it being attacked?" and "is it vulnerable?" are different questions, and collapsing them into a single number would let a storm of ordinary misconfiguration flags drown out one real attack — or vice versa.

The two axes​

AxisFieldQuestionNature
Threatmax_risk_scoreIs something dangerous happening right now?An event — active attack / abuse
Exposuremax_posture_scoreHow open / vulnerable is the endpoint standing still?A state — config hygiene

At a glance — severity weights feed the two independent axes, which combine as a 2x2 for prioritization:

  • Threat collects the active-finding flags: BOLA, BFLA, brute-force, scanner / vuln-probe / path-scan, replay, rate & IP anomalies, PII leak, oversized response, latency/error-rate anomaly, sensitive-path keyword, threat-intel hit.
  • Exposure collects the standing-posture flags: anonymous access, plain-text / weak / legacy TLS, missing HSTS / CSP / X-Frame-Options / X-Content-Type-Options, permissive CORS, version disclosure, weak token TTL, internal / external host.

The UI renders them in distinct colour families (Threat = red family, Exposure = blue/purple) so the two scales never read as the same number. Which axis a flag feeds is defined by a fixed posture-flag set, not by the flag's class — the following flags feed Exposure; everything else feeds Threat:

internal_host, external_host, unauthenticated, plain_text_transport,
legacy_protocol, weak_tls_version, missing_hsts, missing_csp,
missing_x_frame_options, missing_x_content_type_options,
version_disclosure, permissive_cors, weak_token_ttl,
error_status, client_error_status

Severity weights​

Each risk flag has a coarse 1–10 severity weight. Operators tune at the score level, not by re-weighting individual flags:

SeverityWeightMeaning
Critical10Exploitable now — no further pivot needed
High7High-confidence attack signal or hardened-posture failure
Medium4Suggestive state — normal in moderation, alert on clusters
Low1Ambient context

Per request, the two axes are computed and the endpoint keeps the max ever seen on each:

  • Threat is max-anchored: the single worst active finding's severity, plus the remaining active findings at ¼ weight (clamped to 255). So a lone Critical outranks a pile of Mediums, and concurrent findings still add — but gently.
  • Exposure is the clamped sum of the standing posture-flag severities (plus ambient outcomes like error_status).

Both max_risk_score and max_posture_score in the inventory are $max-merged — they track the worst request ever seen on the endpoint.

Ordinary backend errors don't inflate Threat

A 5xx storm or a missing security header raises Exposure, never Threat. risk_score ranks by danger, not by misconfiguration or ordinary errors — so the top of a Threat-sorted list is real attack signal.

The UI bands the combined score for badge colour: 0 none · 1–9 low · 10–24 medium · 25–39 high · 40+ critical.

Current vs lifetime​

The inventory scores are monotonic — they only ever rise. That is correct for "worst ever" but useless for "is it still bad?". So the Threat badge overlays a current window on top of the lifetime max, using the ClickHouse rollups:

  • Current = the max threat over the last 7 days (ClickHouse-backed). This is the headline when available.
  • Lifetime = the all-time max_risk_score. Shown when ClickHouse is offline, and always available on hover. Sorting is always on the lifetime score.

Two derived states surface on the badge:

  • Dormant (a moon icon) — no traffic in the current window. The badge dims and shows the lifetime max; the endpoint isn't currently active.
  • Improved (a green ↓) — the current score is below the all-time peak: the endpoint was remediated (or the attack stopped). It reads as fixed instead of stuck at its historical worst.

If the current-vs-ever overlay is unavailable (ClickHouse offline), the view falls back to lifetime max with a clear inline note — the lifetime max never self-lowers, so it's the safe fallback.

Security Score (A–F grade)​

The Security Score dashboard rolls the mix of threat and exposure signals across a surface into a single A–F posture grade, with the contributing factors called out. It's the number for a stakeholder summary and for tracking posture release-over-release. See Discovery Dashboards.

Flag classes​

Independently of the threat/exposure axis, each flag carries a class — the dimension it belongs to. The Risk dashboard breaks findings down by class:

ClassWhat it covers
authMissing, weak, or inconsistent credentials
attack_patternBehavioural abuse — brute force, enumeration, scanning, replay
transportConnection-layer hygiene — plain HTTP, weak TLS, missing HSTS, legacy protocol
data_leakSensitive-data exposure — PII observed, oversized responses
discoveryContextual surface signals — internal vs external host, sensitive path keywords
behaviorResponse-status signals — 4xx/5xx clustering, latency/error-rate anomalies
consistencyThe same endpoint behaving differently across events — usually misconfig

The full per-flag catalog, with class, severity, OWASP mapping, and remediation, is the Risk Flags Reference.

How to prioritize​

Read the two axes together as a 2×2:

Low exposureHigh exposure
Low threat✅ Clean🔵 Boring but open → harden
High threat🔴 Solid but under attack⛔ Open & under attack — top priority
  • High Threat + High Exposure first — open and actively attacked.
  • High Exposure, Low Threat — "boring but open": no attacker yet, but a standing weakness. Fix the hygiene (TLS, auth, headers) before it's found.
  • High Threat, Low Exposure — a solid endpoint under attack; the standing config is fine, so lean on rate limits / RBAC and watch it.

Sort the endpoints view by Threat to find what's under attack, by Exposure to find what to harden.

Resetting scores (admin)​

Because the catalog scores are monotonic, after a collector scoring change they can read stale-high — an endpoint that was fixed still shows its all-time-worst number. Admins/Owners have two levers:

  • Reset risk scores / rebaseline (project-wide, from the Listeners tab) — zeroes max_risk_score and max_posture_score on every endpoint; the collector re-accumulates the correct value from the next event. Nothing else is deleted (counters, flags, and discovery metadata are untouched). Low-traffic endpoints may briefly read 0 until their next request.
  • Reset counters & risk (single endpoint, from its detail page) — the same, scoped to one operation.

For an up-to-the-window answer without resetting, use the current posture panel on an endpoint's detail page.