diff --git a/DOCS.md b/DOCS.md index 92c8bdf2..2cc04ce2 100644 --- a/DOCS.md +++ b/DOCS.md @@ -501,6 +501,7 @@ external files. The page opens with a summary: - DriftScore and the four component scores. A missing score is `n/a`. A measured `0` stays `0`. - 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. +- A **Fix available only** checkbox. It starts unchecked, so the tables show every finding. Checking it hides findings that do not name a fix version. The page file still contains every finding, and the checkbox does not call the network. - Links to the full tables under the summary: vulnerabilities, security findings, and the other findings. The same artifact always produces the same page. Save it with a shell redirect: @@ -597,7 +598,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`, a Fix available only checkbox, and links to the tables), then the full vulnerability, security, and other-finding tables. The checkbox starts unchecked. Checking it hides findings that do not name a fix version. Those rows stay in the page file, and the summary counts stay the full set. A check that did not run stays "Not scanned". A missing score is `n/a`. The same artifact always produces the same page. ##### Differences @@ -610,7 +611,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. A Fix available only checkbox starts unchecked and can hide findings that do not name a fix version. The page file still lists every finding. It does not print Markdown's recommended-standards section or the text inventory. ##### JSON diff --git a/src/reporting/formatters/html.test.ts b/src/reporting/formatters/html.test.ts index 96ab9deb..c59b2244 100644 --- a/src/reporting/formatters/html.test.ts +++ b/src/reporting/formatters/html.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest'; import type { SecurityFinding } from '../../core-open/types.js'; import type { ScanArtifact } from '../types.js'; -import { ADVISORY_CORPUS_FOOTER_ID, FIX_AVAILABLE_ATTR, formatHtmlReport } from './html.js'; +import { ADVISORY_CORPUS_FOOTER_ID, FIX_AVAILABLE_ATTR, FIX_AVAILABLE_ONLY_ID, formatHtmlReport } from './html.js'; const TOKEN = ['ghp', '0123456789abcdefghijklmnopqrstuvwxyzA'].join('_'); @@ -226,7 +226,7 @@ describe('formatHtmlReport', () => { expect(block(html, 'vuln-counts')).toContain('Not scanned'); expect(block(html, 'vuln-counts')).not.toContain('critical'); expect(block(html, 'security-counts')).toContain('Not scanned'); - expect(html).not.toContain('id="fix-available"'); + expect(html).not.toContain('
'); expect(section(html, 'vulnerabilities')).toContain('Not scanned'); expect(section(html, 'security')).toContain('Not scanned'); expect(section(html, 'findings')).toContain('None'); @@ -278,6 +278,31 @@ describe('formatHtmlReport', () => { expect(block(html, 'fix-available')).toBe('
Fix available: 0
'); }); + it('adds an unchecked fix-available-only toggle and still emits every row', () => { + const html = formatHtmlReport(sample()); + const control = ``; + const rule = `body:has(#${FIX_AVAILABLE_ONLY_ID}:checked) tr[${FIX_AVAILABLE_ATTR}="no"] { display: none; }`; + + expect(html).toContain(control); + expect(html).toContain(rule); + expect(html).not.toMatch(/]*\schecked[\s=>]/); + expect(html.split(control)).toHaveLength(2); + + const vulns = section(html, 'vulnerabilities'); + const findings = section(html, 'findings'); + expect(vulns).toContain(`${FIX_AVAILABLE_ATTR}="yes"`); + expect(vulns).toContain(`${FIX_AVAILABLE_ATTR}="no"`); + expect(vulns).toContain('fix available (4.17.21)'); + expect(vulns).toContain('GHSA-high'); + expect(findings).toContain(`${FIX_AVAILABLE_ATTR}="yes"`); + expect(section(html, 'security')).toContain(`${FIX_AVAILABLE_ATTR}="no"`); + + const empty = formatHtmlReport(artifact()); + expect(empty).toContain(control); + expect(empty).toContain(rule); + expect(formatHtmlReport(sample())).toBe(html); + }); + it('is deterministic and does not embed a timestamp, a script, or external assets', () => { const first = sample(); const again = formatHtmlReport(first); @@ -293,7 +318,8 @@ describe('formatHtmlReport', () => { expect(again).not.toMatch(/]/i); expect(again).not.toMatch(/\ssrc\s*=/i); expect(again).not.toMatch(/Fix available: ${fix}
\n`} +${fix === null ? '' : `Fix available: ${fix}
\n`}${fixAvailableOnlyControl()}