Skip to content

Commit f65fffe

Browse files
committed
add wip vulnerability table
1 parent 5560df3 commit f65fffe

File tree

7 files changed

+263
-5
lines changed

7 files changed

+263
-5
lines changed

apps/site/components/Downloads/ReleaseModal/index.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,7 @@ const ReleaseModal: FC<ReleaseModalProps> = ({
4242
>
4343
{t.rich('components.releaseModal.unsupportedVersionWarning', {
4444
link: text => (
45-
<Link
46-
onClick={closeModal}
47-
href="/eol"
48-
>
45+
<Link onClick={closeModal} href="/eol">
4946
{text}
5047
</Link>
5148
),
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
'use client';
2+
3+
import { useTranslations } from 'next-intl';
4+
import type { FC } from 'react';
5+
import { use } from 'react';
6+
7+
import LinkWithArrow from '#site/components/LinkWithArrow';
8+
import { ReleaseModalContext } from '#site/providers/releaseModalProvider';
9+
import type { NodeRelease } from '#site/types';
10+
11+
type DetailsButtonProps = {
12+
versionData: NodeRelease;
13+
};
14+
15+
/**
16+
* TODO @bmuenzenmeyer adapt to vulnerabilities - this is currently a copy paste of the release - versionData should have everything we need :fingers_crossed:
17+
* @param param0
18+
* @returns
19+
*/
20+
const DetailsButton: FC<DetailsButtonProps> = ({ versionData }) => {
21+
const t = useTranslations('components.downloadReleasesTable');
22+
23+
const { openModal } = use(ReleaseModalContext);
24+
25+
return (
26+
<LinkWithArrow
27+
className="cursor-pointer"
28+
onClick={() => openModal(versionData)}
29+
>
30+
{t('details')}
31+
</LinkWithArrow>
32+
);
33+
};
34+
35+
export default DetailsButton;
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { getTranslations } from 'next-intl/server';
2+
import type { FC } from 'react';
3+
4+
import FormattedTime from '#site/components/Common/FormattedTime';
5+
import DetailsButton from '#site/components/MDX/EOL/DetailsButton';
6+
import provideVulnerabilities from '#site/next-data/providers/vulnerabilities';
7+
import getReleaseData from '#site/next-data/releaseData';
8+
9+
import VulnerabilityChips from './VulnerabilityChips';
10+
11+
const EOLTable: FC = async () => {
12+
const releaseData = await getReleaseData();
13+
const vulnerabilities = await provideVulnerabilities();
14+
const EOLReleases = releaseData.filter(
15+
release => release.status === 'End-of-life'
16+
);
17+
18+
const t = await getTranslations();
19+
20+
return (
21+
<table id="tbVulnerabilities" className="download-table full-width">
22+
<thead>
23+
<tr>
24+
{/* TODO @bmuenzenmeyer change these to new i18n keys */}
25+
<th>
26+
{t('components.downloadReleasesTable.version')} (
27+
{t('components.downloadReleasesTable.codename')})
28+
</th>
29+
<th>{t('components.downloadReleasesTable.lastUpdated')}</th>
30+
<th>Vulnerabilities</th>
31+
<th>Details</th>
32+
</tr>
33+
</thead>
34+
<tbody>
35+
{EOLReleases.map(release => (
36+
<tr key={release.major}>
37+
<td data-label="Version">
38+
v{release.major} {release.codename ? `(${release.codename})` : ''}
39+
</td>
40+
<td data-label="Date">
41+
<FormattedTime date={release.releaseDate} />
42+
</td>
43+
<td>
44+
<VulnerabilityChips
45+
vulnerabilities={vulnerabilities[release.major]}
46+
/>
47+
</td>
48+
<td className="download-table-last">
49+
<DetailsButton versionData={release} />
50+
</td>
51+
</tr>
52+
))}
53+
</tbody>
54+
</table>
55+
);
56+
};
57+
58+
export default EOLTable;
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import Badge from '@node-core/ui-components/Common/Badge';
2+
import type { FC } from 'react';
3+
4+
// mapping of vulnerability severities to UI labels and colors
5+
// TODO @bmuenzenmeyer we need i18n keys for these labels
6+
const severityLabels: Record<string, { label: string; kind: string }> = {
7+
low: { label: 'Low', kind: 'default' },
8+
medium: { label: 'Medium', kind: 'info' },
9+
high: { label: 'High', kind: 'warning' },
10+
critical: { label: 'Critical', kind: 'error' },
11+
};
12+
13+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
14+
// TODO @bmuenzenmeyer type
15+
const VulnerabilityChips: FC<{ vulnerabilities: Array<any> }> = ({
16+
vulnerabilities,
17+
}) => {
18+
if (!vulnerabilities || vulnerabilities.length === 0) {
19+
return <span>No vulnerabilities reported</span>;
20+
}
21+
22+
// group all vulnerabilities by severity
23+
const groupedVulnerabilities = vulnerabilities.reduce(
24+
(acc, vuln) => {
25+
const severity = vuln.severity.toLowerCase();
26+
if (!acc[severity]) {
27+
acc[severity] = [];
28+
}
29+
acc[severity].push(vuln);
30+
return acc;
31+
},
32+
{} as Record<string, Array<any>>
33+
);
34+
35+
return (
36+
<div className="vulnerability-chips">
37+
{Object.entries(groupedVulnerabilities)
38+
.sort((a, b) => {
39+
const severityOrder = ['critical', 'high', 'medium', 'low'];
40+
return severityOrder.indexOf(a[0]) - severityOrder.indexOf(b[0]);
41+
})
42+
.filter(([severity]) => severityLabels[severity]) // filter out unknown severities. there are 78+, with increasing frequency the older you get into our release or security governance
43+
.map(([severity, vulnerabilityCount]) => {
44+
const { label, kind } = severityLabels[severity];
45+
return (
46+
<Badge size="small" key={severity} kind={kind} className="mr-0.5">
47+
{label} ({vulnerabilityCount.length})
48+
</Badge>
49+
);
50+
})}
51+
</div>
52+
);
53+
};
54+
55+
export default VulnerabilityChips;
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { cache } from 'react';
2+
3+
interface Vulnerability {
4+
cve: Array<string>;
5+
ref?: string;
6+
vulnerable: string;
7+
patched?: string;
8+
description: string;
9+
overview: string;
10+
affectedEnvironments: Array<string>;
11+
severity: string;
12+
}
13+
14+
interface GroupedVulnerabilities {
15+
[majorVersion: string]: Array<Vulnerability>;
16+
}
17+
18+
/**
19+
* Vulnerability data looks like this:
20+
*
21+
*
22+
* [
23+
* "1": {
24+
"cve": [
25+
"CVE-2017-1000381"
26+
],
27+
"ref": "https://nodejs.org/en/blog/vulnerability/july-2017-security-releases/",
28+
"vulnerable": "8.x || 7.x || 4.x || 6.x || 5.x",
29+
"patched": "^8.1.4 || ^7.10.1 || ^4.8.4 || ^6.11.1",
30+
"description": "memory overread when parsing invalid NAPTR responses",
31+
"overview": "The c-ares function ares_parse_naptr_reply(), which is used for parsing NAPTR\nresponses, could be triggered to read memory outside of the given input buffer\nif the passed in DNS response packet was crafted in a particular way.\n\n",
32+
"affectedEnvironments": [
33+
"all"
34+
],
35+
"severity": "unknown"
36+
},
37+
"2": {
38+
"cve": [],
39+
"vulnerable": "4.x || 5.x || 6.x || 7.x || 8.x",
40+
"patched": "^4.8.4 || ^6.11.1 || ^7.10.1 || ^8.1.4",
41+
"description": "DoS possible in V8 object lookup",
42+
"overview": "Disable V8 snapshots - The hashseed embedded in the snapshot is\ncurrently the same for all runs of the binary. This opens node up to\ncollision attacks which could result in a Denial of Service. We have\ntemporarily disabled snapshots until a more robust solution is found\nFixed: Ali Ijaz Sheikh\nReported: Fedor Indutny\nref: https://nodejs.org/en/blog/vulnerability/july-2017-security-releases/\n\n",
43+
"affectedEnvironments": [
44+
"all"
45+
],
46+
"severity": "unknown"
47+
}
48+
]
49+
* TODO: @bmuenzenmeyer Better document
50+
* @param vulnerabilities
51+
*/
52+
53+
const groupVulnerabilitiesByMajor = (
54+
vulnerabilities: Array<Vulnerability>
55+
): GroupedVulnerabilities => {
56+
const grouped: GroupedVulnerabilities = {};
57+
58+
Object.values(vulnerabilities).forEach(vulnerability => {
59+
// `vulnerable` value can look as complicated as >=6.0.0 <6.2.0 || 5.x || 4.x
60+
// extract just the major versions, which is the unique first integer before any dot
61+
// e.g. 6, 5, 4
62+
// use a regex to get the integers and ignore the potential ranges
63+
// e.g. >=6.0.0 <6.2.0 will be parsed as 6
64+
// 5.x will be parsed as 5
65+
// 6.0.0 will be parsed as 6
66+
// 6.2.0 will be parsed as 6
67+
const majorVersions =
68+
vulnerability.vulnerable
69+
.match(/\d+/g)
70+
?.map(Number)
71+
.filter(major => !isNaN(major)) || [];
72+
73+
majorVersions.forEach(majorVersion => {
74+
if (!grouped[majorVersion]) {
75+
grouped[majorVersion] = [];
76+
}
77+
grouped[majorVersion].push(vulnerability);
78+
});
79+
});
80+
81+
return grouped;
82+
};
83+
84+
const provideVulnerabilities = cache(async () => {
85+
const data = await fetchVulnerabilities();
86+
87+
// group by major version
88+
const groupedData = groupVulnerabilitiesByMajor(data);
89+
90+
return groupedData;
91+
});
92+
93+
/**
94+
* TODO: @bmuenzenmeyer We need to extend the same data loading patterns at next-data/ to account for static generation.
95+
* This is a good place for others to extensively review what I've done here.
96+
*/
97+
export default provideVulnerabilities;
98+
99+
async function fetchVulnerabilities() {
100+
const response = await fetch(
101+
'https://raw.githubusercontent.com/nodejs/security-wg/main/vuln/core/index.json'
102+
);
103+
104+
if (!response.ok) {
105+
throw new Error('Failed to fetch vulnerabilities data');
106+
}
107+
108+
const data = await response.json();
109+
return data;
110+
}

apps/site/next.mdx.use.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import BadgeGroup from '@node-core/ui-components/Common/BadgeGroup';
55
import DownloadReleasesTable from './components/Downloads/DownloadReleasesTable';
66
import UpcomingMeetings from './components/MDX/Calendar/UpcomingMeetings';
77
import EOLAlertBox from './components/MDX/EOL/Alert';
8+
import EOLTable from './components/MDX/EOL/Table';
89
import WithBadgeGroup from './components/withBadgeGroup';
910
import WithBanner from './components/withBanner';
1011
import WithNodeRelease from './components/withNodeRelease';
@@ -28,4 +29,6 @@ export const mdxComponents = {
2829
UpcomingMeetings,
2930
// Renders an EOL alert
3031
EOLAlertBox,
32+
// Renders the EOL Table
33+
EOLTable,
3134
};

apps/site/pages/en/eol.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ The security implications are immediate and serious. For example, when new secur
2121

2222
## EOL Versions
2323

24-
`TO BE BUILT`
24+
<EOLTable />
2525

2626
## Commercial Support
2727

0 commit comments

Comments
 (0)