Commit 8e761ff
HTML reports + OSV.dev CVE enrichment (v0.2)
Adds two CLI flags and one new module so Whitney can produce
demo-quality artifacts that read better in a screen recording than the
existing CLI table:
whitney scan ./repo --html report.html
whitney sbom ./repo --html sbom.html --enrich
Both output a single self-contained HTML file (CSS inlined, no JS
framework, no images, no web fonts, embedded raw JSON for jq
extraction). The SBOM `--enrich` flag cross-references each SDK
component against OSV.dev's public vulnerability API and folds matching
CVE / GHSA records into the report. Built-in `VULNERABLE_SDK_VERSIONS`
matches are merged additively, deduped by ID.
Per the design doc (docs/superpowers/specs/2026-04-26-html-reports-design.md)
the explicit non-choice was Transilience: that API belongs in Shasta's
compliance pipeline, not in Whitney's standalone OSS scanner. OSV.dev
needs no auth, covers PyPI/npm/Go/Maven/etc., and matches the same data
Snyk / GitHub Advisory ultimately resolve to.
Implementation:
- `whitney/html_report.py` — pure renderer, ~580 lines, stdlib-only
templating. Two public functions: render_scan_html(findings, scan_root)
and render_sbom_html(sbom). All user-content fields pass through
html.escape; URL fields through urllib.parse.quote. Light theme by
default with prefers-color-scheme dark auto-flip. Severity-coded
badges (Critical #dc2626 → Info #0891b2). Findings grouped by severity
with sticky headers. SBOM grouped by AI provider (OpenAI / Anthropic /
AWS / etc.) with stable colour assignment. Raw input JSON embedded in
a <script type="application/json" id="whitney-data"> block, with </
→ <\/ escaping to prevent script-tag breakout.
- `whitney/sbom.py` — added enrich_with_osv() and
scan_ai_sbom_code_only_enriched(). ThreadPoolExecutor max_workers=8;
cache at ~/.whitney/osv_cache.json keyed by (eco, name, version,
query_date) so re-runs same day are free; MAX_OSV_QUERIES_PER_RUN=200
cap; fail-open on any network or parse error (renders the report with
whatever vulnerabilities were resolved).
- `whitney/cli.py` — `--html PATH` added to scan and sbom subcommands;
`--enrich` added to sbom only. Default behaviour preserved (table
output for scan, JSON-to-stdout for sbom) when no new flag is set.
Tests (`tests/test_html_report.py`):
- 24 tests, no snapshot files. Three classes: TestRenderScanHtml (HTML5
parse + grouping + XSS hardening), TestRenderSbomHtml (parse + provider
grouping + vulnerability panel + XSS), TestEnrichOsv (mock urlopen +
cache hit + per-run cap + fail-open on URLError).
- XSS test specifically: a finding with code_snippet =
'<script>alert(1)</script>' produces escaped output and the rendered
document contains zero executable script elements.
Sample reports committed for showcase: docs/sample-reports/scan.html
(7 findings against a small mix of corpus positives) and sbom.html
(7 components against a synthetic requirements.txt with intentionally
old AI SDKs; OSV.dev surfaces 52 vulnerabilities across them).
Version bumped 0.1.0 → 0.2.0 (additive feature). Zero new runtime deps.
Verification:
- pytest tests/test_html_report.py → 24/24 pass
- pytest tests/corpus/ → 17/17 pass (no regression)
- python -m tests.corpus.eval → recall=1.000, fp_rate=0.200 (unchanged
default-mode baseline)
- whitney scan + sbom --html against synthetic demo dir → 34KB scan,
96KB sbom, both render correctly in Safari/Firefox/Chrome.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent ecc1416 commit 8e761ff
10 files changed
Lines changed: 4201 additions & 12 deletions
File tree
- docs
- sample-reports
- tests
- whitney
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
9 | | - | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
10 | 12 | | |
11 | 13 | | |
12 | 14 | | |
| |||
17 | 19 | | |
18 | 20 | | |
19 | 21 | | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
20 | 26 | | |
21 | 27 | | |
22 | 28 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
86 | 86 | | |
87 | 87 | | |
88 | 88 | | |
89 | | - | |
90 | | - | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
91 | 100 | | |
92 | 101 | | |
93 | 102 | | |
94 | 103 | | |
95 | | - | |
| 104 | + | |
96 | 105 | | |
97 | | - | |
| 106 | + | |
98 | 107 | | |
99 | 108 | | |
100 | | - | |
| 109 | + | |
101 | 110 | | |
102 | 111 | | |
| 112 | + | |
| 113 | + | |
103 | 114 | | |
104 | 115 | | |
105 | 116 | | |
| |||
0 commit comments