diff --git a/web/src/layout/package/securityReport/Modal.tsx b/web/src/layout/package/securityReport/Modal.tsx index fbf45e9e0..524c55108 100644 --- a/web/src/layout/package/securityReport/Modal.tsx +++ b/web/src/layout/package/securityReport/Modal.tsx @@ -9,7 +9,12 @@ import API from '../../../api'; import { FixableVulnerabilitiesInReport, RepositoryKind, SecurityReport, SecurityReportSummary } from '../../../types'; import alertDispatcher from '../../../utils/alertDispatcher'; import isFuture from '../../../utils/isFuture'; -import { filterFixableVulnerabilities, prepareFixableSummary } from '../../../utils/vulnerabilities'; +import sumObjectValues from '../../../utils/sumObjectValues'; +import { + filterFixableVulnerabilities, + prepareFixableSummary, + prepareUniqueVulnerabilitiesSummary, +} from '../../../utils/vulnerabilities'; import Modal from '../../common/Modal'; import styles from './Modal.module.css'; import OldVulnerabilitiesWarning from './OldVulnerabilitiesWarning'; @@ -49,6 +54,8 @@ const SecurityModal = (props: Props) => { const [hasOnlyOneTarget, setHasOnlyOneTarget] = useState(false); const [contentHeight, setContentHeight] = useState(undefined); const [fixableReportSummary, setFixableReportSummary] = useState(); + const [uniqueSummary, setUniqueSummary] = useState(null); + const [totalUniqueVulnerabilities, setTotalUniqueVulnerabilities] = useState(0); const [showOnlyFixableVulnerabilities, setShowOnlyFixableVulnerabilities] = useState(false); const allVulnerabilitiesAreFixable = !isUndefined(fixableReportSummary) && fixableReportSummary.total === props.totalVulnerabilities; @@ -79,6 +86,11 @@ const SecurityModal = (props: Props) => { const fixableVulnerabilities = filterFixableVulnerabilities(currentReport); setFixableReport(fixableVulnerabilities); setFixableReportSummary(prepareFixableSummary(fixableVulnerabilities)); + const uniqueSummary = prepareUniqueVulnerabilitiesSummary(currentReport); + setUniqueSummary(uniqueSummary); + if (!isNull(uniqueSummary)) { + setTotalUniqueVulnerabilities(sumObjectValues(uniqueSummary)); + } activateTargetWhenIsOnlyOne(currentReport); setIsLoading(false); setOpenStatus(true); @@ -228,6 +240,8 @@ const SecurityModal = (props: Props) => { fixableSummary={fixableReportSummary.summary} totalFixableVulnerabilities={fixableReportSummary.total} allVulnerabilitiesAreFixable={allVulnerabilitiesAreFixable} + uniqueSummary={uniqueSummary} + totalUniqueVulnerabilities={totalUniqueVulnerabilities} /> )} diff --git a/web/src/layout/package/securityReport/Summary.test.tsx b/web/src/layout/package/securityReport/Summary.test.tsx index 1bb51a118..983f5c185 100644 --- a/web/src/layout/package/securityReport/Summary.test.tsx +++ b/web/src/layout/package/securityReport/Summary.test.tsx @@ -21,6 +21,14 @@ const defaultProps = { medium: 41, unknown: 0, }, + uniqueSummary: { + critical: 2, + high: 7, + low: 40, + medium: 60, + unknown: 0, + }, + totalUniqueVulnerabilities: 109, allVulnerabilitiesAreFixable: false, }; @@ -39,15 +47,17 @@ describe('SecuritySummary', () => { render(); expect(screen.getByText('170')).toBeInTheDocument(); expect(screen.getByText(/have been detected in this package's/)).toBeInTheDocument(); - expect(screen.getByText('2')).toBeInTheDocument(); + expect(screen.getAllByText('2')).toHaveLength(2); expect(screen.getByText('10')).toBeInTheDocument(); expect(screen.getByText('53')).toBeInTheDocument(); expect(screen.getByText('105')).toBeInTheDocument(); expect(screen.queryByText('0')).toBeNull(); expect(screen.getByText('80')).toBeInTheDocument(); - expect(screen.getByText('7')).toBeInTheDocument(); + expect(screen.getAllByText('7')).toHaveLength(2); expect(screen.getByText('32')).toBeInTheDocument(); expect(screen.getByText('41')).toBeInTheDocument(); + expect(screen.getByText('40')).toBeInTheDocument(); + expect(screen.getByText('60')).toBeInTheDocument(); }); it('renders component with 0 vulnerabilities', () => { @@ -67,6 +77,8 @@ describe('SecuritySummary', () => { low: 0, }} allVulnerabilitiesAreFixable={false} + uniqueSummary={null} + totalUniqueVulnerabilities={0} /> ); expect(screen.getByText(/No vulnerabilities have been detected in this package's/)).toBeInTheDocument(); @@ -90,6 +102,8 @@ describe('SecuritySummary', () => { low: 0, }} allVulnerabilitiesAreFixable={false} + uniqueSummary={null} + totalUniqueVulnerabilities={0} /> ); expect(screen.getByText(/No vulnerabilities have been detected in this package's/)).toBeInTheDocument(); diff --git a/web/src/layout/package/securityReport/Summary.tsx b/web/src/layout/package/securityReport/Summary.tsx index bacf98a8e..f1c9e0f3d 100644 --- a/web/src/layout/package/securityReport/Summary.tsx +++ b/web/src/layout/package/securityReport/Summary.tsx @@ -1,4 +1,4 @@ -import { isUndefined } from 'lodash'; +import { isNull, isUndefined } from 'lodash'; import { RepositoryKind, SecurityReportSummary, VulnerabilitySeverity } from '../../../types'; import { SEVERITY_ORDER, SEVERITY_RATING } from '../../../utils/data'; @@ -9,6 +9,8 @@ interface Props { totalVulnerabilities: number; summary: SecurityReportSummary; fixableSummary: SecurityReportSummary; + uniqueSummary: SecurityReportSummary | null; + totalUniqueVulnerabilities: number; totalFixableVulnerabilities: number; allVulnerabilitiesAreFixable: boolean; } @@ -22,11 +24,25 @@ const SecuritySummary = (props: Props) => { } }; - const getFixableVulnerabilitiesNumber = (): JSX.Element => { - if (props.totalFixableVulnerabilities > 0) { + const getExtraData = (): JSX.Element => { + if (props.totalFixableVulnerabilities > 0 || props.totalUniqueVulnerabilities > 0) { + const visibleFixable = props.totalFixableVulnerabilities > 0 && !props.allVulnerabilitiesAreFixable; + return ( <> - ({props.totalFixableVulnerabilities} fixable){' '} + ( + {props.totalUniqueVulnerabilities > 0 && ( + <> + {props.totalUniqueVulnerabilities} unique + {visibleFixable && <>, } + + )} + {visibleFixable && ( + <> + {props.totalFixableVulnerabilities} fixable + + )} + ){' '} ); } else { @@ -36,6 +52,7 @@ const SecuritySummary = (props: Props) => { const renderProgressBar = (summary: SecurityReportSummary, total: number, legend: string): JSX.Element | null => { if (total === 0) return null; + return ( <>
@@ -70,10 +87,20 @@ const SecuritySummary = (props: Props) => { return (
- {getVulnerabilitiesNumber()} vulnerabilities {getFixableVulnerabilitiesNumber()} have been detected in this - package's {props.repoKind === RepositoryKind.Container ? 'image' : 'images'}. + {getVulnerabilitiesNumber()} vulnerabilities {getExtraData()} have been detected in this package's{' '} + {props.repoKind === RepositoryKind.Container ? 'image' : 'images'}.
+ {!isNull(props.uniqueSummary) && ( + <> + {renderProgressBar( + props.uniqueSummary, + props.totalUniqueVulnerabilities, + `Unique vulnerabilities (${props.totalUniqueVulnerabilities})` + )} + + )} + {!props.allVulnerabilitiesAreFixable && ( <> {renderProgressBar( @@ -87,7 +114,9 @@ const SecuritySummary = (props: Props) => { {renderProgressBar( props.summary, props.totalVulnerabilities, - props.allVulnerabilitiesAreFixable ? '' : `All vulnerabilities (${props.totalVulnerabilities})` + props.allVulnerabilitiesAreFixable && isNull(props.uniqueSummary) + ? '' + : `All vulnerabilities (${props.totalVulnerabilities})` )}
); diff --git a/web/src/layout/package/securityReport/__snapshots__/Modal.test.tsx.snap b/web/src/layout/package/securityReport/__snapshots__/Modal.test.tsx.snap index 6648abd1c..b93c3a808 100644 --- a/web/src/layout/package/securityReport/__snapshots__/Modal.test.tsx.snap +++ b/web/src/layout/package/securityReport/__snapshots__/Modal.test.tsx.snap @@ -293,6 +293,12 @@ exports[`SecurityModal creates snapshot 1`] = ` vulnerabilities ( + 511 + + unique, + 186 @@ -304,6 +310,66 @@ exports[`SecurityModal creates snapshot 1`] = ` .
+
+ + Unique vulnerabilities (511) + +
+
+
+ + 1 + +
+
+ + 13 + +
+
+ + 212 + +
+
+ + 285 + +
+
diff --git a/web/src/layout/package/securityReport/__snapshots__/Summary.test.tsx.snap b/web/src/layout/package/securityReport/__snapshots__/Summary.test.tsx.snap index a516ea6f1..2388523f5 100644 --- a/web/src/layout/package/securityReport/__snapshots__/Summary.test.tsx.snap +++ b/web/src/layout/package/securityReport/__snapshots__/Summary.test.tsx.snap @@ -16,6 +16,12 @@ exports[`SecuritySummary creates snapshot 1`] = ` vulnerabilities ( + 109 + + unique, + 80 @@ -27,6 +33,66 @@ exports[`SecuritySummary creates snapshot 1`] = ` .
+
+ + Unique vulnerabilities (109) + +
+
+
+ + 2 + +
+
+ + 7 + +
+
+ + 60 + +
+
+ + 40 + +
+
diff --git a/web/src/utils/vulnerabilities.ts b/web/src/utils/vulnerabilities.ts index 2a69695e0..7e8259bda 100644 --- a/web/src/utils/vulnerabilities.ts +++ b/web/src/utils/vulnerabilities.ts @@ -1,6 +1,13 @@ -import { filter, isEmpty, isNull, isUndefined } from 'lodash'; +import { filter, isEmpty, isEqual, isNull, isUndefined } from 'lodash'; -import { FixableVulnerabilitiesInReport, SecurityReport, SecurityReportResult, Vulnerability } from '../types'; +import { + FixableVulnerabilitiesInReport, + SecurityReport, + SecurityReportResult, + SecurityReportSummary, + Vulnerability, + VulnerabilitySeverity, +} from '../types'; import formatSecurityReport from './formatSecurityReport'; import sumObjectValues from './sumObjectValues'; @@ -31,6 +38,41 @@ const prepareFixableSummary = ( return fixReport; }; +const prepareUniqueVulnerabilitiesSummary = (currentReport: SecurityReport | null): SecurityReportSummary | null => { + if (isNull(currentReport)) return null; + + const fullReportSumary: SecurityReportSummary = {}; + const summary: any = {}; + const uniqueSummaryReport: SecurityReportSummary = {}; + + Object.keys(currentReport).forEach((img: string) => { + currentReport[img].Results.forEach((target: SecurityReportResult) => { + if (target.Vulnerabilities) { + target.Vulnerabilities.forEach((vulnerability: Vulnerability) => { + const severity = vulnerability.Severity.toLowerCase() as VulnerabilitySeverity; + if (isUndefined(summary[severity])) { + summary[severity] = [vulnerability.VulnerabilityID]; + fullReportSumary[severity] = 1; + } else { + summary[severity].push(vulnerability.VulnerabilityID); + fullReportSumary[severity]! += 1; + } + }); + } + }); + }); + + Object.keys(summary).forEach((severity: string) => { + uniqueSummaryReport[severity as VulnerabilitySeverity] = new Set(summary[severity]).size; + }); + + if (isEqual(fullReportSumary, uniqueSummaryReport)) { + return null; + } else { + return uniqueSummaryReport; + } +}; + const filterFixableVulnerabilities = (currentReport: SecurityReport | null): SecurityReport | null => { if (isNull(currentReport)) return null; @@ -55,4 +97,4 @@ const filterFixableVulnerabilities = (currentReport: SecurityReport | null): Sec return tmpReport; }; -export { filterFixableVulnerabilities, prepareFixableSummary }; +export { filterFixableVulnerabilities, prepareFixableSummary, prepareUniqueVulnerabilitiesSummary };