Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ All notable changes to the "vscode-java-dependency" extension will be documented
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## 0.27.0

- 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

## 0.26.5

- Enhancement - Register Context Provider after Java LS ready in https://github.com/microsoft/vscode-java-dependency/pull/939
Expand Down
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
88 changes: 50 additions & 38 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 { batchGetCVEIssues } from './cve';

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

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;
})
);
async function getDependencyIssues(dependencies: PackageDescription[]): Promise<UpgradeIssue[]> {

return packageContainerIssues
.map(x => {
if (x.status === "fulfilled") {
return x.value;
}
const issues = dependencies.map(getDependencyIssue).filter((x): x is UpgradeIssue => Boolean(x));
const versionRangeByGroupId = collectVersionRange(dependencies.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[] = [];
const dependencies = await getAllDependencies(projectNode);
issues.push(...await getCVEIssues(dependencies));
issues.push(...getJavaIssues(projectNode));
issues.push(...(await getDependencyIssues(projectNode)));
issues.push(...await getDependencyIssues(dependencies));

return issues;

}

async function getWorkspaceIssues(workspaceFolderUri: string): Promise<UpgradeIssue[]> {
Expand All @@ -184,11 +163,44 @@ 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("", {
operationName: "java.dependency.assessmentManager.getAllDependencies.packageDataFailure",
});
return [];
})
.flat();
}

async function getCVEIssues(dependencies: PackageDescription[]): Promise<UpgradeIssue[]> {
const gavCoordinates = dependencies.map(pkg => `${pkg.groupId}:${pkg.artifactId}:${pkg.version}`);
return batchGetCVEIssues(gavCoordinates);
}

export default {
getWorkspaceIssues,
};
188 changes: 188 additions & 0 deletions src/upgrade/cve.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
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 enum Severity {
unknown = 0,
low = 1,
medium = 2,
high = 3,
critical = 4,
}

export interface CVE {
id: string;
ghsa_id: string;
severity: keyof typeof 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 batchGetCVEIssues(
coordinates: string[]
): Promise<CveUpgradeIssue[]> {
// Split dependencies into smaller batches to avoid URL length limit
const BATCH_SIZE = 30;
const allCVEUpgradeIssues: CveUpgradeIssue[] = [];

// Process dependencies in batches
for (let i = 0; i < coordinates.length; i += BATCH_SIZE) {
const batchCoordinates = coordinates.slice(i, i + BATCH_SIZE);
const cveUpgradeIssues = await getCveUpgradeIssues(batchCoordinates);
allCVEUpgradeIssues.push(...cveUpgradeIssues);
Comment on lines +47 to +49
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing error handling in batchGetCVEIssues. While fetchCves has a try-catch block, errors in getCveUpgradeIssues or mapCvesToUpgradeIssues will propagate and could cause the entire assessment process to fail. Consider wrapping the loop body in a try-catch:

for (let i = 0; i < coordinates.length; i += BATCH_SIZE) {
  try {
    const batchCoordinates = coordinates.slice(i, i + BATCH_SIZE);
    const cveUpgradeIssues = await getCveUpgradeIssues(batchCoordinates);
    allCVEUpgradeIssues.push(...cveUpgradeIssues);
  } catch (error) {
    // Log error but continue processing other batches
    console.error('Error fetching CVE issues for batch:', error);
  }
}
Suggested change
const batchCoordinates = coordinates.slice(i, i + BATCH_SIZE);
const cveUpgradeIssues = await getCveUpgradeIssues(batchCoordinates);
allCVEUpgradeIssues.push(...cveUpgradeIssues);
try {
const batchCoordinates = coordinates.slice(i, i + BATCH_SIZE);
const cveUpgradeIssues = await getCveUpgradeIssues(batchCoordinates);
allCVEUpgradeIssues.push(...cveUpgradeIssues);
} catch (error) {
// Log error but continue processing other batches
console.error('Error fetching CVE issues for batch:', error);
}

Copilot uses AI. Check for mistakes.
}

return allCVEUpgradeIssues;
}

async function getCveUpgradeIssues(
coordinates: string[]
): Promise<CveUpgradeIssue[]> {
if (coordinates.length === 0) {
return [];
}
const deps = coordinates
.map((d) => d.split(":", 3))
.map((p) => ({ name: `${p[0]}:${p[1]}`, version: p[2] }))
.filter((d) => d.version);

const depsCves = await fetchCves(deps);
return mapCvesToUpgradeIssues(depsCves);
}

async function fetchCves(deps: { name: string; version: string }[]) {
if (deps.length === 0) {
return [];
}
try {
const allCves: CVE[] = await retrieveVulnerabilityData(deps);

if (allCves.length === 0) {
return [];
}
// group the cves by coordinate
const depsCves: { dep: string; version: string; cves: CVE[] }[] = [];

for (const dep of deps) {
const depCves: CVE[] = allCves.filter((cve) =>
isCveAffectingDep(cve, dep.name, dep.version)
);

if (depCves.length < 1) {
continue;
}

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

return depsCves;
} catch (error) {
return [];
}
}

async function retrieveVulnerabilityData(
deps: { name: string; version: string }[]
) {
if (deps.length === 0) {
return [];
}
const octokit = new Octokit();

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
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing pagination handling for GitHub Security Advisories API. The per_page: 100 parameter limits results to 100 CVEs, but if there are more than 100 matching advisories, they will be silently ignored. This could miss critical vulnerabilities.

Consider implementing pagination to fetch all results:

let allData = [];
let page = 1;
let hasMore = true;

while (hasMore) {
  const response = await octokit.securityAdvisories.listGlobalAdvisories({
    ecosystem: "maven",
    affects: deps.map((p) => `${p.name}@${p.version}`),
    direction: "asc",
    sort: "published",
    per_page: 100,
    page
  });
  
  allData.push(...response.data);
  hasMore = response.data.length === 100;
  page++;
}
Suggested change
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
let allAdvisories: typeof response.data = [];
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await octokit.securityAdvisories.listGlobalAdvisories({
ecosystem: "maven",
affects: deps.map((p) => `${p.name}@${p.version}`),
direction: "asc",
sort: "published",
per_page: 100,
page,
});
allAdvisories.push(...response.data);
hasMore = response.data.length === 100;
page++;
}
const allCves: CVE[] = allAdvisories

Copilot uses AI. Check for mistakes.
.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,
})),
}));
return allCves;
}

function mapCvesToUpgradeIssues(
depsCves: { dep: string; version: string; cves: CVE[] }[]
) {
if (depsCves.length === 0) {
return [];
}
const upgradeIssues = depsCves.map((depCve) => {
const mostCriticalCve = [...depCve.cves]
.sort((a, b) => Severity[b.severity] - Severity[a.severity])[0];
return {
packageId: depCve.dep,
packageDisplayName: depCve.dep,
currentVersion: depCve.version || "unknown",
name: `${mostCriticalCve.id || "CVE"}`,
reason: UpgradeReason.CVE as const,
suggestedVersion: {
name: "",
description: "",
},
severity: mostCriticalCve.severity,
description:
mostCriticalCve.description ||
mostCriticalCve.summary ||
"Security vulnerability detected",
link: mostCriticalCve.html_url,
};
});
return upgradeIssues;
}

function isCveAffectingDep(
cve: CVE,
depName: string,
depVersion: string
): boolean {
if (!cve.affectedDeps || cve.affectedDeps.length === 0) {
return false;
}
return cve.affectedDeps.some((d) => {
if (d.name !== depName || !d.vulVersions) {
return false;
}

return semver.satisfies(depVersion || "0.0.0", d.vulVersions);
});
}
34 changes: 27 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 } 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 Now";
const BUTTON_TEXT_INSTALL_AND_UPGRADE = "Install Extension and Upgrade";
const BUTTON_TEXT_INSTALL_AND_FIX_CVE = "Install Extension and Fix";
const BUTTON_TEXT_NOT_NOW = "Not Now";

const SECONDS_IN_A_DAY = 24 * 60 * 60;
Expand Down Expand Up @@ -51,22 +54,39 @@ 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
);
notificationMessage = buildCVENotificationMessage(cveIssues, hasExtension);
} 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
Loading
Loading