Skip to content

Commit d6fab9c

Browse files
authored
EDM-4078: Add clarification details to severity badges (#679)
* EDM-4078: Add clarification details to severity badges Made-with: Cursor
1 parent a550d78 commit d6fab9c

7 files changed

Lines changed: 106 additions & 46 deletions

File tree

libs/i18n/locales/en/translation.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1673,6 +1673,7 @@
16731673
"Impact summary": "Impact summary",
16741674
"{{ advisoryId }} - Red Hat Security Advisory": "{{ advisoryId }} - Red Hat Security Advisory",
16751675
"CVE details - {{ advisoryId }}": "CVE details - {{ advisoryId }}",
1676+
"(may show a different impact rating)": "(may show a different impact rating)",
16761677
"CPU": "CPU",
16771678
"Memory": "Memory",
16781679
"Disk": "Disk",
@@ -1685,6 +1686,9 @@
16851686
"Overall status of application workloads.": "Overall status of application workloads.",
16861687
"Overall status of device hardware and operating system.": "Overall status of device hardware and operating system.",
16871688
"Current system configuration vs. latest system configuration.": "Current system configuration vs. latest system configuration.",
1689+
"This is the industry-standard CVSS base severity. Red Hat may assign a different impact rating for this CVE based on product-specific factors like default configurations and mitigations.": "This is the industry-standard CVSS base severity. Red Hat may assign a different impact rating for this CVE based on product-specific factors like default configurations and mitigations.",
1690+
"CVSS {{ score }}": "CVSS {{ score }}",
1691+
"CVSS base score help": "CVSS base score help",
16881692
"{{brand}} is waiting for the device to connect and report its status. It will report a ʼPending syncʼ status until it is able to reconnect. If it has configuration conflicts, it will report a ʼSuspendedʼ status and require manual action to resume.": "{{brand}} is waiting for the device to connect and report its status. It will report a ʼPending syncʼ status until it is able to reconnect. If it has configuration conflicts, it will report a ʼSuspendedʼ status and require manual action to resume.",
16891693
"{{brand}} is waiting for devices to connect and report their status. Devices will report a ʼPending syncʼ status until they are able to connect. Devices with configuration conflicts will report a ʼSuspendedʼ status and require manual action to resume.": "{{brand}} is waiting for devices to connect and report their status. Devices will report a ʼPending syncʼ status until they are able to connect. Devices with configuration conflicts will report a ʼSuspendedʼ status and require manual action to resume.",
16901694
"System recovery complete": "System recovery complete",

libs/ui-components/src/components/SecurityOverview/VulnerabilitiesTable.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ import StatusDisplay from '../Status/StatusDisplay';
3131
import { VulnerabilitiesTableCompactRow, VulnerabilitiesTableFullRow } from './VulnerabilitiesTableRow';
3232
import VulnerabilityDetailsDrawer from './VulnerabilityDetailsDrawer';
3333
import { VulnerabilitiesSingleEntityEmptyState } from './VulnerabilitiesEmptyState';
34-
3534
type VulnerabilitySeverity = Vulnerability['severity'];
3635

3736
type VulnerabilitiesTableCommonProps = {

libs/ui-components/src/components/SecurityOverview/VulnerabilitiesTableRow.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { useTranslation } from '../../hooks/useTranslation';
77
import { Link, ROUTE } from '../../hooks/useNavigate';
88
import { getDateNoTimeDisplay } from '../../utils/dates';
99
import { isVulnerabilityGroup } from '../../utils/vulnerabilities';
10-
import VulnerabilitySeverityStatus from '../Status/VulnerabilitySeverityStatus';
10+
import { VulnerabilitySeverityBadge } from '../Status/VulnerabilitySeverityStatus';
1111
import VulnerabilityAffectedImages, { getAffectedImages } from './VulnerabilityAffectedImages';
1212

1313
type VulnerabilitiesTableCompactRowProps = {
@@ -40,7 +40,7 @@ const VulnerabilitiesBaseTr = ({
4040
</Button>
4141
</Td>
4242
<Td dataLabel={t('Severity')}>
43-
<VulnerabilitySeverityStatus severity={vulnerability.severity} />
43+
<VulnerabilitySeverityBadge severity={vulnerability.severity} />
4444
</Td>
4545
{children}
4646
<Td dataLabel={t('Published')}>

libs/ui-components/src/components/SecurityOverview/VulnerabilityDetailsDrawer.tsx

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as React from 'react';
2-
import { Vulnerability, VulnerabilityGroup } from '@flightctl/types/alpha';
3-
import { isVulnerabilityGroup } from '../../utils/vulnerabilities';
2+
import { Vulnerability, VulnerabilityGroup, VulnerabilityGroupItem } from '@flightctl/types/alpha';
3+
import { getPrimaryVulnerabilityGroupFinding, isRedHatIssuer, isVulnerabilityGroup } from '../../utils/vulnerabilities';
44

55
import {
66
Content,
@@ -18,7 +18,7 @@ import {
1818
StackItem,
1919
} from '@patternfly/react-core';
2020

21-
import VulnerabilitySeverityStatus from '../Status/VulnerabilitySeverityStatus';
21+
import { VulnerabilitySeverityBadge, VulnerabilitySeverityDetails } from '../Status/VulnerabilitySeverityStatus';
2222
import { getDateDisplay } from '../../utils/dates';
2323
import { useTranslation } from '../../hooks/useTranslation';
2424

@@ -41,19 +41,27 @@ const VulnerabilityDetailsDrawer = ({
4141
onClose,
4242
}: VulnerabilityDetailsDrawerProps) => {
4343
const { t } = useTranslation();
44+
45+
let primaryFinding: Vulnerability | VulnerabilityGroupItem | undefined = vulnerability;
4446
const isGrouped = isVulnerabilityGroup(vulnerability);
47+
if (isGrouped) {
48+
primaryFinding = getPrimaryVulnerabilityGroupFinding(vulnerability.findings);
49+
}
4550

51+
let cvssScore: number | undefined;
4652
let publishedAt: string | undefined;
4753
let description: string | undefined;
48-
let hasReferences = false;
54+
let isRHIssuer: boolean = false;
55+
if (primaryFinding) {
56+
cvssScore = primaryFinding.cvssScore;
57+
publishedAt = primaryFinding.publishedAt;
58+
description = primaryFinding.description;
59+
isRHIssuer = isRedHatIssuer(primaryFinding.issuer);
60+
}
4961
if (isGrouped) {
50-
publishedAt = vulnerability.maxPublishedAt;
51-
description = vulnerability.findings[0].description;
52-
hasReferences = vulnerability.findings[0].link !== undefined;
53-
} else {
54-
publishedAt = vulnerability.publishedAt;
55-
description = vulnerability.description;
56-
hasReferences = !!vulnerability.link;
62+
// The vulnerabilityGroup does not have links or descriptions
63+
cvssScore = cvssScore ?? vulnerability.maxCvssScore;
64+
publishedAt = publishedAt ?? vulnerability.maxPublishedAt;
5765
}
5866

5967
return (
@@ -82,7 +90,11 @@ const VulnerabilityDetailsDrawer = ({
8290
<DescriptionListGroup>
8391
<DescriptionListTerm>{t('Severity')}</DescriptionListTerm>
8492
<DescriptionListDescription>
85-
<VulnerabilitySeverityStatus severity={vulnerability.severity} />
93+
{isRHIssuer ? (
94+
<VulnerabilitySeverityDetails severity={vulnerability.severity} cvssScore={cvssScore} />
95+
) : (
96+
<VulnerabilitySeverityBadge severity={vulnerability.severity} />
97+
)}
8698
</DescriptionListDescription>
8799
</DescriptionListGroup>
88100
<DescriptionListGroup>
@@ -117,9 +129,9 @@ const VulnerabilityDetailsDrawer = ({
117129
<Divider />
118130
</StackItem>
119131

120-
{hasReferences && (
132+
{primaryFinding && (
121133
<StackItem>
122-
<VulnerabilityReferences vulnerability={vulnerability} cveId={vulnerability.cveId} />
134+
<VulnerabilityReferences finding={primaryFinding} cveId={vulnerability.cveId} />
123135
</StackItem>
124136
)}
125137
</Stack>

libs/ui-components/src/components/SecurityOverview/VulnerabilityReferences.tsx

Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,40 @@
11
import * as React from 'react';
22
import { TFunction } from 'react-i18next';
3-
import { Stack, StackItem, Title } from '@patternfly/react-core';
3+
import { Content, Stack, StackItem, Title } from '@patternfly/react-core';
44

5-
import { Vulnerability, VulnerabilityGroup } from '@flightctl/types/alpha';
5+
import { Vulnerability, VulnerabilityGroupItem } from '@flightctl/types/alpha';
66
import { useTranslation } from '../../hooks/useTranslation';
77
import LearnMoreLink from '../common/LearnMoreLink';
8-
import { isVulnerabilityGroup } from '../../utils/vulnerabilities';
8+
import { isRedHatIssuer } from '../../utils/vulnerabilities';
99

10-
const RED_HAT_ISSUER = 'Red Hat';
11-
12-
const getVulnLinkText = (t: TFunction, advisoryId: string, issuer: string | undefined) => {
13-
if (issuer === RED_HAT_ISSUER) {
10+
const getVulnLinkText = (t: TFunction, advisoryId: string, isRHIssuer: boolean) => {
11+
if (isRHIssuer) {
1412
return t('{{ advisoryId }} - Red Hat Security Advisory', { advisoryId });
1513
}
1614
return t('CVE details - {{ advisoryId }}', { advisoryId });
1715
};
1816

1917
const VulnerabilityReferences = ({
20-
vulnerability,
18+
finding,
2119
cveId,
2220
}: {
23-
vulnerability: Vulnerability | VulnerabilityGroup;
21+
finding: Vulnerability | VulnerabilityGroupItem;
2422
cveId: string;
2523
}) => {
2624
const { t } = useTranslation();
27-
28-
let link: string | undefined;
29-
let advisoryId: string | undefined;
30-
let issuer: string | undefined;
31-
32-
if (isVulnerabilityGroup(vulnerability)) {
33-
link = vulnerability.findings[0].link;
34-
advisoryId = vulnerability.findings[0].advisoryId;
35-
issuer = vulnerability.findings[0].issuer;
36-
} else {
37-
link = vulnerability.link;
38-
advisoryId = vulnerability.advisoryId;
39-
issuer = vulnerability.issuer;
40-
}
41-
if (!link) {
25+
if (!finding.link) {
4226
return null;
4327
}
4428

29+
const isRHIssuer = isRedHatIssuer(finding.issuer);
4530
return (
4631
<Stack hasGutter>
4732
<StackItem>
4833
<Title headingLevel="h2">{t('References')}</Title>
4934
</StackItem>
5035
<StackItem>
51-
<LearnMoreLink link={link} text={getVulnLinkText(t, advisoryId || cveId, issuer)} />
36+
<LearnMoreLink link={finding.link} text={getVulnLinkText(t, finding.advisoryId || cveId, isRHIssuer)} />
37+
{isRHIssuer && <Content component="small">{t('(may show a different impact rating)')}</Content>}
5238
</StackItem>
5339
</Stack>
5440
);

libs/ui-components/src/components/Status/VulnerabilitySeverityStatus.tsx

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as React from 'react';
2+
import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons/dist/js/icons/outlined-question-circle-icon';
23

3-
import { Icon, Label } from '@patternfly/react-core';
4+
import { Button, Content, Flex, FlexItem, Icon, Label, Popover } from '@patternfly/react-core';
45
import { Vulnerability } from '@flightctl/types/alpha';
56

67
import { VULNERABILITY_SEVERITY_COLOR } from '../../utils/vulnerabilities';
@@ -9,14 +10,29 @@ import {
910
getVulnerabilitySeverityStatusItems,
1011
} from '../../utils/status/vulnerabilities';
1112
import { useTranslation } from '../../hooks/useTranslation';
13+
import LearnMoreLink from '../common/LearnMoreLink';
1214

1315
type VulnerabilitySeverityStatusProps = {
1416
severity: Vulnerability.severity;
17+
cvssScore?: number;
1518
};
1619

17-
const VulnerabilitySeverityStatus = ({ severity }: VulnerabilitySeverityStatusProps) => {
20+
const RED_HAT_CVSS_CLASSIFICATION_URL = 'https://access.redhat.com/security/updates/classification';
21+
22+
const CvssScorePopoverContent = () => {
1823
const { t } = useTranslation();
24+
return (
25+
<>
26+
{t(
27+
'This is the industry-standard CVSS base severity. Red Hat may assign a different impact rating for this CVE based on product-specific factors like default configurations and mitigations.',
28+
)}{' '}
29+
<LearnMoreLink link={RED_HAT_CVSS_CLASSIFICATION_URL} text={t('Learn more')} />
30+
</>
31+
);
32+
};
1933

34+
export const VulnerabilitySeverityBadge = ({ severity }: VulnerabilitySeverityStatusProps) => {
35+
const { t } = useTranslation();
2036
const statusItems = getVulnerabilitySeverityStatusItems(t);
2137
const item = statusItems.find((item) => item.id === severity) || defaultVulnerabilitySeverityStatusItem(t);
2238

@@ -43,4 +59,34 @@ const VulnerabilitySeverityStatus = ({ severity }: VulnerabilitySeverityStatusPr
4359
);
4460
};
4561

46-
export default VulnerabilitySeverityStatus;
62+
export const VulnerabilitySeverityDetails = ({ severity, cvssScore }: VulnerabilitySeverityStatusProps) => {
63+
const { t } = useTranslation();
64+
65+
return (
66+
<Flex gap={{ default: 'gapSm' }} alignItems={{ default: 'alignItemsCenter' }} flexWrap={{ default: 'wrap' }}>
67+
<FlexItem>
68+
<VulnerabilitySeverityBadge severity={severity} />
69+
</FlexItem>
70+
<FlexItem>
71+
{cvssScore !== undefined && (
72+
<Content component="small">{`(${t('CVSS {{ score }}', { score: cvssScore.toFixed(1) })})`}</Content>
73+
)}
74+
</FlexItem>
75+
<FlexItem>
76+
<Popover
77+
aria-label={t('CVSS base score help')}
78+
bodyContent={<CvssScorePopoverContent />}
79+
withFocusTrap
80+
triggerAction="click"
81+
>
82+
<Button
83+
icon={<OutlinedQuestionCircleIcon />}
84+
isInline
85+
variant="plain"
86+
aria-label={t('CVSS base score help')}
87+
/>
88+
</Popover>
89+
</FlexItem>
90+
</Flex>
91+
);
92+
};

libs/ui-components/src/utils/vulnerabilities.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import { TFunction } from 'react-i18next';
2-
import { CveCountsBySeverity, Vulnerability, VulnerabilityGroup } from '@flightctl/types/alpha';
2+
import { CveCountsBySeverity, Vulnerability, VulnerabilityGroup, VulnerabilityGroupItem } from '@flightctl/types/alpha';
33

44
const SeverityColorCritical = 'var(--pf-t--global--icon--color--severity--critical--default)';
55
const SeverityColorImportant = 'var(--pf-t--global--icon--color--severity--important--default)';
66
const SeverityColorModerate = 'var(--pf-t--global--icon--color--severity--moderate--default)';
77
const SeverityColorMinor = 'var(--pf-t--global--icon--color--severity--minor--default)';
88
const SeverityColorNone = 'var(--pf-t--global--icon--color--severity--none--default)';
99

10+
// Accepts different variations for "Red Hat" issuer detection
11+
const RED_HAT_ISSUER_PATTERN = /red\s*hat/i;
12+
1013
type Severity = Vulnerability.severity;
1114

1215
export const VULNERABILITY_SEVERITY_ORDER: Severity[] = [
@@ -33,6 +36,16 @@ export const isVulnerabilityGroup = (
3336
vulnerability: Vulnerability | VulnerabilityGroup,
3437
): vulnerability is VulnerabilityGroup => 'findings' in vulnerability;
3538

39+
export const isRedHatIssuer = (issuer?: string): boolean => !!issuer && RED_HAT_ISSUER_PATTERN.test(issuer);
40+
41+
// Prefer a Red Hat advisory finding when present; otherwise use the first finding.
42+
export const getPrimaryVulnerabilityGroupFinding = (
43+
findings: VulnerabilityGroupItem[],
44+
): VulnerabilityGroupItem | undefined => {
45+
const findingsWithLink = findings.filter((finding) => finding.link);
46+
return findingsWithLink.find((finding) => isRedHatIssuer(finding.issuer)) || findingsWithLink[0] || findings[0];
47+
};
48+
3649
export const getSeverityCountValue = (severity: Severity, counts: CveCountsBySeverity) => {
3750
switch (severity) {
3851
case Vulnerability.severity.CRITICAL:

0 commit comments

Comments
 (0)