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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- The Components and Vulnerabilities sections link to each other. A component's expanded detail opens the vulnerability list filtered to that component, and a vulnerability's detail opens the component list filtered to its package. Moving between the two lists meant retyping the name into the other section's search box. The links sit in the expanded detail because each table row is itself the expand control, and a control nested inside a control is not announced reliably by screen readers.

- The fixed-version column reads in a darker green. The previous shade measured 3.77:1 against the light background, below the 4.5:1 minimum.

- Deleting a scan asks for confirmation first, naming the scan it is about to remove. The delete control in the scan table and the one in the top bar's scan menu both removed a scan's output folder on a single click, and because the files are gone from disk with no copy kept, a mis-click could not be taken back. The prompt opens on Cancel, and a confirmed delete says so. Modal dialogs also hold the keyboard now: focus moves into the panel when one opens, Tab stays inside it, and it returns to whatever opened the dialog on close.

### Added
Expand Down
29 changes: 26 additions & 3 deletions docker/web/frontend/src/components/ComponentsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ interface Props {
/** License id seeded from a Licenses distribution row; selects that license
* in the license filter, leaving the other filters open. */
initialLicense?: string;
/** Open the Vulnerabilities section filtered to this component — the other
* half of the investigation loop (which CVEs does this row stand for?). */
onPickVulns?: (name: string) => void;
}

type Sort = { key: ComponentSortKey; dir: SortDir };
Expand Down Expand Up @@ -127,6 +130,7 @@ export function ComponentsTable({
truncated,
initialQuery,
initialLicense,
onPickVulns,
}: Props) {
const { t } = useTranslation();
const [filters, setFilters] = useState<ComponentFilters>(() => ({
Expand Down Expand Up @@ -541,9 +545,28 @@ export function ComponentsTable({
{c.vulnCount ? (
<>
<dt className="font-medium text-muted-foreground">{t("nav.vulnerabilities")}</dt>
<dd>
{c.maxSeverity ? `${t(`severity.${c.maxSeverity}`)} · ` : ""}
{c.vulnCount}
<dd className="flex flex-wrap items-baseline gap-2">
<span>
{c.maxSeverity ? `${t(`severity.${c.maxSeverity}`)} · ` : ""}
{c.vulnCount}
</span>
{/* Into the CVEs behind this row. It sits in the
expanded detail rather than on the risk badge
because the row itself is the toggle control,
and a control inside a control is not announced
reliably. */}
{onPickVulns && (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
onPickVulns(c.name);
}}
className="rounded text-primary underline-offset-2 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
{t("result.viewInVulns", { name: c.name })}
</button>
)}
</dd>
</>
) : null}
Expand Down
10 changes: 8 additions & 2 deletions docker/web/frontend/src/components/NextApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -349,10 +349,16 @@ export function NextApp() {
}
};

// An Overview risk-bar click routes into the section with that filter applied.
// An Overview risk-bar click, or a name picked out of a result table, routes
// into the section with that filter applied.
const handleFilterPick = (
section: SectionId,
filter: { severity?: Severity; tier?: LicenseRiskTier; license?: string },
filter: {
severity?: Severity;
tier?: LicenseRiskTier;
license?: string;
term?: string;
},
) => {
setSeed({ section, ...filter });
if (loadedIdRef.current) {
Expand Down
20 changes: 17 additions & 3 deletions docker/web/frontend/src/components/ResultSections.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,17 @@ export function ResultSection({
seedTier?: LicenseRiskTier | "";
/** License id seeded into the Components license filter (Licenses row click). */
seedLicense?: string;
/** Route into a section with a filter pre-applied (the Overview risk bars,
* a Licenses distribution row). */
/** Route into a section with a filter pre-applied (the Overview risk bars, a
* Licenses distribution row, a component or package name from the table the
* user is reading). */
onPick?: (
section: SectionId,
seed: { severity?: Severity; tier?: LicenseRiskTier; license?: string },
seed: {
severity?: Severity;
tier?: LicenseRiskTier;
license?: string;
term?: string;
},
) => void;
/** An artifact was produced after the scan (the on-demand SPDX export), so
* the owner can refresh the result it holds. */
Expand All @@ -83,6 +89,11 @@ export function ResultSection({
truncated={result.sbom?.truncated}
initialQuery={searchQuery}
initialLicense={seedLicense}
onPickVulns={
onPick && result.security
? (name) => onPick("vulnerabilities", { term: name })
: undefined
}
/>
);

Expand All @@ -92,6 +103,9 @@ export function ResultSection({
security={result.security}
initialQuery={searchQuery}
initialSeverity={seedSeverity}
onPickComponent={
onPick ? (name) => onPick("components", { term: name }) : undefined
}
/>
) : (
<EmptyState>{t("result.noSecurity")}</EmptyState>
Expand Down
45 changes: 41 additions & 4 deletions docker/web/frontend/src/components/VulnerabilitiesTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ interface Props {
initialQuery?: string;
/** Severity seeded from an Overview severity-bar click (filters on open). */
initialSeverity?: string;
/** Open the Components section filtered to this package — the other half of
* the investigation loop (what does this CVE's package ship under?). */
onPickComponent?: (name: string) => void;
}

type Sort = { key: VulnSortKey; dir: SortDir };
Expand Down Expand Up @@ -88,7 +91,15 @@ function vulnLinks(v: VulnItem): string[] {
}

/** Expanded detail for one CVE — CVSS, description and reference links. */
function VulnDetail({ vuln, links }: { vuln: VulnItem; links: string[] }) {
function VulnDetail({
vuln,
links,
onPickComponent,
}: {
vuln: VulnItem;
links: string[];
onPickComponent?: (name: string) => void;
}) {
const { t } = useTranslation();
if (vuln.cvss == null && !vuln.description && links.length === 0) {
return <p className="text-muted-foreground">{t("result.vulnNoDetail")}</p>;
Expand Down Expand Up @@ -134,6 +145,21 @@ function VulnDetail({ vuln, links }: { vuln: VulnItem; links: string[] }) {
</ul>
</div>
) : null}
{/* Back to the package this CVE is against, in the component inventory.
It lives in the expanded detail because the row itself is the toggle
control, and a control nested in a control is not announced reliably. */}
{onPickComponent && vuln.pkg ? (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
onPickComponent(vuln.pkg);
}}
className="rounded text-primary underline-offset-2 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
{t("result.viewInComponents", { name: vuln.pkg })}
</button>
) : null}
</div>
);
}
Expand All @@ -143,7 +169,12 @@ function VulnDetail({ vuln, links }: { vuln: VulnItem; links: string[] }) {
* CVSS score, description and reference links already present in the Trivy
* report — no extra fetch, no side panel.
*/
export function VulnerabilitiesTable({ security, initialQuery, initialSeverity }: Props) {
export function VulnerabilitiesTable({
security,
initialQuery,
initialSeverity,
onPickComponent,
}: Props) {
const { t } = useTranslation();
const items = security.vulnerabilities ?? [];
const [openKey, setOpenKey] = useState<string | null>(null);
Expand Down Expand Up @@ -324,9 +355,11 @@ export function VulnerabilitiesTable({ security, initialQuery, initialSeverity }
<td className="px-3 py-2 font-mono tabular-nums text-muted-foreground">
{v.installed || "—"}
</td>
{/* Fixed version: -700 rather than -600, which measures 3.77
against the light surface, under the 4.5 minimum. */}
<td className="px-3 py-2 font-mono tabular-nums">
{v.fixed ? (
<span className="text-emerald-600 dark:text-emerald-400">
<span className="text-emerald-700 dark:text-emerald-400">
{v.fixed}
</span>
) : (
Expand All @@ -337,7 +370,11 @@ export function VulnerabilitiesTable({ security, initialQuery, initialSeverity }
{isOpen && (
<tr className="border-b last:border-0">
<td colSpan={anyEpss ? 7 : 6} className="bg-muted/30 px-3 py-3">
<VulnDetail vuln={v} links={links} />
<VulnDetail
vuln={v}
links={links}
onPickComponent={onPickComponent}
/>
</td>
</tr>
)}
Expand Down
4 changes: 3 additions & 1 deletion docker/web/frontend/src/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,9 @@
"colCurrency": "Version currency",
"maliciousBadge": "Malicious package",
"maliciousBadgeHint": "Published to attack whoever installs it. Remove it and rotate any credential the build could reach — there is no version to upgrade to.",
"kernelAdvisories": "{{count}} kernel advisories are reported in the security report and are not counted above. Most advisories against a kernel are for subsystems a device never compiled in, and the SBOM cannot tell which."
"kernelAdvisories": "{{count}} kernel advisories are reported in the security report and are not counted above. Most advisories against a kernel are for subsystems a device never compiled in, and the SBOM cannot tell which.",
"viewInComponents": "View {{name}} in Components",
"viewInVulns": "View vulnerabilities for {{name}}"
},
"deps": {
"loading": "Loading dependencies…",
Expand Down
4 changes: 3 additions & 1 deletion docker/web/frontend/src/locales/ko/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,9 @@
"colCurrency": "버전 최신성",
"maliciousBadge": "악성 패키지",
"maliciousBadgeHint": "설치하는 쪽을 공격하려고 배포된 패키지입니다. 올릴 수 있는 버전이 없으므로 제거하고, 빌드가 접근할 수 있었던 자격 증명을 교체하세요.",
"kernelAdvisories": "커널 관련 권고 {{count}}건은 보안 보고서에 실리며 위 집계에는 넣지 않았습니다. 커널 권고 대부분은 그 장비가 빌드하지 않은 서브시스템에 대한 것이고, SBOM으로는 어느 것인지 가릴 수 없습니다."
"kernelAdvisories": "커널 관련 권고 {{count}}건은 보안 보고서에 실리며 위 집계에는 넣지 않았습니다. 커널 권고 대부분은 그 장비가 빌드하지 않은 서브시스템에 대한 것이고, SBOM으로는 어느 것인지 가릴 수 없습니다.",
"viewInComponents": "컴포넌트에서 {{name}} 보기",
"viewInVulns": "{{name}}의 취약점 보기"
},
"deps": {
"loading": "의존성을 불러오는 중…",
Expand Down
Loading
Loading