From 4d493af61f21def9a61b1ab031a7600b4d519ae0 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 11 Oct 2026 21:58:43 +0000 Subject: [PATCH 1/3] feat(report): collapse advisory corpus provenance in the HTML footer vg report --format html left #advisory-corpus empty, so vulnerability database provenance never appeared. The footer now records extended.vulnerabilities source, advisory and package counts, and severity counts after the finding tables, collapsed so those tables stay first. An unreachable source does not print zero counts, and the same artifact still produces the same HTML. Signed-off-by: Cursor Agent Co-authored-by: vibgrate-team --- DOCS.md | 5 +- README.md | 2 +- src/reporting/formatters/html.test.ts | 119 +++++++++++++++++++++++++- src/reporting/formatters/html.ts | 57 +++++++++--- 4 files changed, 167 insertions(+), 16 deletions(-) diff --git a/DOCS.md b/DOCS.md index def4288b..4a04432f 100644 --- a/DOCS.md +++ b/DOCS.md @@ -501,6 +501,7 @@ external files. The page opens with a summary: - Counts by severity for vulnerability findings and for security findings. A check that did not run stays "Not scanned" rather than a row of zeros. A check that finished with nothing to report shows zeros. - When a finding carries `details.fixedVersions`, how many of those findings name a fix. - Links to the full tables under the summary: vulnerabilities, security findings, and the other findings. +- Advisory-database provenance is a collapsed footer after those tables (`#advisory-corpus`), not a banner above the findings. It reads `extended.vulnerabilities`: `source` (`osv`, `manifest`, `unreachable`, or `none`), `totalAdvisories`, how many `packages` are listed, and `severityCounts` (critical, high, moderate, low, unknown). The scan artifact does not store a separate corpus version; `source` is the recorded database (online OSV, or an offline package-version manifest). A missing vulnerability block says "Not scanned". `source: "unreachable"` does not print zero counts. The same artifact always produces the same page. Save it with a shell redirect: @@ -596,7 +597,7 @@ When set: ##### HTML (`formatHtmlReport`) -`vg report --format html` writes one self-contained page from the same artifact. It does not call the network and does not load external files. The page opens with the summary listed above (DriftScore, severity counts, a fix-available count when a finding carries `details.fixedVersions`, and links to the tables), then the full vulnerability, security, and other-finding tables. A check that did not run stays "Not scanned". A missing score is `n/a`. The same artifact always produces the same page. +`vg report --format html` writes one self-contained page from the same artifact. It does not call the network and does not load external files. The page opens with the summary listed above (DriftScore, severity counts, a fix-available count when a finding carries `details.fixedVersions`, and links to the tables), then the full vulnerability, security, and other-finding tables. Advisory-database provenance is the collapsed `#advisory-corpus` footer after those tables: `extended.vulnerabilities.source`, the advisory count, the affected-package count, and severity counts. A check that did not run stays "Not scanned". A missing score is `n/a`. The same artifact always produces the same page. ##### Differences @@ -609,7 +610,7 @@ When set: - DriftScore: Markdown puts the lower-is-better gloss in the opening cell. Text puts the score in **DriftScore Summary** without that gloss. - VCS: Markdown uses separate VCS, Branch, and Commit rows, and Commit requires `vcs.sha`. Text uses one line and can show `shortSha` without requiring `sha`. - Runtime wording: Markdown always says `major(s)`. Text says `major` or `majors`. -- HTML is the browser page: the summary above, then the vulnerability, security, and other-finding tables. It does not print Markdown's recommended-standards section or the text inventory. +- HTML is the browser page: the summary above, then the vulnerability, security, and other-finding tables, then the collapsed `#advisory-corpus` footer for advisory-database provenance. It does not print Markdown's recommended-standards section or the text inventory. ##### JSON diff --git a/README.md b/README.md index d4893a3b..811d6366 100644 --- a/README.md +++ b/README.md @@ -829,7 +829,7 @@ All HCS computation runs in an optional, separately-licensed engine module that | `vg evidence` | Signed, reproducible regulatory evidence — jurisdiction-neutral regimes (EU CRA first, DORA incident reporting too): `init`, `product`, `release`, `exposure`, `readiness`, `drill`, `watch`, `pack`, `verify`, `push`, `export` | | `vg fix` | Ranked, risk-tiered upgrade plans from the hosted planner — then apply the one you choose | | `vg init [path]` | Initialise config and `.vibgrate/` | -| `vg report` | Render a saved scan artifact as text (default), Markdown, or JSON. There is no HTML report. Field coverage is in [DOCS.md](./DOCS.md#report-format-coverage) | +| `vg report` | Render a saved scan artifact as text (default), Markdown, JSON, or HTML. HTML leads with the summary and finding tables; advisory-database provenance is the collapsed `#advisory-corpus` footer. Field coverage is in [DOCS.md](./DOCS.md#report-format-coverage) | | `vg review` | **Vibgrate Review** — architecture + security-control review of the current change, locally (`--in-place`, `--local`, `--loop`). Deterministic blast-radius findings from the code graph via `vg review findings-from-diff`; `vg review propose ` attaches a PatchIR dry-run (same `--base` / `--in-place` / `--diff`, `--findings`, or `.vibgrate/review-propose-handoff.json`). One decision (`pass` / `needs_review` / `fail` / `undetermined`) in a signed receipt (Ed25519 over the receipt digest; `vg review verify ` checks it offline); protected findings cannot be blessed into a pass. Reports change integrity, not a proof of security. Builds or refreshes the code map itself when it is missing or stale (`--no-auto-build` opts out) | | `vg sbom export` / `delta` / `vex` | Export CycloneDX/SPDX SBOM, diff two artifacts, or emit an OpenVEX document | | `vg scan [path]` | Scan for upgrade drift | diff --git a/src/reporting/formatters/html.test.ts b/src/reporting/formatters/html.test.ts index 96ab9deb..5ea86c92 100644 --- a/src/reporting/formatters/html.test.ts +++ b/src/reporting/formatters/html.test.ts @@ -48,6 +48,14 @@ function section(html: string, id: string): string { return html.slice(start, end); } +function footer(html: string): string { + const start = html.indexOf(`', start); + if (end < 0) throw new Error('unclosed advisory corpus footer'); + return html.slice(start, end + ''.length); +} + function sample(): ScanArtifact { return artifact({ drift: { @@ -174,10 +182,12 @@ describe('formatHtmlReport', () => { expect(html).toContain('href="#vulnerabilities"'); expect(html).toContain('href="#security"'); expect(html).toContain('href="#findings"'); + expect(html).toContain(`href="#${ADVISORY_CORPUS_FOOTER_ID}"`); expect(html).toContain('id="summary"'); expect(html).toContain('id="vulnerabilities"'); expect(html).toContain('id="security"'); expect(html).toContain('id="findings"'); + expect(html).toContain(`id="${ADVISORY_CORPUS_FOOTER_ID}"`); }); it('keeps the full finding tables, in stable order, under the summary', () => { @@ -209,6 +219,11 @@ describe('formatHtmlReport', () => { expect(html).not.toContain('engine/1'); expect(findings).not.toContain('GHSA-crit'); expect(html.indexOf('id="summary"')).toBeLessThan(html.indexOf('id="vulnerabilities"')); + expect(html.indexOf('id="vulnerabilities"')).toBeLessThan(html.indexOf('id="security"')); + expect(html.indexOf('id="security"')).toBeLessThan(html.indexOf('id="findings"')); + expect(html.lastIndexOf('')).toBeLessThan(html.indexOf(`id="${ADVISORY_CORPUS_FOOTER_ID}"`)); + expect(footer(html)).toContain('Not scanned'); + expect(footer(html)).not.toContain('GHSA-crit'); }); it('escapes markup and redacts credential-shaped text', () => { @@ -230,6 +245,9 @@ describe('formatHtmlReport', () => { expect(section(html, 'vulnerabilities')).toContain('Not scanned'); expect(section(html, 'security')).toContain('Not scanned'); expect(section(html, 'findings')).toContain('None'); + expect(footer(html)).toContain('

Not scanned

'); + expect(footer(html)).not.toContain('
Advisories
'); + expect(footer(html)).not.toContain('critical'); }); it('renders real zeros when a vulnerability check finished with nothing to report', () => { @@ -246,6 +264,12 @@ describe('formatHtmlReport', () => { expect(block(html, 'vuln-counts')).toContain('data-evaluated="yes"'); expect(block(html, 'vuln-counts')).toContain('critical 0'); expect(section(html, 'vulnerabilities')).toContain('None'); + const foot = footer(html); + expect(foot).toContain('
Source
none
'); + expect(foot).toContain('
Advisories
0
'); + expect(foot).toContain('
Packages
0
'); + expect(foot).toContain('
Severity
critical 0, high 0, moderate 0, low 0, unknown 0
'); + expect(html.lastIndexOf('')).toBeLessThan(html.indexOf(`id="${ADVISORY_CORPUS_FOOTER_ID}"`)); }); it('leaves an unfinished vulnerability check blank instead of zero', () => { @@ -261,6 +285,12 @@ describe('formatHtmlReport', () => { })); expect(block(html, 'vuln-counts')).toContain('Not scanned'); expect(html).not.toContain('critical 0'); + const foot = footer(html); + expect(foot).toContain('
Source
unreachable
'); + expect(foot).not.toContain('
Advisories
'); + expect(foot).not.toContain('
Packages
'); + expect(foot).not.toContain('
Severity
'); + expect(html.lastIndexOf('')).toBeLessThan(html.indexOf(`id="${ADVISORY_CORPUS_FOOTER_ID}"`)); }); it('shows a zero fix rollup when the field is present and no version is listed', () => { @@ -292,9 +322,94 @@ describe('formatHtmlReport', () => { expect(again).not.toMatch(/