Skip to content

Commit 6e02353

Browse files
author
Ye Zhu
committed
add CVE checking
1 parent d2e7fdc commit 6e02353

File tree

6 files changed

+286
-48
lines changed

6 files changed

+286
-48
lines changed

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: 63 additions & 40 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 { batchGetCVEs } from './cve';
1314

1415
function packageNodeToDescription(node: INodeData): PackageDescription | null {
1516
const version = node.metaData?.["maven.version"];
@@ -122,50 +123,32 @@ function getDependencyIssue(pkg: PackageDescription): UpgradeIssue | null {
122123
}
123124

124125
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-
);
126+
const packages = await getAllDependencies(projectNode);
149127

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

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

164140
async function getProjectIssues(projectNode: INodeData): Promise<UpgradeIssue[]> {
165-
const issues: UpgradeIssue[] = [];
166-
issues.push(...getJavaIssues(projectNode));
167-
issues.push(...(await getDependencyIssues(projectNode)));
168-
return issues;
141+
const cveIssues = await getCVEIssues(projectNode);
142+
if (cveIssues.length > 0) {
143+
return cveIssues;
144+
}
145+
const javaIssues = getJavaIssues(projectNode);
146+
if (javaIssues.length > 0) {
147+
return javaIssues;
148+
}
149+
const dependencyIssues = await getDependencyIssues(projectNode);
150+
return dependencyIssues;
151+
169152
}
170153

171154
async function getWorkspaceIssues(workspaceFolderUri: string): Promise<UpgradeIssue[]> {
@@ -184,11 +167,51 @@ async function getWorkspaceIssues(workspaceFolderUri: string): Promise<UpgradeIs
184167
operationName: "java.dependency.assessmentManager.getWorkspaceIssues",
185168
});
186169
return [];
187-
}).reduce((a, b) => [...a, ...b]);
170+
}).flat();
188171

189172
return workspaceIssues;
190173
}
191174

175+
async function getAllDependencies(projectNode: INodeData): Promise<PackageDescription[]> {
176+
const projectStructureData = await Jdtls.getPackageData({ kind: NodeKind.Project, projectUri: projectNode.uri });
177+
const packageContainers = projectStructureData.filter(x => x.kind === NodeKind.Container);
178+
179+
const allPackages = await Promise.allSettled(
180+
packageContainers.map(async (packageContainer) => {
181+
const packageNodes = await Jdtls.getPackageData({
182+
kind: NodeKind.Container,
183+
projectUri: projectNode.uri,
184+
path: packageContainer.path,
185+
});
186+
return packageNodes.map(packageNodeToDescription).filter((x): x is PackageDescription => Boolean(x));
187+
})
188+
);
189+
190+
return allPackages
191+
.map(x => {
192+
if (x.status === "fulfilled") {
193+
return x.value;
194+
}
195+
sendInfo("", {
196+
operationName: "java.dependency.assessmentManager.getAllDependencies.packageDataFailure",
197+
});
198+
return [];
199+
})
200+
.flat();
201+
}
202+
203+
async function getCVEIssues(projectNode: INodeData): Promise<UpgradeIssue[]> {
204+
205+
// 1. Getting all dependencies from the project
206+
const dependencies = await getAllDependencies(projectNode);
207+
// 2. Convert to GAV (groupId:artifactId:version) format
208+
const gavCoordinates = dependencies.map(pkg => `${pkg.groupId}:${pkg.artifactId}:${pkg.version}`);
209+
// 3. Checking them against a CVE database to find known vulnerabilities
210+
const cveResults = await batchGetCVEs(gavCoordinates);
211+
212+
return cveResults;
213+
}
214+
192215
export default {
193216
getWorkspaceIssues,
194217
};

src/upgrade/cve.ts

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
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 const severityOrder = {
11+
critical: 4,
12+
high: 3,
13+
medium: 2,
14+
low: 1,
15+
unknown: 0,
16+
} as const;
17+
18+
export type Severity = keyof typeof severityOrder;
19+
20+
export interface CVE {
21+
id: string;
22+
ghsa_id: string;
23+
severity: Severity;
24+
summary: string;
25+
description: string;
26+
html_url: string;
27+
affectedDeps: {
28+
name?: string | null;
29+
vulVersions?: string | null;
30+
patchedVersion?: string | null;
31+
}[];
32+
}
33+
34+
export type CveUpgradeIssue = UpgradeIssue & { reason: UpgradeReason.CVE; severity: string; link: string };
35+
36+
export async function batchGetCVEs(
37+
coordinates: string[]
38+
): Promise<CveUpgradeIssue[]> {
39+
40+
// Split dependencies into smaller batches to avoid URL length limit
41+
const BATCH_SIZE = 30;
42+
const allCVEDeps: CveUpgradeIssue[] = [];
43+
44+
// Process dependencies in batches
45+
for (let i = 0; i < coordinates.length; i += BATCH_SIZE) {
46+
const batchCoordinates = coordinates.slice(i, i + BATCH_SIZE);
47+
const batchCVEDeps = await getCVEs(batchCoordinates);
48+
allCVEDeps.push(...batchCVEDeps);
49+
}
50+
51+
return allCVEDeps;
52+
}
53+
54+
async function getCVEs(
55+
coordinates: string[]
56+
): Promise<CveUpgradeIssue[]> {
57+
try {
58+
const octokit = new Octokit();
59+
60+
const deps = coordinates
61+
.map((d) => d.split(':', 3))
62+
.map((p) => ({ name: `${p[0]}:${p[1]}`, version: p[2] }))
63+
.filter((d) => d.version);
64+
const response = await octokit.securityAdvisories.listGlobalAdvisories({
65+
ecosystem: 'maven',
66+
affects: deps.map((p) => `${p.name}@${p.version}`),
67+
direction: 'asc',
68+
sort: 'published',
69+
per_page: 100
70+
});
71+
72+
const allCves: CVE[] = response.data
73+
.filter((c) => !c.withdrawn_at?.trim() &&
74+
(c.severity === 'critical' || c.severity === 'high')) // only consider critical and high severity CVEs
75+
.map((cve) => ({
76+
id: cve.cve_id || cve.ghsa_id,
77+
ghsa_id: cve.ghsa_id,
78+
severity: cve.severity,
79+
summary: cve.summary,
80+
description: cve.description || cve.summary,
81+
html_url: cve.html_url,
82+
affectedDeps: (cve.vulnerabilities ?? []).map((v) => ({
83+
name: v.package?.name,
84+
vulVersions: v.vulnerable_version_range,
85+
patchedVersion: v.first_patched_version
86+
}))
87+
}));
88+
89+
// group the cves by coordinate
90+
const depsCves: { dep: string; cves: CVE[]; minVersion?: string | null }[] = [];
91+
for (const dep of deps) {
92+
const depCves: CVE[] = allCves.filter((cve) => cve.affectedDeps.some((d) => d.name === dep.name));
93+
if (depCves.length < 1) {
94+
continue;
95+
}
96+
// find the min patched version for each coordinate
97+
let maxPatchedVersion: string | undefined | null;
98+
for (const cve of depCves) {
99+
const patchedVersion = cve.affectedDeps.find((d) => d.name === dep.name && d.patchedVersion)?.patchedVersion;
100+
const coercedPatchedVersion = semver.coerce(patchedVersion);
101+
const coercedMaxPatchedVersion = semver.coerce(maxPatchedVersion);
102+
if (
103+
!maxPatchedVersion ||
104+
(coercedPatchedVersion &&
105+
coercedMaxPatchedVersion &&
106+
semver.gt(coercedPatchedVersion, coercedMaxPatchedVersion))
107+
) {
108+
maxPatchedVersion = patchedVersion;
109+
}
110+
}
111+
112+
depsCves.push({
113+
dep: dep.name,
114+
cves: depCves,
115+
minVersion: maxPatchedVersion
116+
});
117+
}
118+
119+
const upgradeIssues = depsCves.map(depCve => {
120+
const currentDep = deps.find(d => d.name === depCve.dep);
121+
const mostCriticalCve = findMostCriticalCve(depCve.cves);
122+
return {
123+
packageId: depCve.dep,
124+
packageDisplayName: depCve.dep,
125+
currentVersion: currentDep?.version || 'unknown',
126+
name: `${mostCriticalCve.id || 'CVE'}`,
127+
reason: UpgradeReason.CVE as const,
128+
suggestedVersion: {
129+
name: depCve.minVersion || 'unknown',
130+
description: mostCriticalCve.description || mostCriticalCve.summary || 'Security vulnerability detected'
131+
},
132+
severity: mostCriticalCve.severity,
133+
description: mostCriticalCve.description || mostCriticalCve.summary || 'Security vulnerability detected',
134+
link: mostCriticalCve.html_url,
135+
};
136+
});
137+
return upgradeIssues;
138+
} catch (error) {
139+
throw error;
140+
}
141+
}
142+
143+
function findMostCriticalCve(depCves: CVE[]) {
144+
let mostCriticalSeverity: Severity = 'unknown';
145+
let mostCriticalCve = depCves[0];
146+
147+
for (const cve of depCves) {
148+
if (severityOrder[cve.severity] > severityOrder[mostCriticalSeverity]) {
149+
mostCriticalSeverity = cve.severity;
150+
mostCriticalCve = cve;
151+
}
152+
}
153+
return mostCriticalCve;
154+
}

src/upgrade/display/notificationManager.ts

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,21 @@
22
// Licensed under the MIT license.
33

44
import { commands, ExtensionContext, extensions, window } from "vscode";
5-
import type { IUpgradeIssuesRenderer, UpgradeIssue } from "../type";
6-
import { buildFixPrompt, buildNotificationMessage } from "../utility";
5+
import { UpgradeReason, type IUpgradeIssuesRenderer, type UpgradeIssue } from "../type";
6+
import { buildCVENotificationMessage, buildFixPrompt, buildNotificationMessage } from "../utility";
77
import { Commands } from "../../commands";
88
import { Settings } from "../../settings";
99
import { instrumentOperation, sendInfo } from "vscode-extension-telemetry-wrapper";
1010
import { ExtensionName } from "../../constants";
11+
import { CveUpgradeIssue, Severity, severityOrder } from "../cve";
1112

1213
const KEY_PREFIX = 'javaupgrade.notificationManager';
1314
const NEXT_SHOW_TS_KEY = `${KEY_PREFIX}.nextShowTs`;
1415

1516
const BUTTON_TEXT_UPGRADE = "Upgrade Now";
17+
const BUTTON_TEXT_FIX_CVE = "Fix CVE Issues";
1618
const BUTTON_TEXT_INSTALL_AND_UPGRADE = "Install Extension and Upgrade";
19+
const BUTTON_TEXT_INSTALL_AND_FIX_CVE = "Install Extension and Fix CVE Issues";
1720
const BUTTON_TEXT_NOT_NOW = "Not Now";
1821

1922
const SECONDS_IN_A_DAY = 24 * 60 * 60;
@@ -51,22 +54,45 @@ class NotificationManager implements IUpgradeIssuesRenderer {
5154

5255
const hasExtension = !!extensions.getExtension(ExtensionName.APP_MODERNIZATION_UPGRADE_FOR_JAVA);
5356
const prompt = buildFixPrompt(issue);
54-
const notificationMessage = buildNotificationMessage(issue, hasExtension);
55-
const upgradeButtonText = hasExtension ? BUTTON_TEXT_UPGRADE : BUTTON_TEXT_INSTALL_AND_UPGRADE;
5657

58+
let notificationMessage = "";
59+
let cveIssues: CveUpgradeIssue[] = [];
60+
if (issue.reason === UpgradeReason.CVE) {
61+
// Filter to only CVE issues and cast to CveUpgradeIssue[]
62+
cveIssues = issues.filter(
63+
(i): i is CveUpgradeIssue => i.reason === UpgradeReason.CVE
64+
);
65+
// Sort by severity (critical first, then high, etc.)
66+
cveIssues.sort((a, b) => {
67+
const severityA = (a.severity?.toLowerCase().trim() || 'unknown') as Severity;
68+
const severityB = (b.severity?.toLowerCase().trim() || 'unknown') as Severity;
69+
return (severityOrder[severityB] ?? 0) - (severityOrder[severityA] ?? 0);
70+
});
71+
notificationMessage = buildCVENotificationMessage(cveIssues);
72+
} else {
73+
notificationMessage = buildNotificationMessage(issue, hasExtension);
74+
}
75+
const upgradeButtonText = hasExtension ? BUTTON_TEXT_UPGRADE : BUTTON_TEXT_INSTALL_AND_UPGRADE;
76+
const fixCVEButtonText = hasExtension ? BUTTON_TEXT_FIX_CVE : BUTTON_TEXT_INSTALL_AND_FIX_CVE;
5777
sendInfo(operationId, {
5878
operationName: "java.dependency.upgradeNotification.show",
5979
});
80+
81+
const buttons = issue.reason === UpgradeReason.CVE
82+
? [fixCVEButtonText, BUTTON_TEXT_NOT_NOW]
83+
: [upgradeButtonText, BUTTON_TEXT_NOT_NOW];
84+
6085
const selection = await window.showInformationMessage(
61-
notificationMessage,
62-
upgradeButtonText,
63-
BUTTON_TEXT_NOT_NOW);
86+
notificationMessage,
87+
...buttons
88+
);
6489
sendInfo(operationId, {
6590
operationName: "java.dependency.upgradeNotification.runUpgrade",
6691
choice: selection ?? "",
6792
});
6893

6994
switch (selection) {
95+
case fixCVEButtonText:
7096
case upgradeButtonText: {
7197
commands.executeCommand(Commands.JAVA_UPGRADE_WITH_COPILOT, prompt);
7298
break;

src/upgrade/type.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ export type DependencyCheckItemEol = DependencyCheckItemBase & {
1010
};
1111
export type DependencyCheckItemJreTooOld = DependencyCheckItemBase & { reason: UpgradeReason.JRE_TOO_OLD };
1212
export type DependencyCheckItemDeprecated = DependencyCheckItemBase & { reason: UpgradeReason.DEPRECATED };
13-
export type DependencyCheckItem = (DependencyCheckItemEol | DependencyCheckItemJreTooOld | DependencyCheckItemDeprecated);
13+
export type DependencyCheckItemCve = DependencyCheckItemBase & { reason: UpgradeReason.CVE, severity: string, description: string, link: string };
14+
export type DependencyCheckItem = (DependencyCheckItemEol | DependencyCheckItemJreTooOld | DependencyCheckItemDeprecated | DependencyCheckItemCve);
1415
export type DependencyCheckMetadata = Record<string, DependencyCheckItem>;
1516

1617
export enum UpgradeReason {

0 commit comments

Comments
 (0)