Files
rengine/reports/analysis/scoring.py
T
Yogesh OjhaandClaude Fable 5.1 0a69f3201c copy: state the fact, delete the explanation
Repo-wide sweep of every user-facing string: frontend components, routes,
config and stores; API details and route docs; shared definitions and
models; stage config help; report prose; MCP, toolbox, interest and
connector text; notification and worker messages.

Descriptions that explained, reassured, justified or restated their title
are deleted. Confirm dialogs name what is removed and stop. Failed actions
read "<Noun> not <verbed>", successes "<Noun> <verbed>". No reader address,
no product-as-subject, no em-dashes or parentheticals in prose.

Mirrors updated on both sides: EPSS band help, compare refusal reasons,
interest labels. Four tests asserting on old wording follow the new text.
The rules are condensed into CLAUDE.md under Writing.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SK5o8xR96cbSnAfjYSTPSe
2026-09-12 11:35:07 +05:30

117 lines
3.5 KiB
Python

"""Posture and risk scores."""
from __future__ import annotations
import math
from reports.analysis.brief import Posture
from reports.data.models import Issue
from shared.definitions.compliance import cwe_top_25_rank
from shared.definitions.vulnerabilities import EPSS_HIGH, Severity
_SEVERITY_WEIGHT = {
Severity.CRITICAL.value: 100.0,
Severity.HIGH.value: 60.0,
Severity.MEDIUM.value: 25.0,
Severity.LOW.value: 8.0,
Severity.INFO.value: 1.0,
Severity.UNKNOWN.value: 1.0,
}
# each rule is (label, per-item cost, ceiling)
_RULES = {
"critical": ("Critical findings", 12.0, 36.0),
"kev": ("Known exploited weaknesses", 10.0, 20.0),
"high": ("High findings", 6.0, 24.0),
"takeover": ("Subdomain takeover candidates", 9.0, 18.0),
"origin": ("Origins reachable outside the CDN", 7.0, 14.0),
"sensitive": ("Sensitive services exposed", 2.0, 12.0),
"expired": ("Expired certificates", 3.0, 9.0),
"medium": ("Medium findings", 1.2, 10.0),
"default_login": ("Default credentials", 10.0, 20.0),
}
_GRADES = ((90, "A"), (80, "B"), (70, "C"), (60, "D"), (45, "E"))
def grade_for(score: int) -> str:
for floor, letter in _GRADES:
if score >= floor:
return letter
return "F"
def posture(observations: dict[str, int]) -> Posture:
total = 100.0
deductions: list[dict] = []
for key, count in observations.items():
rule = _RULES.get(key)
if not rule or not count:
continue
label, cost, ceiling = rule
loss = min(count * cost, ceiling)
total -= loss
deductions.append({"label": label, "count": count, "points": round(loss, 1)})
deductions.sort(key=lambda d: d["points"], reverse=True)
score = max(0, min(100, round(total)))
return Posture(score=score, grade=grade_for(score), deductions=deductions)
def issue_risk(issue: Issue) -> tuple[float, list[str]]:
"""Risk score and ranking signals for one issue."""
score = _SEVERITY_WEIGHT.get(issue.severity, 1.0)
signals: list[str] = []
if issue.is_kev:
score *= 2.0
signals.append("Known exploited")
if issue.epss_score:
score *= 1 + issue.epss_score
if issue.epss_score >= EPSS_HIGH:
signals.append(f"EPSS {issue.epss_score * 100:.0f}%")
if issue.cvss_score:
score *= 0.7 + issue.cvss_score / 10
hosts = max(1, len(issue.hosts))
score *= 1 + math.log10(hosts)
if hosts > 1:
signals.append(f"On {hosts} assets")
new = issue.new_count
if new:
score *= 1.15
signals.append(
"New since the previous run" if new == issue.count else f"{new} new"
)
rank = cwe_top_25_rank(issue.cwe_ids)
if rank:
score *= 1.1
signals.append(f"CWE Top 25 rank {rank}")
if issue.cve_ids:
signals.append(
issue.cve_ids[0]
if len(issue.cve_ids) == 1
else f"{len(issue.cve_ids)} CVEs"
)
return (round(score, 2), signals)
_EFFORT_RULES = (
({"default-login", "weak-password"}, "Credential change"),
({"config", "misconfig", "exposure", "files", "backup", "debug"}, "Configuration"),
({"cve", "outdated", "tech"}, "Patch or upgrade"),
({"takeover"}, "DNS change"),
({"ssl", "tls"}, "Certificate change"),
({"sqli", "xss", "rce", "ssti", "lfi", "ssrf", "injection"}, "Code change"),
)
def effort_for(tags: list[str]) -> str:
lowered = {t.lower() for t in tags}
for keys, label in _EFFORT_RULES:
if lowered & keys:
return label
return "Investigate"