The scanner reports two independent numbers. Reading either one alone is a mistake, and the reports are designed so that you cannot do it by accident:
Risk: 92/100 how much risk the findings represent
Coverage: 61% how much of the audit actually ran
Confidence: medium derived from coverage
A high risk score with low coverage means "not examined", not "clean". Terminal and Markdown reports print an explicit warning whenever coverage is below 80%.
The risk score is a deterministic function of the findings: same findings → same score, on any machine, in any order. It is documented, reproducible, and severity-aware — and informational noise cannot distort it.
Each failed finding contributes by severity:
| Severity | Weight |
|---|---|
| critical | 40 |
| high | 20 |
| medium | 10 |
| low | 4 |
| info | 0 |
Within one severity group (ranked by weight, ties broken by check ID for stability), the first three findings count at full weight; every subsequent finding in that group counts at half weight (rounding up to stay integral). Rationale: a critical vulnerability should move the score hard, but fifty medium findings should not zero a site on their own.
overall = clamp(0, 100, 100 − Σ contributions)over all failed findingscategory = clamp(0, 100, 100 − Σ contributions of that category)
Passed, skipped, unknown, and suppressed findings contribute nothing. A suppressed finding stays in the report (with its reason) but stops moving the score and the exit policy — that is the difference between "accepted risk" and "deleted finding".
Some checks describe exposure or policy rather than a missing control, and
they must not debit a universal score. They fail at info severity (weight 0)
so they remain visible without moving the number, and they carry ImpContext
coverage weight:
| Check | Default severity | Why |
|---|---|---|
DEFAULT_DATABASE_PREFIX |
info | A prefix is not a control, and changing it on a live site is invasive. |
XMLRPC_ENABLED |
info | Legitimate on many sites; the real control is rate limiting. |
FILE_MODS_ALLOWED |
info | Correct for immutable/deployment-managed sites, wrong for others. |
FORCE_SSL_ADMIN_DISABLED |
info | Modern WordPress already forces HTTPS for admin when the site URL is HTTPS. |
REST_USER_ENUMERATION |
low | A verified exposure and a real footgun, but a public login name is not a broken control. |
Raise any of them with severity_overrides when your project treats them as
requirements:
severity_overrides:
XMLRPC_ENABLED: mediumFindings: 1× high (vulnerable plugin), 4× medium, 4× low.
- high: 20 → deduction 20
- medium: first three 10+10+10, fourth 10/2=5 → deduction 35
- low: first three 4+4+4, fourth 4/2=2 → deduction 14
overall = 100 − 69 = 31.
Coverage answers a different question: how much of the audit reached a
conclusion? It is the share of check weight whose status is passed or
failed; skipped and unknown count against it.
Each check carries an importance weight:
| Importance | Weight | Assigned to |
|---|---|---|
ImpCore |
3 | Integrity, vulnerability data, authentication controls, configuration secrets, exposure of backups/secrets, WordPress layout resolution. |
ImpStandard |
2 | The default: most posture checks. |
ImpContext |
1 | Advisory/policy checks (see the table above). |
A site whose vulnerability database was unavailable therefore loses more
coverage than one that could not read a server config file — which is the
point: a missing PLUGIN_VULNERABILITY is a blind spot, a missing
DIRECTORY_LISTING is a detail.
Confidence is derived from coverage:
| Coverage | Confidence |
|---|---|
| ≥ 90% | high |
| ≥ 70% | medium |
| < 70% | low |
--offline(release data, vulnerability lookups, checksum manifest, probes).- A missing WP-CLI and no
--live: update state, admin accounts, XML-RPC, PHP runtime facts. - No configured vulnerability provider: every vulnerability check.
--verify-plugin-checksumsnot passed: plugin file integrity.- A filesystem walk hitting its time/file budget (
coverage.walk_truncated). - A WordPress path constant that could not be resolved statically
(
CUSTOM_CONTENT_PATHreportsunknown, and the check appears as a gap).
Every gap is listed with its check ID, status, and the reason the check reported, so a low coverage score is always explainable:
"coverage": {
"score": 61,
"checks_determined": 26,
"checks_total": 42,
"confidence": "medium",
"gaps": [
{"check_id": "PLUGIN_VULNERABILITY", "status": "skipped",
"reason": "vulnerability data unavailable — configure a provider", "importance": 3}
],
"files_walked": 4213
}wpus scan /srv/site --fail-on-coverage-below 80or in configuration:
fail_on_coverage_below: 80This is the CI-grade guard: a pipeline cannot go green on a scan that did not actually inspect the site.
Every failed finding also debits its own category, so reports show where risk lives:
Core 100
Configuration 86
Authentication 100
Plugins 80
Themes 100
Filesystem 96
Server 100
Implementation: internal/scoring/scoring.go (risk weights, decay, clamping,
and order-independence are unit-tested; coverage weighting and confidence
boundaries are covered by the same package's tests).