Skip to content

Commit 4980cf9

Browse files
yezhu6chagong
andauthored
feat: add CVE checking in java upgrade promotion (#948)
* add CVE checking * fix comments * update changelog and fix comments * Remove patched version from CVE issue && fix commets * Remove patchedVersion checking * Fix copilot comments * Add pagination for CVE fetch from github * Fix comments --------- Co-authored-by: Changyong Gong <chagon@microsoft.com>
1 parent 3dc88d9 commit 4980cf9

File tree

7 files changed

+316
-47
lines changed

7 files changed

+316
-47
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ All notable changes to the "vscode-java-dependency" extension will be documented
44
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
55
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
66

7+
## 0.27.0
8+
9+
- feat - Add CVE checking to notify users to fix the critical/high-severity CVE issues in https://github.com/microsoft/vscode-java-dependency/pull/948
10+
711
## 0.26.5
812

913
- Enhancement - Register Context Provider after Java LS ready in https://github.com/microsoft/vscode-java-dependency/pull/939

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1111,6 +1111,7 @@
11111111
},
11121112
"dependencies": {
11131113
"@github/copilot-language-server": "^1.388.0",
1114+
"@octokit/rest": "^21.1.1",
11141115
"await-lock": "^2.2.2",
11151116
"fmtr": "^1.1.4",
11161117
"fs-extra": "^10.1.0",

src/upgrade/assessmentManager.ts

Lines changed: 48 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { Upgrade } from '../constants';
1010
import { buildPackageId } from './utility';
1111
import metadataManager from './metadataManager';
1212
import { sendInfo } from 'vscode-extension-telemetry-wrapper';
13+
import { batchGetCVEIssues } from './cve';
1314

1415
function packageNodeToDescription(node: INodeData): PackageDescription | null {
1516
const version = node.metaData?.["maven.version"];
@@ -121,51 +122,29 @@ function getDependencyIssue(pkg: PackageDescription): UpgradeIssue | null {
121122
return getUpgradeForDependency(version, supportedVersionDefinition, packageId);
122123
}
123124

124-
async function getDependencyIssues(projectNode: INodeData): Promise<UpgradeIssue[]> {
125-
const projectStructureData = await Jdtls.getPackageData({ kind: NodeKind.Project, projectUri: projectNode.uri });
126-
const packageContainerIssues = await Promise.allSettled(
127-
projectStructureData
128-
.filter(x => x.kind === NodeKind.Container)
129-
.map(async (packageContainer) => {
130-
const packageNodes = await Jdtls.getPackageData({
131-
kind: NodeKind.Container,
132-
projectUri: projectNode.uri,
133-
path: packageContainer.path,
134-
});
135-
const packages = packageNodes.map(packageNodeToDescription).filter((x): x is PackageDescription => Boolean(x));
136-
137-
const issues = packages.map(getDependencyIssue).filter((x): x is UpgradeIssue => Boolean(x));
138-
const versionRangeByGroupId = collectVersionRange(packages.filter(getPackageUpgradeMetadata));
139-
if (Object.keys(versionRangeByGroupId).length > 0) {
140-
sendInfo("", {
141-
operationName: "java.dependency.assessmentManager.getDependencyVersionRange",
142-
versionRangeByGroupId: JSON.stringify(versionRangeByGroupId),
143-
});
144-
}
145-
146-
return issues;
147-
})
148-
);
125+
async function getDependencyIssues(dependencies: PackageDescription[]): Promise<UpgradeIssue[]> {
149126

150-
return packageContainerIssues
151-
.map(x => {
152-
if (x.status === "fulfilled") {
153-
return x.value;
154-
}
127+
const issues = dependencies.map(getDependencyIssue).filter((x): x is UpgradeIssue => Boolean(x));
128+
const versionRangeByGroupId = collectVersionRange(dependencies.filter(pkg => getPackageUpgradeMetadata(pkg)));
129+
if (Object.keys(versionRangeByGroupId).length > 0) {
130+
sendInfo("", {
131+
operationName: "java.dependency.assessmentManager.getDependencyVersionRange",
132+
versionRangeByGroupId: JSON.stringify(versionRangeByGroupId),
133+
});
134+
}
155135

156-
sendInfo("", {
157-
operationName: "java.dependency.assessmentManager.getDependencyIssues.packageDataFailure",
158-
});
159-
return [];
160-
})
161-
.reduce((a, b) => [...a, ...b]);
136+
return issues;
162137
}
163138

164139
async function getProjectIssues(projectNode: INodeData): Promise<UpgradeIssue[]> {
165140
const issues: UpgradeIssue[] = [];
141+
const dependencies = await getAllDependencies(projectNode);
142+
issues.push(...await getCVEIssues(dependencies));
166143
issues.push(...getJavaIssues(projectNode));
167-
issues.push(...(await getDependencyIssues(projectNode)));
144+
issues.push(...await getDependencyIssues(dependencies));
145+
168146
return issues;
147+
169148
}
170149

171150
async function getWorkspaceIssues(workspaceFolderUri: string): Promise<UpgradeIssue[]> {
@@ -184,11 +163,42 @@ async function getWorkspaceIssues(workspaceFolderUri: string): Promise<UpgradeIs
184163
operationName: "java.dependency.assessmentManager.getWorkspaceIssues",
185164
});
186165
return [];
187-
}).reduce((a, b) => [...a, ...b]);
166+
}).flat();
188167

189168
return workspaceIssues;
190169
}
191170

171+
async function getAllDependencies(projectNode: INodeData): Promise<PackageDescription[]> {
172+
const projectStructureData = await Jdtls.getPackageData({ kind: NodeKind.Project, projectUri: projectNode.uri });
173+
const packageContainers = projectStructureData.filter(x => x.kind === NodeKind.Container);
174+
175+
const allPackages = await Promise.allSettled(
176+
packageContainers.map(async (packageContainer) => {
177+
const packageNodes = await Jdtls.getPackageData({
178+
kind: NodeKind.Container,
179+
projectUri: projectNode.uri,
180+
path: packageContainer.path,
181+
});
182+
return packageNodes.map(packageNodeToDescription).filter((x): x is PackageDescription => Boolean(x));
183+
})
184+
);
185+
186+
const fulfilled = allPackages.filter((x): x is PromiseFulfilledResult<PackageDescription[]> => x.status === "fulfilled");
187+
const failedPackageCount = allPackages.length - fulfilled.length;
188+
if (failedPackageCount > 0) {
189+
sendInfo("", {
190+
operationName: "java.dependency.assessmentManager.getAllDependencies.rejected",
191+
failedPackageCount: String(failedPackageCount),
192+
});
193+
}
194+
return fulfilled.map(x => x.value).flat();
195+
}
196+
197+
async function getCVEIssues(dependencies: PackageDescription[]): Promise<UpgradeIssue[]> {
198+
const gavCoordinates = dependencies.map(pkg => `${pkg.groupId}:${pkg.artifactId}:${pkg.version}`);
199+
return batchGetCVEIssues(gavCoordinates);
200+
}
201+
192202
export default {
193203
getWorkspaceIssues,
194204
};

src/upgrade/cve.ts

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import { UpgradeIssue, UpgradeReason } from "./type";
2+
import { Octokit } from "@octokit/rest";
3+
import * as semver from "semver";
4+
5+
/**
6+
* Severity levels ordered by criticality (higher number = more critical)
7+
* The official doc about the severity levels can be found at:
8+
* https://docs.github.com/en/rest/security-advisories/global-advisories?apiVersion=2022-11-28
9+
*/
10+
export enum Severity {
11+
unknown = 0,
12+
low = 1,
13+
medium = 2,
14+
high = 3,
15+
critical = 4,
16+
}
17+
18+
export interface CVE {
19+
id: string;
20+
ghsa_id: string;
21+
severity: keyof typeof Severity;
22+
summary: string;
23+
description: string;
24+
html_url: string;
25+
affectedDeps: {
26+
name?: string | null;
27+
vulVersions?: string | null;
28+
patchedVersion?: string | null;
29+
}[];
30+
}
31+
32+
export type CveUpgradeIssue = UpgradeIssue & {
33+
reason: UpgradeReason.CVE;
34+
severity: string;
35+
link: string;
36+
};
37+
38+
export async function batchGetCVEIssues(
39+
coordinates: string[]
40+
): Promise<CveUpgradeIssue[]> {
41+
// Split dependencies into smaller batches to avoid URL length limit
42+
const BATCH_SIZE = 30;
43+
const allCVEUpgradeIssues: CveUpgradeIssue[] = [];
44+
45+
// Process dependencies in batches
46+
for (let i = 0; i < coordinates.length; i += BATCH_SIZE) {
47+
const batchCoordinates = coordinates.slice(i, i + BATCH_SIZE);
48+
const cveUpgradeIssues = await getCveUpgradeIssues(batchCoordinates);
49+
allCVEUpgradeIssues.push(...cveUpgradeIssues);
50+
}
51+
52+
return allCVEUpgradeIssues;
53+
}
54+
55+
async function getCveUpgradeIssues(
56+
coordinates: string[]
57+
): Promise<CveUpgradeIssue[]> {
58+
if (coordinates.length === 0) {
59+
return [];
60+
}
61+
const deps = coordinates
62+
.map((d) => d.split(":", 3))
63+
.map((p) => ({ name: `${p[0]}:${p[1]}`, version: p[2] }))
64+
.filter((d) => d.version);
65+
66+
const depsCves = await fetchCves(deps);
67+
return mapCvesToUpgradeIssues(depsCves);
68+
}
69+
70+
async function fetchCves(deps: { name: string; version: string }[]) {
71+
if (deps.length === 0) {
72+
return [];
73+
}
74+
try {
75+
const allCves: CVE[] = await retrieveVulnerabilityData(deps);
76+
77+
if (allCves.length === 0) {
78+
return [];
79+
}
80+
// group the cves by coordinate
81+
const depsCves: { dep: string; version: string; cves: CVE[] }[] = [];
82+
83+
for (const dep of deps) {
84+
const depCves: CVE[] = allCves.filter((cve) =>
85+
isCveAffectingDep(cve, dep.name, dep.version)
86+
);
87+
88+
if (depCves.length < 1) {
89+
continue;
90+
}
91+
92+
depsCves.push({
93+
dep: dep.name,
94+
version: dep.version,
95+
cves: depCves,
96+
});
97+
}
98+
99+
return depsCves;
100+
} catch (error) {
101+
return [];
102+
}
103+
}
104+
105+
async function retrieveVulnerabilityData(
106+
deps: { name: string; version: string }[]
107+
) {
108+
if (deps.length === 0) {
109+
return [];
110+
}
111+
const octokit = new Octokit();
112+
113+
// Use paginate to fetch all pages of results
114+
const allAdvisories = await octokit.paginate(
115+
octokit.securityAdvisories.listGlobalAdvisories,
116+
{
117+
ecosystem: "maven",
118+
affects: deps.map((p) => `${p.name}@${p.version}`),
119+
direction: "asc",
120+
sort: "published",
121+
per_page: 100,
122+
}
123+
);
124+
125+
const allCves: CVE[] = allAdvisories
126+
.filter(
127+
(c) =>
128+
!c.withdrawn_at?.trim() &&
129+
(c.severity === "critical" || c.severity === "high")
130+
) // only consider critical and high severity CVEs
131+
.map((cve) => ({
132+
id: cve.cve_id || cve.ghsa_id,
133+
ghsa_id: cve.ghsa_id,
134+
severity: cve.severity,
135+
summary: cve.summary,
136+
description: cve.description || cve.summary,
137+
html_url: cve.html_url,
138+
affectedDeps: (cve.vulnerabilities ?? []).map((v) => ({
139+
name: v.package?.name,
140+
vulVersions: v.vulnerable_version_range,
141+
patchedVersion: v.first_patched_version,
142+
})),
143+
}));
144+
return allCves;
145+
}
146+
147+
function mapCvesToUpgradeIssues(
148+
depsCves: { dep: string; version: string; cves: CVE[] }[]
149+
) {
150+
if (depsCves.length === 0) {
151+
return [];
152+
}
153+
const upgradeIssues = depsCves.map((depCve) => {
154+
const mostCriticalCve = [...depCve.cves]
155+
.sort((a, b) => Severity[b.severity] - Severity[a.severity])[0];
156+
return {
157+
packageId: depCve.dep,
158+
packageDisplayName: depCve.dep,
159+
currentVersion: depCve.version || "unknown",
160+
name: `${mostCriticalCve.id || "CVE"}`,
161+
reason: UpgradeReason.CVE as const,
162+
suggestedVersion: {
163+
name: "",
164+
description: "",
165+
},
166+
severity: mostCriticalCve.severity,
167+
description:
168+
mostCriticalCve.description ||
169+
mostCriticalCve.summary ||
170+
"Security vulnerability detected",
171+
link: mostCriticalCve.html_url,
172+
};
173+
});
174+
return upgradeIssues;
175+
}
176+
177+
function isCveAffectingDep(
178+
cve: CVE,
179+
depName: string,
180+
depVersion: string
181+
): boolean {
182+
if (!cve.affectedDeps || cve.affectedDeps.length === 0) {
183+
return false;
184+
}
185+
return cve.affectedDeps.some((d) => {
186+
if (d.name !== depName || !d.vulVersions) {
187+
return false;
188+
}
189+
190+
return semver.satisfies(depVersion || "0.0.0", d.vulVersions);
191+
});
192+
}

0 commit comments

Comments
 (0)