Files
vulncheck/tests/test_report_scope.py
T
vulncheckandClaude Opus 5 1d1caf973b fix(reports): correct the numbers and rank what claims to be ranked
Every export queried the vulnerabilities table raw, so all four disagreed
with the dashboard they were exported from, in the same three ways:

- Findings on decommissioned and inactive assets were counted. Those hosts
  are deliberately hidden on screen, and the gap grows with every machine
  retired.
- EOL- and NESSUS-PLUGIN- pseudo-CVEs were counted as CVEs. They are real
  work items but they are not vulnerabilities, and they have their own
  surface in the UI.
- The executive summary counted severities across ALL statuses, so a
  remediated estate still reported hundreds of "Critical Severity" — the
  patched ones. A reader takes those as outstanding work. They are now
  explicitly labelled and scoped to open findings.

"Top Priority Risks" was the worst of it: a LIMIT 5 with no ORDER BY, so the
database returned any five critical rows it liked under a heading promising
the five that matter most. It now ranks by priority score, then CVSS.

Two exports could also take the server down on a large estate. Patching
Progress put every patched finding of the last 30 days in the table — one
reconcile here closed 14703 at once — and the ISO report loaded every
non-compliant finding into memory just to call len() on it, while printing
20. Both now count in the database and list a bounded, ordered page. The CSV
streams in batches instead of materialising the whole file first.

The scope rule lives in app/services/report_scope.py so the reports cannot
drift apart again, and so it can be tested without the web stack —
tests/test_report_scope.py asserts it against the emitted SQL.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 11:42:43 +02:00

56 lines
1.9 KiB
Python

"""Report scope + ranking — run: python tests/test_report_scope.py
Two things the reports got wrong silently, so both are pinned here:
1. Scope. They queried the table raw, counting findings on decommissioned
assets and counting EOL-/NESSUS-PLUGIN- pseudo-CVEs as CVEs — so the
exported numbers disagreed with the dashboard for the same estate.
2. "Top Priority Risks" had a LIMIT and no ORDER BY, so the five rows under
that heading were whichever five the database felt like returning.
Checked by reading the emitted SQL — no database needed.
"""
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from app.models.vulnerability import Vulnerability, VulnerabilitySeverity, VulnerabilityStatus
from app.services.report_scope import scoped as _scoped
def _sql(q):
return str(q.statement.compile(compile_kwargs={"literal_binds": True})).lower()
def demo():
from sqlalchemy.orm import Session
from sqlalchemy import create_engine
engine = create_engine("sqlite://")
db = Session(engine)
sql = _sql(_scoped(db))
# Inactive/decommissioned assets are excluded, orphans (no asset) kept.
assert "left outer join assets" in sql
assert "active" in sql
assert "assets.id is null" in sql
# Pseudo-CVEs excluded by prefix.
assert "eol-" in sql and "nessus-plugin-" in sql
# The ranked query really carries an ORDER BY — the bug was its absence.
top = (_scoped(db)
.filter(Vulnerability.severity == VulnerabilitySeverity.critical,
Vulnerability.status == VulnerabilityStatus.open)
.order_by(Vulnerability.priority_score.desc().nullslast())
.limit(5))
tsql = _sql(top)
assert "order by" in tsql and "priority_score desc" in tsql
assert "limit 5" in tsql
db.close()
print("report scope OK")
if __name__ == "__main__":
demo()