Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1111,6 +1111,7 @@
},
"dependencies": {
"@github/copilot-language-server": "^1.388.0",
"@octokit/rest": "^21.1.1",
"await-lock": "^2.2.2",
"fmtr": "^1.1.4",
"fs-extra": "^10.1.0",
Expand Down
103 changes: 63 additions & 40 deletions src/upgrade/assessmentManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Upgrade } from '../constants';
import { buildPackageId } from './utility';
import metadataManager from './metadataManager';
import { sendInfo } from 'vscode-extension-telemetry-wrapper';
import { batchGetCVEs } from './cve';

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

async function getDependencyIssues(projectNode: INodeData): Promise<UpgradeIssue[]> {
const projectStructureData = await Jdtls.getPackageData({ kind: NodeKind.Project, projectUri: projectNode.uri });
const packageContainerIssues = await Promise.allSettled(
projectStructureData
.filter(x => x.kind === NodeKind.Container)
.map(async (packageContainer) => {
const packageNodes = await Jdtls.getPackageData({
kind: NodeKind.Container,
projectUri: projectNode.uri,
path: packageContainer.path,
});
const packages = packageNodes.map(packageNodeToDescription).filter((x): x is PackageDescription => Boolean(x));

const issues = packages.map(getDependencyIssue).filter((x): x is UpgradeIssue => Boolean(x));
const versionRangeByGroupId = collectVersionRange(packages.filter(getPackageUpgradeMetadata));
if (Object.keys(versionRangeByGroupId).length > 0) {
sendInfo("", {
operationName: "java.dependency.assessmentManager.getDependencyVersionRange",
versionRangeByGroupId: JSON.stringify(versionRangeByGroupId),
});
}

return issues;
})
);
const packages = await getAllDependencies(projectNode);

return packageContainerIssues
.map(x => {
if (x.status === "fulfilled") {
return x.value;
}
const issues = packages.map(getDependencyIssue).filter((x): x is UpgradeIssue => Boolean(x));
const versionRangeByGroupId = collectVersionRange(packages.filter(pkg => getPackageUpgradeMetadata(pkg)));
if (Object.keys(versionRangeByGroupId).length > 0) {
sendInfo("", {
operationName: "java.dependency.assessmentManager.getDependencyVersionRange",
versionRangeByGroupId: JSON.stringify(versionRangeByGroupId),
});
}

sendInfo("", {
operationName: "java.dependency.assessmentManager.getDependencyIssues.packageDataFailure",
});
return [];
})
.reduce((a, b) => [...a, ...b]);
return issues;
}

async function getProjectIssues(projectNode: INodeData): Promise<UpgradeIssue[]> {
const issues: UpgradeIssue[] = [];
issues.push(...getJavaIssues(projectNode));
issues.push(...(await getDependencyIssues(projectNode)));
return issues;
const cveIssues = await getCVEIssues(projectNode);
Comment thread
yezhu6 marked this conversation as resolved.
Outdated
if (cveIssues.length > 0) {
return cveIssues;
Comment thread
yezhu6 marked this conversation as resolved.
Outdated
}
const javaIssues = getJavaIssues(projectNode);
if (javaIssues.length > 0) {
return javaIssues;
}
const dependencyIssues = await getDependencyIssues(projectNode);
return dependencyIssues;

}

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

return workspaceIssues;
}

async function getAllDependencies(projectNode: INodeData): Promise<PackageDescription[]> {
const projectStructureData = await Jdtls.getPackageData({ kind: NodeKind.Project, projectUri: projectNode.uri });
const packageContainers = projectStructureData.filter(x => x.kind === NodeKind.Container);

const allPackages = await Promise.allSettled(
packageContainers.map(async (packageContainer) => {
const packageNodes = await Jdtls.getPackageData({
kind: NodeKind.Container,
projectUri: projectNode.uri,
path: packageContainer.path,
});
return packageNodes.map(packageNodeToDescription).filter((x): x is PackageDescription => Boolean(x));
})
);

return allPackages
.map(x => {
if (x.status === "fulfilled") {
return x.value;
}
sendInfo("", {
Comment thread
yezhu6 marked this conversation as resolved.
Outdated
operationName: "java.dependency.assessmentManager.getAllDependencies.packageDataFailure",
});
return [];
})
.flat();
}

async function getCVEIssues(projectNode: INodeData): Promise<UpgradeIssue[]> {

// 1. Getting all dependencies from the project
const dependencies = await getAllDependencies(projectNode);
// 2. Convert to GAV (groupId:artifactId:version) format
const gavCoordinates = dependencies.map(pkg => `${pkg.groupId}:${pkg.artifactId}:${pkg.version}`);
// 3. Checking them against a CVE database to find known vulnerabilities
const cveResults = await batchGetCVEs(gavCoordinates);

return cveResults;
}

export default {
getWorkspaceIssues,
};
154 changes: 154 additions & 0 deletions src/upgrade/cve.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { UpgradeIssue, UpgradeReason } from "./type";
import { Octokit } from "@octokit/rest";
import * as semver from 'semver';

/**
* Severity levels ordered by criticality (higher number = more critical)
* The official doc about the severity levels can be found at:
* https://docs.github.com/en/rest/security-advisories/global-advisories?apiVersion=2022-11-28
*/
export const severityOrder = {
critical: 4,
high: 3,
medium: 2,
low: 1,
unknown: 0,
} as const;

export type Severity = keyof typeof severityOrder;
Comment thread
yezhu6 marked this conversation as resolved.
Outdated

export interface CVE {
id: string;
ghsa_id: string;
severity: Severity;
summary: string;
description: string;
html_url: string;
affectedDeps: {
name?: string | null;
vulVersions?: string | null;
patchedVersion?: string | null;
}[];
}

export type CveUpgradeIssue = UpgradeIssue & { reason: UpgradeReason.CVE; severity: string; link: string };

export async function batchGetCVEs(
coordinates: string[]
): Promise<CveUpgradeIssue[]> {

// Split dependencies into smaller batches to avoid URL length limit
const BATCH_SIZE = 30;
const allCVEDeps: CveUpgradeIssue[] = [];

// Process dependencies in batches
for (let i = 0; i < coordinates.length; i += BATCH_SIZE) {
const batchCoordinates = coordinates.slice(i, i + BATCH_SIZE);
const batchCVEDeps = await getCVEs(batchCoordinates);
allCVEDeps.push(...batchCVEDeps);
}

return allCVEDeps;
}

async function getCVEs(
coordinates: string[]
): Promise<CveUpgradeIssue[]> {
try {
const octokit = new Octokit();

const deps = coordinates
.map((d) => d.split(':', 3))
.map((p) => ({ name: `${p[0]}:${p[1]}`, version: p[2] }))
.filter((d) => d.version);
const response = await octokit.securityAdvisories.listGlobalAdvisories({
ecosystem: 'maven',
affects: deps.map((p) => `${p.name}@${p.version}`),
direction: 'asc',
sort: 'published',
per_page: 100
});

const allCves: CVE[] = response.data
.filter((c) => !c.withdrawn_at?.trim() &&
(c.severity === 'critical' || c.severity === 'high')) // only consider critical and high severity CVEs
.map((cve) => ({
id: cve.cve_id || cve.ghsa_id,
ghsa_id: cve.ghsa_id,
severity: cve.severity,
summary: cve.summary,
description: cve.description || cve.summary,
html_url: cve.html_url,
affectedDeps: (cve.vulnerabilities ?? []).map((v) => ({
name: v.package?.name,
vulVersions: v.vulnerable_version_range,
patchedVersion: v.first_patched_version
}))
}));

// group the cves by coordinate
const depsCves: { dep: string; cves: CVE[]; minVersion?: string | null }[] = [];
for (const dep of deps) {
const depCves: CVE[] = allCves.filter((cve) => cve.affectedDeps.some((d) => d.name === dep.name));
if (depCves.length < 1) {
continue;
}
// find the min patched version for each coordinate
let maxPatchedVersion: string | undefined | null;
for (const cve of depCves) {
const patchedVersion = cve.affectedDeps.find((d) => d.name === dep.name && d.patchedVersion)?.patchedVersion;
const coercedPatchedVersion = semver.coerce(patchedVersion);
const coercedMaxPatchedVersion = semver.coerce(maxPatchedVersion);
if (
!maxPatchedVersion ||
(coercedPatchedVersion &&
coercedMaxPatchedVersion &&
semver.gt(coercedPatchedVersion, coercedMaxPatchedVersion))
) {
maxPatchedVersion = patchedVersion;
}
}

depsCves.push({
dep: dep.name,
cves: depCves,
minVersion: maxPatchedVersion
});
}

const upgradeIssues = depsCves.map(depCve => {
const currentDep = deps.find(d => d.name === depCve.dep);
const mostCriticalCve = findMostCriticalCve(depCve.cves);
return {
packageId: depCve.dep,
packageDisplayName: depCve.dep,
currentVersion: currentDep?.version || 'unknown',
name: `${mostCriticalCve.id || 'CVE'}`,
reason: UpgradeReason.CVE as const,
suggestedVersion: {
name: depCve.minVersion || 'unknown',
description: mostCriticalCve.description || mostCriticalCve.summary || 'Security vulnerability detected'
},
severity: mostCriticalCve.severity,
description: mostCriticalCve.description || mostCriticalCve.summary || 'Security vulnerability detected',
link: mostCriticalCve.html_url,
};
});
Comment thread
yezhu6 marked this conversation as resolved.
Outdated
return upgradeIssues;
} catch (error) {
throw error;
}
}

function findMostCriticalCve(depCves: CVE[]) {
Comment thread
yezhu6 marked this conversation as resolved.
Outdated
let mostCriticalSeverity: Severity = 'unknown';
let mostCriticalCve = depCves[0];

for (const cve of depCves) {
if (severityOrder[cve.severity] > severityOrder[mostCriticalSeverity]) {
mostCriticalSeverity = cve.severity;
mostCriticalCve = cve;
}
}
return mostCriticalCve;
}
40 changes: 33 additions & 7 deletions src/upgrade/display/notificationManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,21 @@
// Licensed under the MIT license.

import { commands, ExtensionContext, extensions, window } from "vscode";
import type { IUpgradeIssuesRenderer, UpgradeIssue } from "../type";
import { buildFixPrompt, buildNotificationMessage } from "../utility";
import { UpgradeReason, type IUpgradeIssuesRenderer, type UpgradeIssue } from "../type";
import { buildCVENotificationMessage, buildFixPrompt, buildNotificationMessage } from "../utility";
import { Commands } from "../../commands";
import { Settings } from "../../settings";
import { instrumentOperation, sendInfo } from "vscode-extension-telemetry-wrapper";
import { ExtensionName } from "../../constants";
import { CveUpgradeIssue, Severity, severityOrder } from "../cve";

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

const BUTTON_TEXT_UPGRADE = "Upgrade Now";
const BUTTON_TEXT_FIX_CVE = "Fix CVE Issues";
Comment thread
wangmingliang-ms marked this conversation as resolved.
Outdated
const BUTTON_TEXT_INSTALL_AND_UPGRADE = "Install Extension and Upgrade";
const BUTTON_TEXT_INSTALL_AND_FIX_CVE = "Install Extension and Fix CVE Issues";
Comment thread
yezhu6 marked this conversation as resolved.
Outdated
const BUTTON_TEXT_NOT_NOW = "Not Now";

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

const hasExtension = !!extensions.getExtension(ExtensionName.APP_MODERNIZATION_UPGRADE_FOR_JAVA);
const prompt = buildFixPrompt(issue);
const notificationMessage = buildNotificationMessage(issue, hasExtension);
const upgradeButtonText = hasExtension ? BUTTON_TEXT_UPGRADE : BUTTON_TEXT_INSTALL_AND_UPGRADE;

let notificationMessage = "";
let cveIssues: CveUpgradeIssue[] = [];
if (issue.reason === UpgradeReason.CVE) {
// Filter to only CVE issues and cast to CveUpgradeIssue[]
cveIssues = issues.filter(
(i): i is CveUpgradeIssue => i.reason === UpgradeReason.CVE
);
// Sort by severity (critical first, then high, etc.)
cveIssues.sort((a, b) => {
const severityA = (a.severity?.toLowerCase().trim() || 'unknown') as Severity;
const severityB = (b.severity?.toLowerCase().trim() || 'unknown') as Severity;
return (severityOrder[severityB] ?? 0) - (severityOrder[severityA] ?? 0);
});
Comment thread
yezhu6 marked this conversation as resolved.
Outdated
notificationMessage = buildCVENotificationMessage(cveIssues);
} else {
notificationMessage = buildNotificationMessage(issue, hasExtension);
}
const upgradeButtonText = hasExtension ? BUTTON_TEXT_UPGRADE : BUTTON_TEXT_INSTALL_AND_UPGRADE;
const fixCVEButtonText = hasExtension ? BUTTON_TEXT_FIX_CVE : BUTTON_TEXT_INSTALL_AND_FIX_CVE;
sendInfo(operationId, {
operationName: "java.dependency.upgradeNotification.show",
});

const buttons = issue.reason === UpgradeReason.CVE
? [fixCVEButtonText, BUTTON_TEXT_NOT_NOW]
: [upgradeButtonText, BUTTON_TEXT_NOT_NOW];

const selection = await window.showInformationMessage(
notificationMessage,
upgradeButtonText,
BUTTON_TEXT_NOT_NOW);
notificationMessage,
...buttons
);
sendInfo(operationId, {
operationName: "java.dependency.upgradeNotification.runUpgrade",
choice: selection ?? "",
});

switch (selection) {
case fixCVEButtonText:
case upgradeButtonText: {
commands.executeCommand(Commands.JAVA_UPGRADE_WITH_COPILOT, prompt);
break;
Expand Down
3 changes: 2 additions & 1 deletion src/upgrade/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ export type DependencyCheckItemEol = DependencyCheckItemBase & {
};
export type DependencyCheckItemJreTooOld = DependencyCheckItemBase & { reason: UpgradeReason.JRE_TOO_OLD };
export type DependencyCheckItemDeprecated = DependencyCheckItemBase & { reason: UpgradeReason.DEPRECATED };
export type DependencyCheckItem = (DependencyCheckItemEol | DependencyCheckItemJreTooOld | DependencyCheckItemDeprecated);
export type DependencyCheckItemCve = DependencyCheckItemBase & { reason: UpgradeReason.CVE, severity: string, description: string, link: string };
export type DependencyCheckItem = (DependencyCheckItemEol | DependencyCheckItemJreTooOld | DependencyCheckItemDeprecated | DependencyCheckItemCve);
export type DependencyCheckMetadata = Record<string, DependencyCheckItem>;

export enum UpgradeReason {
Expand Down
Loading
Loading