mirror of
https://github.com/yogeshojha/rengine.git
synced 2026-10-20 07:11:28 +02:00
A full static review of the 3.0 tree, run as two fan-out passes with every
finding verified against the running stack rather than taken on inspection.
Correctness and security
- instance settings encrypted the mask over the stored AI key: `!= MASK` never
matched `••••••••abcd`, so saving settings destroyed the key and `test_ai`
posted the mask as a bearer token
- the MFA token was replayable for its full five-minute life; the gate now
checks revocation and consumes the token after a successful exchange
- logout revoked nothing for a bearer client while answering "Logged out"
- `POST /api-keys/{id}/test` returned the plaintext key inside httpx's error
string, which embeds the whole URL
- an interest rule could be edited or deleted through another project's id
- redis ran the celery broker under `allkeys-lru`, free to evict a queued task
or a chord counter; now `noeviction`
- nuclei's stored command carried the OAST token and every auth header
- a finalize step could poison the session and leave a scan RUNNING for ever
- `is:issue` raised TypeError on every search
- offset paging repeated and skipped rows in four dimensions
- a cancelled run kept a stale `scans.*_found` rollup
- every PDF report raised `KeyError: 'software'`
- `version_key` truncated a wide numeric run and inverted release ordering
- psycopg raises a NUL as a bare ValueError, above StatementError, so every
savepoint guard was blind to the case it was written for
Performance
- endpoints.host had no trigram index: 35.9ms seq scan -> 0.389ms
- free text compiled to one OR, not the prescribed UNION ALL: 132ms -> 41ms
- `/http-assets/summary` and `/ips/summary` materialised whole tables
- the cross-link `ip` kind ran an estate query per page and drew nothing
Frontend
- `$effect` loaders that read the state they write, an unbounded request loop
on a failing endpoint
- error states that rendered as empty states
- 2FA backup codes were unusable while the copy said they worked
- a "Scan history" dialog that always reported none
- ~400 duplicated lines across five filter bars, now one component
Build and tooling
- every scanner installed at @latest; all 13 pinned to the measured versions
- CI built `web/`, gone since 2.x, with tags that cannot be valid
- the prettier pre-commit hook discarded its filenames and always passed
- the test suite destroyed itself when run twice
Also: subdomains from public code, via a pinned github-subdomains source and a
GitHub API key that subfinder receives too; sublist3r and oneforall removed
after measuring one unique host in 89 across four targets.
Verified: 853 tests, ruff clean over eleven packages, svelte-check 0 errors,
the count-is-a-promise gate at 131 checks and 0 mismatches, and 143 GET routes
with no 5xx. Four migrations, each with an exercised downgrade.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DVkdx5ynZ6aof5kLFczmhX
422 lines
14 KiB
Python
422 lines
14 KiB
Python
"""Turn a source into a brief."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from reports.analysis.brief import (
|
|
Action,
|
|
Caveat,
|
|
ChangeLine,
|
|
Concentration,
|
|
Highlight,
|
|
ReportBrief,
|
|
)
|
|
from reports.analysis.narratives import attack_paths
|
|
from reports.analysis.scoring import effort_for, issue_risk, posture
|
|
from reports.data.models import Issue
|
|
from reports.data.source import REPORT_DIMENSIONS, ReportSource
|
|
from shared.definitions.compliance import (
|
|
FRAMEWORK_BY_KEY,
|
|
SURFACE_CONTROLS,
|
|
cwe_top_25_rank,
|
|
map_finding,
|
|
)
|
|
from shared.definitions.ports import ServiceClass
|
|
from shared.definitions.surface import (
|
|
SURFACE_LABELS,
|
|
SurfaceDimension,
|
|
)
|
|
from shared.definitions.vulnerabilities import (
|
|
SEVERITY_LABELS,
|
|
SEVERITY_ORDER,
|
|
Severity,
|
|
severity_rank,
|
|
)
|
|
|
|
_DIM = SurfaceDimension
|
|
_MAX_ACTIONS = 12
|
|
_MAX_RISKS = 25
|
|
|
|
|
|
def build_issues(source: ReportSource) -> list[Issue]:
|
|
grouped: dict[str, Issue] = {}
|
|
for finding in source.findings:
|
|
issue = grouped.get(finding.template_id)
|
|
if issue is None:
|
|
issue = Issue(
|
|
template_id=finding.template_id,
|
|
name=finding.name,
|
|
severity=finding.severity,
|
|
scanner=finding.scanner,
|
|
protocol=finding.protocol,
|
|
description=finding.description,
|
|
impact=finding.impact,
|
|
remediation=finding.remediation,
|
|
references=list(finding.references),
|
|
tags=list(finding.tags),
|
|
cve_ids=list(finding.cve_ids),
|
|
cwe_ids=list(finding.cwe_ids),
|
|
cvss_score=finding.cvss_score,
|
|
epss_score=finding.epss_score,
|
|
is_kev=finding.is_kev,
|
|
)
|
|
issue.controls = map_finding(finding.cwe_ids, finding.tags)
|
|
issue.cwe_rank = cwe_top_25_rank(finding.cwe_ids)
|
|
grouped[finding.template_id] = issue
|
|
issue.findings.append(finding)
|
|
if severity_rank(finding.severity) < severity_rank(issue.severity):
|
|
issue.severity = finding.severity
|
|
|
|
for issue in grouped.values():
|
|
issue.risk = issue_risk(issue)[0]
|
|
return sorted(grouped.values(), key=lambda i: (-i.risk, severity_rank(i.severity)))
|
|
|
|
|
|
def _observations(source: ReportSource, issues: list[Issue], paths) -> dict[str, int]:
|
|
keyed = {p.key: p for p in paths}
|
|
severity = source.severity_counts
|
|
live = {h.name for h in source.host_rows if h.status}
|
|
return {
|
|
"critical": severity.get(Severity.CRITICAL.value, 0),
|
|
"high": severity.get(Severity.HIGH.value, 0),
|
|
"medium": severity.get(Severity.MEDIUM.value, 0),
|
|
"kev": sum(1 for i in issues if i.is_kev),
|
|
"takeover": keyed["takeover"].count if "takeover" in keyed else 0,
|
|
"origin": keyed["origin_exposed"].count if "origin_exposed" in keyed else 0,
|
|
"default_login": keyed["default_credentials"].count
|
|
if "default_credentials" in keyed
|
|
else 0,
|
|
"sensitive": len([s for s in source.sensitive_services if not s.is_http]),
|
|
"expired": len(
|
|
[
|
|
c
|
|
for c in source.certificates
|
|
if c.expired and (not live or c.host in live)
|
|
]
|
|
),
|
|
}
|
|
|
|
|
|
def _actions(issues: list[Issue]) -> list[Action]:
|
|
out: list[Action] = []
|
|
for issue in issues:
|
|
if issue.severity in {Severity.INFO.value, Severity.UNKNOWN.value}:
|
|
continue
|
|
controls = issue.controls.get("owasp_top10", [])
|
|
remediation = (issue.remediation or "").strip()
|
|
detail = (
|
|
remediation.split("\n")[0][:300]
|
|
if remediation
|
|
else (
|
|
f"Review every affected asset and remove the condition {issue.name} reports."
|
|
)
|
|
)
|
|
out.append(
|
|
Action(
|
|
title=issue.name,
|
|
detail=detail,
|
|
severity=issue.severity,
|
|
effort=effort_for(issue.tags),
|
|
clears=issue.count,
|
|
assets=len(issue.hosts),
|
|
templates=[issue.template_id],
|
|
controls=controls,
|
|
)
|
|
)
|
|
if len(out) >= _MAX_ACTIONS:
|
|
break
|
|
return out
|
|
|
|
|
|
def _changes(source: ReportSource) -> list[ChangeLine]:
|
|
if source.previous_scan is None:
|
|
return []
|
|
lines: list[ChangeLine] = []
|
|
for dimension in REPORT_DIMENSIONS:
|
|
if not source.coverage[dimension].covered:
|
|
continue
|
|
added, gone, added_total, gone_total = source.added_and_gone(
|
|
dimension, limit=12
|
|
)
|
|
if not added_total and not gone_total:
|
|
continue
|
|
lines.append(
|
|
ChangeLine(
|
|
dimension=dimension,
|
|
added=added_total,
|
|
gone=gone_total,
|
|
baseline=source.has_baseline(dimension),
|
|
added_sample=added,
|
|
gone_sample=gone,
|
|
)
|
|
)
|
|
return lines
|
|
|
|
|
|
def _concentration(source: ReportSource, issues: list[Issue]) -> list[Concentration]:
|
|
del issues
|
|
rows = sorted(
|
|
((host, data) for host, data in source.findings_by_host.items()),
|
|
key=lambda item: (-item[1][0], severity_rank(item[1][1])),
|
|
)[:8]
|
|
return [
|
|
Concentration(
|
|
label=host,
|
|
count=count,
|
|
worst=SEVERITY_LABELS.get(worst, "Unknown"),
|
|
note="",
|
|
)
|
|
for host, (count, worst) in rows
|
|
]
|
|
|
|
|
|
def _caveats(source: ReportSource) -> list[Caveat]:
|
|
out: list[Caveat] = []
|
|
for dimension in REPORT_DIMENSIONS:
|
|
entry = source.coverage[dimension]
|
|
if not entry.covered:
|
|
out.append(
|
|
Caveat(
|
|
kind="not_scanned",
|
|
text=(f"{SURFACE_LABELS[dimension]} were not scanned."),
|
|
)
|
|
)
|
|
for row in source.coverage_rows:
|
|
if row.status == "partial":
|
|
out.append(
|
|
Caveat(
|
|
kind="partial",
|
|
text=(
|
|
f"The {row.group} scan covered "
|
|
f"{row.hosts_scanned or 0} of {row.hosts_total} hosts."
|
|
),
|
|
)
|
|
)
|
|
elif row.status == "failed":
|
|
out.append(
|
|
Caveat(
|
|
kind="failed",
|
|
text=f"The {row.group} scan failed: {row.error or 'no error recorded'}.",
|
|
)
|
|
)
|
|
if row.hosts_dropped:
|
|
out.append(
|
|
Caveat(
|
|
kind="dropped",
|
|
text=f"{len(row.hosts_dropped)} hosts exceeded the scanner budget and were not checked.",
|
|
)
|
|
)
|
|
if source.scan is not None and source.scan.status == "cancelled":
|
|
out.append(
|
|
Caveat(
|
|
kind="cancelled",
|
|
text="The run was cancelled. Results are those written before it stopped.",
|
|
)
|
|
)
|
|
if source.suppressed_count:
|
|
out.append(
|
|
Caveat(
|
|
kind="suppressed",
|
|
text=f"{source.suppressed_count} findings marked accepted or false positive are excluded.",
|
|
)
|
|
)
|
|
excluded = source.excluded()
|
|
if any(excluded.values()):
|
|
parts = [f"{len(v)} {k}" for k, v in excluded.items() if v]
|
|
out.append(Caveat(kind="scope", text=f"The scope excluded {', '.join(parts)}."))
|
|
seen: set[str] = set()
|
|
return [c for c in out if not (c.text in seen or seen.add(c.text))]
|
|
|
|
|
|
def _compliance(issues: list[Issue], covered: frozenset[str]) -> dict:
|
|
out: dict[str, dict] = {}
|
|
for key, framework in FRAMEWORK_BY_KEY.items():
|
|
counts: dict[str, int] = {}
|
|
for issue in issues:
|
|
for control in issue.controls.get(key, []):
|
|
counts[control] = counts.get(control, 0) + issue.count
|
|
surface = SURFACE_CONTROLS.get(key, ()) if covered else ()
|
|
if not counts and not surface:
|
|
continue
|
|
out[key] = {
|
|
"name": framework.name,
|
|
"version": framework.version,
|
|
"counts": counts,
|
|
"evidence_only": list(surface),
|
|
}
|
|
return out
|
|
|
|
|
|
def _highlights(source: ReportSource, brief: ReportBrief) -> list[Highlight]:
|
|
out: list[Highlight] = []
|
|
for dimension in REPORT_DIMENSIONS:
|
|
entry = source.coverage[dimension]
|
|
if not entry.covered:
|
|
out.append(
|
|
Highlight(
|
|
key=dimension,
|
|
label=SURFACE_LABELS[dimension],
|
|
value="Not scanned",
|
|
detail="",
|
|
tone="absent",
|
|
)
|
|
)
|
|
continue
|
|
detail = ""
|
|
if entry.previous is not None:
|
|
delta = entry.count - entry.previous
|
|
if delta > 0:
|
|
detail = f"{delta} more than the previous run"
|
|
elif delta < 0:
|
|
detail = f"{abs(delta)} fewer than the previous run"
|
|
else:
|
|
detail = "unchanged"
|
|
out.append(
|
|
Highlight(
|
|
key=dimension,
|
|
label=SURFACE_LABELS[dimension],
|
|
value=f"{entry.count:,}",
|
|
detail=detail,
|
|
tone="neutral",
|
|
)
|
|
)
|
|
del brief
|
|
return out
|
|
|
|
|
|
def _headline(brief: ReportBrief) -> str:
|
|
severity = brief.severity
|
|
if brief.kev_count:
|
|
return (
|
|
f"{brief.kev_count} known exploited weakness"
|
|
f"{'es' if brief.kev_count != 1 else ''} are present."
|
|
)
|
|
if severity.get(Severity.CRITICAL.value):
|
|
count = severity[Severity.CRITICAL.value]
|
|
return f"{count} critical finding{'s' if count != 1 else ''} were identified."
|
|
if severity.get(Severity.HIGH.value):
|
|
count = severity[Severity.HIGH.value]
|
|
return (
|
|
f"{count} high severity finding{'s' if count != 1 else ''} were identified."
|
|
)
|
|
if brief.paths:
|
|
return brief.paths[0].title + "."
|
|
if brief.counts.get(
|
|
_DIM.VULNERABILITIES.value
|
|
) == 0 and _DIM.VULNERABILITIES.value in {
|
|
c["dimension"] for c in brief.coverage if c["covered"]
|
|
}:
|
|
return "No actionable weaknesses were identified in the checks that ran."
|
|
return f"{brief.counts.get(_DIM.WEB_ASSETS.value, 0):,} web assets were catalogued."
|
|
|
|
|
|
def build_brief(source: ReportSource) -> ReportBrief:
|
|
issues = build_issues(source)
|
|
paths = attack_paths(source, issues)
|
|
|
|
counts = {d: source.count_of(d) for d in REPORT_DIMENSIONS}
|
|
coverage = [
|
|
{
|
|
"dimension": d,
|
|
"label": SURFACE_LABELS[d],
|
|
"covered": source.coverage[d].covered,
|
|
"count": source.coverage[d].count,
|
|
"previous": source.coverage[d].previous,
|
|
}
|
|
for d in REPORT_DIMENSIONS
|
|
]
|
|
|
|
brief = ReportBrief(
|
|
subject=source.subject,
|
|
subject_type=source.subject_type,
|
|
scope=source.scope,
|
|
observed_at=source.observed_at,
|
|
engine=source.scan.engine_name if source.scan else "",
|
|
duration_seconds=(
|
|
(source.scan.completed_at - source.scan.started_at).total_seconds()
|
|
if source.scan and source.scan.completed_at and source.scan.started_at
|
|
else None
|
|
),
|
|
counts=counts,
|
|
coverage=coverage,
|
|
severity={k: v for k, v in source.severity_counts.items() if v},
|
|
paths=paths,
|
|
suppressed=source.suppressed_count,
|
|
first_run=source.previous_scan is None,
|
|
)
|
|
|
|
brief.posture = posture(_observations(source, issues, paths))
|
|
brief.risks = []
|
|
for issue in issues[:_MAX_RISKS]:
|
|
score, signals = issue_risk(issue)
|
|
brief.risks.append(_risk_item(issue, score, signals))
|
|
brief.actions = _actions(issues)
|
|
brief.caveats = _caveats(source)
|
|
brief.changes = _changes(source)
|
|
brief.concentration = _concentration(source, issues)
|
|
brief.compliance = _compliance(issues, source.covered_dimensions)
|
|
brief.exposure = _exposure(source)
|
|
brief.hosting = _hosting(source)
|
|
brief.highlights = _highlights(source, brief)
|
|
brief.headline = _headline(brief)
|
|
return brief
|
|
|
|
|
|
def _risk_item(issue: Issue, score: float, signals: list[str]):
|
|
from reports.analysis.brief import RiskItem # noqa: PLC0415
|
|
|
|
return RiskItem(
|
|
template_id=issue.template_id,
|
|
name=issue.name,
|
|
severity=issue.severity,
|
|
count=issue.count,
|
|
hosts=len(issue.hosts),
|
|
score=score,
|
|
signals=signals,
|
|
kev=issue.is_kev,
|
|
epss=issue.epss_score,
|
|
cvss=issue.cvss_score,
|
|
cves=list(issue.cve_ids),
|
|
sample=issue.findings[0].matched_at if issue.findings else "",
|
|
new=issue.new_count,
|
|
)
|
|
|
|
|
|
def _exposure(source: ReportSource) -> dict:
|
|
services = source.service_rows
|
|
if not services:
|
|
return {}
|
|
web = sum(1 for s in services if s.is_http)
|
|
classes = {f.name: f.count for f in source.service_classes}
|
|
return {
|
|
"total": len(services),
|
|
"web": web,
|
|
"non_web": len(services) - web,
|
|
"sensitive": len(source.sensitive_services),
|
|
"classes": classes,
|
|
"addresses": len({s.ip for s in services}),
|
|
"top": [f.name for f in source.top_services[:6]],
|
|
"database": classes.get(ServiceClass.DATABASE.value, 0),
|
|
"remote": classes.get(ServiceClass.REMOTE.value, 0),
|
|
"internal": source.internal_estate,
|
|
}
|
|
|
|
|
|
def _hosting(source: ReportSource) -> dict:
|
|
edge, cloud, direct = source.cdn_split
|
|
hosts = source.host_rows
|
|
resolving = sum(1 for h in hosts if h.ips or h.status)
|
|
return {
|
|
"hosts": len(hosts),
|
|
"resolving": resolving,
|
|
"live": sum(1 for h in hosts if h.status),
|
|
"edge": edge,
|
|
"cloud": cloud,
|
|
"direct": direct,
|
|
"networks": [f.name for f in source.networks[:5]],
|
|
"countries": [f.name for f in source.countries[:5]],
|
|
"waf": sum(1 for h in hosts if h.waf),
|
|
}
|
|
|
|
|
|
SEVERITY_SEQUENCE = SEVERITY_ORDER
|