Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 13 additions & 9 deletions src/report/web/App.browser.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,22 +112,26 @@ describe('App report shell', () => {
expect(screen.getByTestId(riskStoryTestIds.severityBar)).toBeInTheDocument();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for long org names, the repo list was not useful

});

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', () => {
Expand All @@ -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');
});

Expand Down
49 changes: 28 additions & 21 deletions src/report/web/acts/MethodologyAppendix.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,10 @@ export function MethodologyAppendix() {
<Citation source="atlassian-dx-2025" />.
</p>
</section>

{cve.reposWithSecurityAlertsDisabled.length > 0 && (
<ReposWithoutAlertsTable repos={cve.reposWithSecurityAlertsDisabled} />
)}
</div>
)}

Expand Down Expand Up @@ -240,13 +244,6 @@ export function MethodologyAppendix() {
['Oldest high', cve.oldestHighDays === null ? 'n/a' : `${cve.oldestHighDays} days`],
]}
/>
<RepoList
id={reposWithoutSecurityAlertsId}
testId={methodologyAppendixTestIds.disabledAlertsRepos}
label="Repos without security alerts enabled"
empty="No scanned repos reported security alerts as disabled."
repos={cve.reposWithSecurityAlertsDisabled}
/>
<SeverityTable repos={cve.topReposBySeverity} />
</DataPanel>

Expand Down Expand Up @@ -319,6 +316,28 @@ function FormulaRow({ label, value }: { label: string; value: ReactNode }) {
);
}

function ReposWithoutAlertsTable({ repos }: { repos: readonly string[] }) {
return (
<section id={reposWithoutSecurityAlertsId} className="scroll-mt-16">
<SectionHeading>Repos without security alerts enabled</SectionHeading>
<p className="text-muted-foreground mt-2 max-w-3xl leading-relaxed">
New CVEs in {repos.length === 1 ? 'this repo' : 'these repos'} will not appear in this report.
</p>
<div className="border-border mt-3 overflow-hidden rounded-md border">
<table data-testid={methodologyAppendixTestIds.disabledAlertsRepos} className="w-full text-left text-sm">
<tbody className="divide-border divide-y">
{repos.map((repo) => (
<tr key={repo}>
<td className="text-foreground px-3 py-2.5 font-mono text-xs">{repo}</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
);
}

function DataPanel({ title, children }: { title: string; children: ReactNode }) {
return (
<div className="border-border bg-card rounded-md border p-4">
Expand Down Expand Up @@ -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 (
<div id={id} data-testid={testId} className="scroll-mt-16">
<div>
<div className="text-muted-foreground mb-1 text-xs font-medium tracking-[0.12em] uppercase">{label}</div>
{repos.length === 0 ? (
<p className="text-muted-foreground">{empty}</p>
Expand Down
31 changes: 17 additions & 14 deletions src/report/web/acts/RiskStory.tsx
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -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 = {
Expand All @@ -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 (
Expand Down Expand Up @@ -136,14 +135,18 @@ export function RiskStory() {
{cve.reposWithSecurityAlertsDisabled.length}
</span>{' '}
of your <span className="text-foreground font-semibold tabular-nums">{orgOverview.repoCount}</span> repos{' '}
{cve.reposWithSecurityAlertsDisabled.length === 1 ? 'does' : 'do'} not have Dependabot security alerts enabled
<FootnoteReference
id="security-alerts-disabled"
title="Repos without security alerts enabled"
body={disabledAlertsNote}
/>
. 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.{' '}
<a
href={`#${reposWithoutSecurityAlertsId}`}
data-testid={riskStoryTestIds.disabledAlertsLink}
onClick={() => reveal('calculation')}
className="text-primary underline underline-offset-4"
>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

footnote was weird - rendering in a table now

{cve.reposWithSecurityAlertsDisabled.length === 1 ? 'See the repo' : 'See the full list'}
</a>
.
</p>
)}
</section>
Expand Down Expand Up @@ -191,7 +194,7 @@ function RepoSeverityBar({ repo, maxTotal }: { repo: RepoSeverityRow; maxTotal:
return (
<div className="grid grid-cols-[minmax(0,10rem)_1fr_2.5rem] items-center gap-3 text-sm">
<div className="text-foreground truncate font-mono text-xs" title={repo.repo}>
{repo.repo}
{repoShortName(repo.repo)}
</div>
<div className="bg-muted h-3.5 overflow-hidden rounded-full">
<div className="flex h-full" style={{ width: `${(total / maxTotal) * 100}%` }}>
Expand Down
3 changes: 3 additions & 0 deletions src/report/web/format/repo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function repoShortName(repo: string): string {
return repo.slice(repo.lastIndexOf('/') + 1);
}