diff --git a/src/report/web/App.browser.test.tsx b/src/report/web/App.browser.test.tsx index ce10e7d..a9c2c0f 100644 --- a/src/report/web/App.browser.test.tsx +++ b/src/report/web/App.browser.test.tsx @@ -112,22 +112,26 @@ describe('App report shell', () => { expect(screen.getByTestId(riskStoryTestIds.severityBar)).toBeInTheDocument(); }); - it('summarizes repos with security alerts disabled and lists them in the source note', () => { + it('summarizes repos with security alerts disabled and links to the appendix table', () => { renderReport({ cve: cveExposureOk.build({ reposWithSecurityAlertsDisabled: ['acme/a', 'acme/b'] }) }); const warning = screen.getByTestId(riskStoryTestIds.disabledAlertsWarning); expect(warning).toHaveTextContent( 'Did you know: 2 of your 24 repos do not have Dependabot security alerts enabled', ); - expect(warning).not.toHaveTextContent('*'); + + const details = screen.getByTestId(methodologyAppendixTestIds.section).querySelector('details'); + expect(details).not.toHaveAttribute('open'); const restore = suppressNavigation(); - fireEvent.click(within(warning).getByTestId(footnoteReferenceTestId)); + fireEvent.click(within(warning).getByTestId(riskStoryTestIds.disabledAlertsLink)); restore(); - const sources = screen.getByTestId(methodologyAppendixTestIds.sources); - expect(sources).toHaveTextContent('Repos without security alerts enabled'); - expect(sources).toHaveTextContent('acme/a, acme/b'); + // The link opens the collapsed appendix on the Calculation tab, where the repos render as a table. + expect(details).toHaveAttribute('open'); + const table = screen.getByTestId(methodologyAppendixTestIds.disabledAlertsRepos); + expect(table).toHaveTextContent('acme/a'); + expect(table).toHaveTextContent('acme/b'); }); it('limits the top repos by severity table to the top five with an optional expansion', () => { @@ -144,12 +148,12 @@ describe('App report shell', () => { }); const table = screen.getByTestId(riskStoryTestIds.topReposTable); - expect(within(table).getByText('acme/repo-5')).toBeInTheDocument(); - expect(within(table).queryByText('acme/repo-6')).not.toBeInTheDocument(); + expect(within(table).getByText('repo-5')).toBeInTheDocument(); + expect(within(table).queryByText('repo-6')).not.toBeInTheDocument(); fireEvent.click(screen.getByTestId(riskStoryTestIds.topReposToggle)); - expect(within(table).getByText('acme/repo-6')).toBeInTheDocument(); + expect(within(table).getByText('repo-6')).toBeInTheDocument(); expect(screen.getByTestId(riskStoryTestIds.topReposToggle)).toHaveTextContent('Show top 5'); }); diff --git a/src/report/web/acts/MethodologyAppendix.tsx b/src/report/web/acts/MethodologyAppendix.tsx index 61bfc73..2142d67 100644 --- a/src/report/web/acts/MethodologyAppendix.tsx +++ b/src/report/web/acts/MethodologyAppendix.tsx @@ -138,6 +138,10 @@ export function MethodologyAppendix() { .

+ + {cve.reposWithSecurityAlertsDisabled.length > 0 && ( + + )} )} @@ -240,13 +244,6 @@ export function MethodologyAppendix() { ['Oldest high', cve.oldestHighDays === null ? 'n/a' : `${cve.oldestHighDays} days`], ]} /> - @@ -319,6 +316,28 @@ function FormulaRow({ label, value }: { label: string; value: ReactNode }) { ); } +function ReposWithoutAlertsTable({ repos }: { repos: readonly string[] }) { + return ( +
+ Repos without security alerts enabled +

+ New CVEs in {repos.length === 1 ? 'this repo' : 'these repos'} will not appear in this report. +

+
+ + + {repos.map((repo) => ( + + + + ))} + +
{repo}
+
+
+ ); +} + function DataPanel({ title, children }: { title: string; children: ReactNode }) { return (
@@ -383,21 +402,9 @@ function SourcesAndNotes() { ); } -function RepoList({ - id, - testId, - label, - empty, - repos, -}: { - id?: string; - testId?: string; - label: string; - empty: string; - repos: readonly string[]; -}) { +function RepoList({ label, empty, repos }: { label: string; empty: string; repos: readonly string[] }) { return ( -
+
{label}
{repos.length === 0 ? (

{empty}

diff --git a/src/report/web/acts/RiskStory.tsx b/src/report/web/acts/RiskStory.tsx index 3886cf3..15fcee1 100644 --- a/src/report/web/acts/RiskStory.tsx +++ b/src/report/web/acts/RiskStory.tsx @@ -1,7 +1,8 @@ import { useState } from 'react'; import { useEmbeddedData } from '../data/EmbeddedDataContext.tsx'; +import { repoShortName } from '../format/repo.ts'; +import { useAssumptionsDisclosure } from '../hooks/useAssumptionsDisclosure.tsx'; import { Citation } from '../primitives/Citation.tsx'; -import { FootnoteReference } from '../primitives/FootnoteReference.tsx'; import { SEGMENTS, StackedBar } from '../primitives/StackedBar.tsx'; export const riskStoryTestIds = { @@ -12,6 +13,7 @@ export const riskStoryTestIds = { topReposTable: 'risk-story-top-repos-table', topReposToggle: 'risk-story-top-repos-toggle', disabledAlertsWarning: 'risk-story-disabled-alerts-warning', + disabledAlertsLink: 'risk-story-disabled-alerts-link', } as const; export const riskStoryCopy = { @@ -25,10 +27,7 @@ export const reposWithoutSecurityAlertsId = 'repos-without-security-alerts'; export function RiskStory() { const { cve, orgOverview } = useEmbeddedData(); - const disabledAlertsNote = - cve.reposWithSecurityAlertsDisabled.length > 0 - ? `Repos without security alerts enabled: ${cve.reposWithSecurityAlertsDisabled.join(', ')}. New CVEs in these repos will not appear in this report.` - : 'These repos returned a not-enabled response for Dependabot security alerts, so new CVEs in them will not appear in this report.'; + const { reveal } = useAssumptionsDisclosure(); if (cve.status === 'scope-missing') { return ( @@ -136,14 +135,18 @@ export function RiskStory() { {cve.reposWithSecurityAlertsDisabled.length} {' '} of your {orgOverview.repoCount} repos{' '} - {cve.reposWithSecurityAlertsDisabled.length === 1 ? 'does' : 'do'} not have Dependabot security alerts enabled - - . New CVEs in {cve.reposWithSecurityAlertsDisabled.length === 1 ? 'that repo' : 'those repos'} will not appear - in this report. + {cve.reposWithSecurityAlertsDisabled.length === 1 ? 'does' : 'do'} not have Dependabot security alerts + enabled. New CVEs in {cve.reposWithSecurityAlertsDisabled.length === 1 ? 'that repo' : 'those repos'} will not + appear in this report.{' '} + reveal('calculation')} + className="text-primary underline underline-offset-4" + > + {cve.reposWithSecurityAlertsDisabled.length === 1 ? 'See the repo' : 'See the full list'} + + .

)} @@ -191,7 +194,7 @@ function RepoSeverityBar({ repo, maxTotal }: { repo: RepoSeverityRow; maxTotal: return (
- {repo.repo} + {repoShortName(repo.repo)}
diff --git a/src/report/web/format/repo.ts b/src/report/web/format/repo.ts new file mode 100644 index 0000000..50772c5 --- /dev/null +++ b/src/report/web/format/repo.ts @@ -0,0 +1,3 @@ +export function repoShortName(repo: string): string { + return repo.slice(repo.lastIndexOf('/') + 1); +}