diff --git a/test/dependencyLicenseEnricher.test.js b/test/dependencyLicenseEnricher.test.js new file mode 100644 index 0000000..9a97dde --- /dev/null +++ b/test/dependencyLicenseEnricher.test.js @@ -0,0 +1,68 @@ +const assert = require("assert"); +const vscode = require("vscode"); +const { + enrichLicenses, +} = require("../util/dependencyLicenseEnricher"); + +suite("dependencyLicenseEnricher", () => { + let originalGetConfiguration; + + setup(() => { + originalGetConfiguration = vscode.workspace.getConfiguration; + vscode.workspace.getConfiguration = () => ({ + get() { + return undefined; + }, + }); + }); + + teardown(() => { + vscode.workspace.getConfiguration = originalGetConfiguration; + }); + + test("classifies found dependencies using package index license metadata", async () => { + const dependencies = [ + { + name: "express", + version: "4.18.2", + format: "npm", + ecosystem: "npm", + cloudsmithStatus: "FOUND", + cloudsmithPackage: { + namespace: "workspace-a", + repository: "production-npm", + slug_perm: "pkg-1", + license: "MIT", + }, + }, + { + name: "copyleft-lib", + version: "1.0.0", + format: "npm", + ecosystem: "npm", + cloudsmithStatus: "FOUND", + cloudsmithPackage: { + namespace: "workspace-a", + repository: "production-npm", + slug_perm: "pkg-2", + spdx_license: "LGPL-2.1", + }, + }, + { + name: "missing-lib", + version: "1.0.0", + format: "npm", + ecosystem: "npm", + cloudsmithStatus: "NOT_FOUND", + }, + ]; + + const enriched = await enrichLicenses(dependencies); + + assert.strictEqual(enriched[0].license.classification, "permissive"); + assert.strictEqual(enriched[0].license.spdx, "MIT"); + assert.strictEqual(enriched[1].license.classification, "weak_copyleft"); + assert.strictEqual(enriched[1].license.spdx, "LGPL-2.1"); + assert.strictEqual(enriched[2].license, undefined); + }); +}); diff --git a/test/dependencyPolicyEnricher.test.js b/test/dependencyPolicyEnricher.test.js new file mode 100644 index 0000000..3e156ec --- /dev/null +++ b/test/dependencyPolicyEnricher.test.js @@ -0,0 +1,51 @@ +const assert = require("assert"); +const { + enrichPolicies, +} = require("../util/dependencyPolicyEnricher"); + +suite("dependencyPolicyEnricher", () => { + test("maps package index policy fields onto dependency objects", async () => { + const dependencies = [ + { + name: "spotipy", + version: "2.25.0", + format: "python", + ecosystem: "python", + cloudsmithStatus: "FOUND", + cloudsmithPackage: { + namespace: "workspace-a", + repository: "production-pypi", + slug_perm: "pkg-1", + status_str: "Quarantined", + deny_policy_violated: true, + policy_violated: true, + status_reason: "Blocked by policy", + }, + }, + { + name: "clean-lib", + version: "1.0.0", + format: "npm", + ecosystem: "npm", + cloudsmithStatus: "FOUND", + cloudsmithPackage: { + namespace: "workspace-a", + repository: "production-npm", + slug_perm: "pkg-2", + status_str: "Completed", + policy_violated: false, + }, + }, + ]; + + const enriched = await enrichPolicies(dependencies); + + assert.strictEqual(enriched[0].policy.violated, true); + assert.strictEqual(enriched[0].policy.denied, true); + assert.strictEqual(enriched[0].policy.quarantined, true); + assert.strictEqual(enriched[0].policy.status, "Quarantined"); + assert.strictEqual(enriched[0].policy.statusReason, "Blocked by policy"); + assert.strictEqual(enriched[1].policy.violated, false); + assert.strictEqual(enriched[1].policy.denied, false); + }); +}); diff --git a/test/dependencyVulnEnricher.test.js b/test/dependencyVulnEnricher.test.js new file mode 100644 index 0000000..936a609 --- /dev/null +++ b/test/dependencyVulnEnricher.test.js @@ -0,0 +1,183 @@ +const assert = require("assert"); +const { + clearVulnerabilityCache, + enrichVulnerabilities, + getVulnerabilityCacheSize, +} = require("../util/dependencyVulnEnricher"); + +suite("dependencyVulnEnricher", () => { + function createFoundDependency(slug, count = 1) { + return { + name: `pkg-${slug}`, + version: "1.0.0", + format: "maven", + ecosystem: "maven", + isDirect: false, + cloudsmithStatus: "FOUND", + cloudsmithPackage: { + namespace: "workspace-a", + repository: "production-maven", + slug_perm: slug, + vulnerability_scan_results_count: count, + max_severity: "High", + }, + }; + } + + setup(() => { + clearVulnerabilityCache(); + }); + + test("hydrates vulnerability summaries from the detail endpoint", async () => { + const calls = []; + const dependencies = [createFoundDependency("pkg-1", 2)]; + + const enriched = await enrichVulnerabilities(dependencies, "workspace-a", { + cloudsmithAPI: { + async getV2(endpoint) { + calls.push(endpoint); + return { + results: [ + { + vulnerability_id: "CVE-2024-1234", + severity: "High", + fix_version: "10.1.20", + }, + { + vulnerability_id: "CVE-2024-5678", + severity: "Medium", + }, + ], + }; + }, + }, + }); + + assert.deepStrictEqual(calls, [ + "vulnerabilities/workspace-a/production-maven/pkg-1/", + ]); + assert.strictEqual(enriched[0].vulnerabilities.count, 2); + assert.strictEqual(enriched[0].vulnerabilities.maxSeverity, "High"); + assert.deepStrictEqual(enriched[0].vulnerabilities.cveIds, [ + "CVE-2024-1234", + "CVE-2024-5678", + ]); + assert.strictEqual(enriched[0].vulnerabilities.hasFixAvailable, true); + assert.strictEqual(enriched[0].vulnerabilities.severityCounts.High, 1); + assert.strictEqual(enriched[0].vulnerabilities.severityCounts.Medium, 1); + }); + + test("skips vulnerability lookups for dependencies not found in Cloudsmith", async () => { + let calls = 0; + const dependencies = [ + { + name: "accepts", + version: "1.3.8", + format: "npm", + ecosystem: "npm", + isDirect: false, + cloudsmithStatus: "NOT_FOUND", + cloudsmithPackage: null, + }, + ]; + + const enriched = await enrichVulnerabilities(dependencies, "workspace-a", { + cloudsmithAPI: { + async getV2() { + calls += 1; + return { results: [] }; + }, + }, + }); + + assert.strictEqual(calls, 0); + assert.strictEqual(enriched[0].vulnerabilities, undefined); + }); + + test("deletes expired cache entries on read when the refresh does not replace them", async () => { + const originalNow = Date.now; + let now = 1_000; + + try { + Date.now = () => now; + + await enrichVulnerabilities([ + createFoundDependency("pkg-1"), + createFoundDependency("pkg-2"), + ], "workspace-a", { + cloudsmithAPI: { + async getV2() { + return { + results: [{ + vulnerability_id: "CVE-2024-1234", + severity: "High", + }], + }; + }, + }, + }); + + assert.strictEqual(getVulnerabilityCacheSize(), 2); + + now += 20 * 60 * 1000; + + const enriched = await enrichVulnerabilities([createFoundDependency("pkg-1")], "workspace-a", { + cloudsmithAPI: { + async getV2() { + return "temporarily unavailable"; + }, + }, + }); + + assert.strictEqual(getVulnerabilityCacheSize(), 1); + assert.strictEqual(enriched[0].vulnerabilities.count, 1); + assert.strictEqual(enriched[0].vulnerabilities.detailsLoaded, false); + } finally { + Date.now = originalNow; + } + }); + + test("prunes expired entries before inserting when the cache reaches the soft size cap", async () => { + const originalNow = Date.now; + let now = 1_000; + + try { + Date.now = () => now; + + const dependencies = Array.from({ length: 5000 }, (_, index) => createFoundDependency(`pkg-${index}`)); + await enrichVulnerabilities(dependencies, "workspace-a", { + cloudsmithAPI: { + async getV2() { + return { + results: [{ + vulnerability_id: "CVE-2024-1234", + severity: "High", + }], + }; + }, + }, + }); + + assert.strictEqual(getVulnerabilityCacheSize(), 5000); + + now += 20 * 60 * 1000; + + await enrichVulnerabilities([createFoundDependency("pkg-fresh")], "workspace-a", { + cloudsmithAPI: { + async getV2() { + return { + results: [{ + vulnerability_id: "CVE-2024-5678", + severity: "Medium", + }], + }; + }, + }, + }); + + assert.strictEqual(getVulnerabilityCacheSize(), 1); + } finally { + Date.now = originalNow; + } + }); +}); diff --git a/test/foundDependencyKey.test.js b/test/foundDependencyKey.test.js new file mode 100644 index 0000000..ecea5b2 --- /dev/null +++ b/test/foundDependencyKey.test.js @@ -0,0 +1,104 @@ +const assert = require("assert"); +const { getFoundDependencyKey } = require("../util/foundDependencyKey"); + +suite("foundDependencyKey", () => { + test("builds a lowercase trimmed key for valid found dependencies", () => { + const key = getFoundDependencyKey({ + cloudsmithPackage: { + namespace: " Workspace-A ", + repository: " Production-NPM ", + slug_perm: "Pkg-1", + }, + }); + + assert.strictEqual(key, "workspace-a:production-npm:pkg-1"); + }); + + test("uses slug fallbacks in priority order", () => { + assert.strictEqual(getFoundDependencyKey({ + cloudsmithPackage: { + namespace: "workspace-a", + repository: "repo-a", + slug_perm: "slug-perm", + slugPerm: "slug-perm-camel", + slug: "slug-value", + identifier: "identifier-value", + }, + }), "workspace-a:repo-a:slug-perm"); + + assert.strictEqual(getFoundDependencyKey({ + cloudsmithPackage: { + namespace: "workspace-a", + repository: "repo-a", + slugPerm: "slug-perm-camel", + slug: "slug-value", + identifier: "identifier-value", + }, + }), "workspace-a:repo-a:slug-perm-camel"); + + assert.strictEqual(getFoundDependencyKey({ + cloudsmithPackage: { + namespace: "workspace-a", + repository: "repo-a", + slug: "slug-value", + identifier: "identifier-value", + }, + }), "workspace-a:repo-a:slug-value"); + + assert.strictEqual(getFoundDependencyKey({ + cloudsmithPackage: { + namespace: "workspace-a", + repository: "repo-a", + identifier: "identifier-value", + }, + }), "workspace-a:repo-a:identifier-value"); + }); + + test("returns null for null or undefined dependency inputs", () => { + assert.strictEqual(getFoundDependencyKey(null), null); + assert.strictEqual(getFoundDependencyKey(undefined), null); + }); + + test("returns null when cloudsmithPackage is missing", () => { + assert.strictEqual(getFoundDependencyKey({}), null); + assert.strictEqual(getFoundDependencyKey({ cloudsmithPackage: null }), null); + }); + + test("returns null when namespace, repository, or slug fields are blank", () => { + assert.strictEqual(getFoundDependencyKey({ + cloudsmithPackage: { + namespace: " ", + repository: "repo-a", + slug_perm: "slug-a", + }, + }), null); + + assert.strictEqual(getFoundDependencyKey({ + cloudsmithPackage: { + namespace: "workspace-a", + repository: " ", + slug_perm: "slug-a", + }, + }), null); + + assert.strictEqual(getFoundDependencyKey({ + cloudsmithPackage: { + namespace: "workspace-a", + repository: "repo-a", + slug_perm: " ", + }, + }), null); + }); + + test("keeps the key format workspace repo slug with lowercase trimmed values", () => { + const key = getFoundDependencyKey({ + cloudsmithPackage: { + namespace: " Workspace-A ", + repository: " Repo-A ", + slugPerm: " slug-value ", + }, + }); + + assert.strictEqual(key, "workspace-a:repo-a:slug-value"); + }); +}); diff --git a/util/dependencyLicenseEnricher.js b/util/dependencyLicenseEnricher.js new file mode 100644 index 0000000..89cfd21 --- /dev/null +++ b/util/dependencyLicenseEnricher.js @@ -0,0 +1,75 @@ +// Copyright 2026 Cloudsmith Ltd. All rights reserved. +const { LicenseClassifier } = require("./licenseClassifier"); +const { getFoundDependencyKey } = require("./foundDependencyKey"); + +function toLicenseClassification(tier) { + switch (tier) { + case "permissive": + return "permissive"; + case "cautious": + return "weak_copyleft"; + case "restrictive": + return "restrictive"; + default: + return "unknown"; + } +} + +function buildLicensePatch(dependencies) { + const patchMap = new Map(); + + for (const dependency of Array.isArray(dependencies) ? dependencies : []) { + if (dependency.cloudsmithStatus !== "FOUND" || !dependency.cloudsmithPackage) { + continue; + } + + const key = getFoundDependencyKey(dependency); + if (!key || patchMap.has(key)) { + continue; + } + + const inspection = LicenseClassifier.inspect(dependency.cloudsmithPackage); + const spdx = inspection.spdxLicense || inspection.canonicalValue || inspection.displayValue || null; + + patchMap.set(key, { + spdx, + display: inspection.displayValue || spdx || null, + url: inspection.licenseUrl || null, + classification: toLicenseClassification(inspection.tier), + classifierTier: inspection.tier, + raw: inspection.rawLicense || inspection.raw || null, + overrideApplied: Boolean(inspection.overrideApplied), + }); + } + + return patchMap; +} + +function applyLicensePatch(dependencies, patchMap) { + return (Array.isArray(dependencies) ? dependencies : []).map((dependency) => { + const key = getFoundDependencyKey(dependency); + if (!key || !patchMap.has(key)) { + return dependency; + } + + return { + ...dependency, + license: patchMap.get(key), + }; + }); +} + +async function enrichLicenses(dependencies, options = {}) { + const onProgress = typeof options.onProgress === "function" ? options.onProgress : null; + const patchMap = buildLicensePatch(dependencies); + + if (onProgress && patchMap.size > 0) { + onProgress(new Map(patchMap), { stage: "licenses" }); + } + + return applyLicensePatch(dependencies, patchMap); +} + +module.exports = { + enrichLicenses, +}; diff --git a/util/dependencyPolicyEnricher.js b/util/dependencyPolicyEnricher.js new file mode 100644 index 0000000..e738c21 --- /dev/null +++ b/util/dependencyPolicyEnricher.js @@ -0,0 +1,67 @@ +// Copyright 2026 Cloudsmith Ltd. All rights reserved. +const { getFoundDependencyKey } = require("./foundDependencyKey"); + +function buildPolicyPatch(dependencies) { + const patchMap = new Map(); + + for (const dependency of Array.isArray(dependencies) ? dependencies : []) { + if (dependency.cloudsmithStatus !== "FOUND" || !dependency.cloudsmithPackage) { + continue; + } + + const key = getFoundDependencyKey(dependency); + if (!key || patchMap.has(key)) { + continue; + } + + const pkg = dependency.cloudsmithPackage; + const status = String(pkg.status_str || "").trim() || null; + const quarantined = status === "Quarantined"; + const denied = quarantined || Boolean(pkg.deny_policy_violated); + const violated = denied + || Boolean(pkg.policy_violated) + || Boolean(pkg.license_policy_violated) + || Boolean(pkg.vulnerability_policy_violated); + + patchMap.set(key, { + violated, + denied, + quarantined, + status, + statusReason: String(pkg.status_reason || "").trim() || null, + vulnerabilityViolated: Boolean(pkg.vulnerability_policy_violated), + licenseViolated: Boolean(pkg.license_policy_violated), + }); + } + + return patchMap; +} + +function applyPolicyPatch(dependencies, patchMap) { + return (Array.isArray(dependencies) ? dependencies : []).map((dependency) => { + const key = getFoundDependencyKey(dependency); + if (!key || !patchMap.has(key)) { + return dependency; + } + + return { + ...dependency, + policy: patchMap.get(key), + }; + }); +} + +async function enrichPolicies(dependencies, options = {}) { + const onProgress = typeof options.onProgress === "function" ? options.onProgress : null; + const patchMap = buildPolicyPatch(dependencies); + + if (onProgress && patchMap.size > 0) { + onProgress(new Map(patchMap), { stage: "policy" }); + } + + return applyPolicyPatch(dependencies, patchMap); +} + +module.exports = { + enrichPolicies, +}; diff --git a/util/dependencyVulnEnricher.js b/util/dependencyVulnEnricher.js new file mode 100644 index 0000000..d9683f3 --- /dev/null +++ b/util/dependencyVulnEnricher.js @@ -0,0 +1,442 @@ +// Copyright 2026 Cloudsmith Ltd. All rights reserved. +const { CloudsmithAPI } = require("./cloudsmithAPI"); +const { getFoundDependencyKey } = require("./foundDependencyKey"); + +const VULNERABILITY_CACHE_TTL_MS = 10 * 60 * 1000; +const VULNERABILITY_CACHE_MAX_SIZE = 5000; +const VULNERABILITY_CONCURRENCY = 10; +const vulnerabilityCache = new Map(); + +function severityRank(severity) { + switch (String(severity || "").trim().toLowerCase()) { + case "critical": + return 4; + case "high": + return 3; + case "medium": + return 2; + case "low": + return 1; + default: + return 0; + } +} + +function canonicalSeverity(severity) { + const normalized = String(severity || "").trim().toLowerCase(); + switch (normalized) { + case "critical": + return "Critical"; + case "high": + return "High"; + case "medium": + return "Medium"; + case "low": + return "Low"; + default: + return severity ? String(severity).trim() : null; + } +} + +function getIndicatorCount(packageModel) { + const rawCount = packageModel && ( + packageModel.vulnerability_scan_results_count + || packageModel.num_vulnerabilities + || packageModel.vulnerabilityCount + ); + const count = Number(rawCount); + return Number.isFinite(count) && count > 0 ? count : 0; +} + +function buildEmptySummary(packageModel) { + return { + count: 0, + maxSeverity: canonicalSeverity(packageModel && packageModel.max_severity), + cveIds: [], + hasFixAvailable: false, + severityCounts: {}, + entries: [], + detailsLoaded: false, + policyViolated: Boolean(packageModel && packageModel.vulnerability_policy_violated), + }; +} + +function buildIndicatorSummary(packageModel) { + const count = getIndicatorCount(packageModel); + if (count === 0) { + return buildEmptySummary(packageModel); + } + + const maxSeverity = canonicalSeverity(packageModel && packageModel.max_severity); + const severityCounts = {}; + if (maxSeverity) { + severityCounts[maxSeverity] = 1; + } + + return { + count, + maxSeverity, + cveIds: [], + hasFixAvailable: false, + severityCounts, + entries: [], + detailsLoaded: false, + policyViolated: Boolean(packageModel && packageModel.vulnerability_policy_violated), + }; +} + +function extractVulnerabilityEntries(payload) { + if (Array.isArray(payload)) { + return payload; + } + + if (!payload || typeof payload !== "object") { + return []; + } + + if (Array.isArray(payload.results)) { + return payload.results; + } + + if (Array.isArray(payload.vulnerabilities)) { + return payload.vulnerabilities; + } + + if (Array.isArray(payload.items)) { + return payload.items; + } + + if (!Array.isArray(payload.scans)) { + return []; + } + + const results = []; + for (const scan of payload.scans) { + if (!scan || !Array.isArray(scan.results)) { + continue; + } + results.push(...scan.results); + } + + return results; +} + +function extractFixVersion(entry) { + const candidates = [ + entry && entry.fixed_version, + entry && entry.fix_version, + entry && entry.fixedVersion, + entry && entry.fixVersion, + entry && entry.suggested_fix, + entry && entry.suggestedFix, + ]; + + if (entry && Array.isArray(entry.fixed_in_versions) && entry.fixed_in_versions.length > 0) { + candidates.push(entry.fixed_in_versions[0]); + } + + if (entry && Array.isArray(entry.fix_versions) && entry.fix_versions.length > 0) { + candidates.push(entry.fix_versions[0]); + } + + for (const candidate of candidates) { + const value = String(candidate || "").trim(); + if (value) { + return value; + } + } + + return null; +} + +function normalizeEntry(entry) { + const severity = canonicalSeverity( + entry && ( + entry.severity + || entry.severity_label + || entry.max_severity + ) + ) || "Unknown"; + + const cveId = String( + (entry && ( + entry.vulnerability_id + || entry.identifier + || entry.id + || entry.name + )) || "Unknown" + ).trim(); + + const fixVersion = extractFixVersion(entry); + + return { + cveId, + severity, + description: String(entry && (entry.title || entry.description || "") || "").trim(), + fixVersion, + hasFixAvailable: Boolean(fixVersion), + }; +} + +function summarizeEntries(entries, fallbackSummary) { + const severityCounts = {}; + const cveIds = []; + const normalizedEntries = []; + let maxSeverity = null; + let hasFixAvailable = false; + + for (const entry of entries.map(normalizeEntry)) { + normalizedEntries.push(entry); + if (!cveIds.includes(entry.cveId)) { + cveIds.push(entry.cveId); + } + severityCounts[entry.severity] = (severityCounts[entry.severity] || 0) + 1; + if (!maxSeverity || severityRank(entry.severity) > severityRank(maxSeverity)) { + maxSeverity = entry.severity; + } + if (entry.hasFixAvailable) { + hasFixAvailable = true; + } + } + + return { + count: normalizedEntries.length || fallbackSummary.count || 0, + maxSeverity: maxSeverity || fallbackSummary.maxSeverity || null, + cveIds, + hasFixAvailable, + severityCounts, + entries: normalizedEntries, + detailsLoaded: true, + policyViolated: Boolean(fallbackSummary.policyViolated), + }; +} + +function isCancellationRequested(cancellationToken) { + return Boolean(cancellationToken && cancellationToken.isCancellationRequested); +} + +function sortGroups(left, right) { + if (left.priority !== right.priority) { + return left.priority - right.priority; + } + + if (left.workspace !== right.workspace) { + return left.workspace.localeCompare(right.workspace); + } + + if (left.repo !== right.repo) { + return left.repo.localeCompare(right.repo); + } + + return left.name.localeCompare(right.name); +} + +function collectPackageGroups(dependencies) { + const groups = new Map(); + + for (const dependency of Array.isArray(dependencies) ? dependencies : []) { + if (dependency.cloudsmithStatus !== "FOUND" || !dependency.cloudsmithPackage) { + continue; + } + + const packageKey = getFoundDependencyKey(dependency); + if (!packageKey) { + continue; + } + + const existing = groups.get(packageKey); + const priority = dependency.isDirect ? 0 : 1; + + if (!existing) { + groups.set(packageKey, { + key: packageKey, + packageModel: dependency.cloudsmithPackage, + workspace: String(dependency.cloudsmithPackage.namespace || "").toLowerCase(), + repo: String(dependency.cloudsmithPackage.repository || "").toLowerCase(), + name: String(dependency.name || "").toLowerCase(), + priority, + }); + continue; + } + + if (priority < existing.priority) { + existing.priority = priority; + } + } + + return [...groups.values()].sort(sortGroups); +} + +async function runPool(items, concurrency, worker) { + const workers = []; + let index = 0; + const poolSize = Math.max(1, Math.min(concurrency, items.length || 1)); + + for (let workerIndex = 0; workerIndex < poolSize; workerIndex += 1) { + workers.push((async () => { + while (index < items.length) { + const item = items[index]; + index += 1; + if (item === undefined) { + break; + } + await worker(item); + } + })()); + } + + await Promise.all(workers); +} + +function pruneExpiredVulnerabilityCache(now = Date.now()) { + for (const [cacheKey, cacheEntry] of vulnerabilityCache.entries()) { + if (!cacheEntry || cacheEntry.expiresAt <= now) { + vulnerabilityCache.delete(cacheKey); + } + } +} + +function getCachedVulnerabilitySummary(packageKey) { + const cached = vulnerabilityCache.get(packageKey); + if (!cached) { + return null; + } + + if (cached.expiresAt > Date.now()) { + return cached.value; + } + + vulnerabilityCache.delete(packageKey); + return null; +} + +function setCachedVulnerabilitySummary(packageKey, value) { + if (vulnerabilityCache.size >= VULNERABILITY_CACHE_MAX_SIZE) { + pruneExpiredVulnerabilityCache(); + } + + vulnerabilityCache.set(packageKey, { + expiresAt: Date.now() + VULNERABILITY_CACHE_TTL_MS, + value, + }); +} + +async function fetchVulnerabilitySummary(api, packageModel, fallbackSummary, cancellationToken) { + const packageKey = getFoundDependencyKey({ cloudsmithPackage: packageModel }); + if (!packageKey) { + return fallbackSummary; + } + + const cachedValue = getCachedVulnerabilitySummary(packageKey); + if (cachedValue) { + return cachedValue; + } + + if (isCancellationRequested(cancellationToken)) { + return fallbackSummary; + } + + const workspace = encodeURIComponent(String(packageModel.namespace || "").trim()); + const repo = encodeURIComponent(String(packageModel.repository || "").trim()); + const identifier = encodeURIComponent(String( + packageModel.slug_perm + || packageModel.slugPerm + || packageModel.slug + || packageModel.identifier + || "" + ).trim()); + + if (!workspace || !repo || !identifier) { + return fallbackSummary; + } + + const response = await api.getV2(`vulnerabilities/${workspace}/${repo}/${identifier}/`); + if (typeof response === "string") { + return fallbackSummary; + } + + const summary = summarizeEntries(extractVulnerabilityEntries(response), fallbackSummary); + setCachedVulnerabilitySummary(packageKey, summary); + return summary; +} + +function applyVulnerabilityPatch(dependencies, patchMap) { + return (Array.isArray(dependencies) ? dependencies : []).map((dependency) => { + const packageKey = getFoundDependencyKey(dependency); + if (!packageKey || !patchMap.has(packageKey)) { + return dependency; + } + + return { + ...dependency, + vulnerabilities: patchMap.get(packageKey), + }; + }); +} + +async function enrichVulnerabilities(dependencies, workspace, options = {}) { + const onProgress = typeof options.onProgress === "function" ? options.onProgress : null; + const cancellationToken = options.cancellationToken || null; + const api = options.cloudsmithAPI || new CloudsmithAPI(options.context); + const groups = collectPackageGroups(dependencies); + const patchMap = new Map(); + const detailTargets = []; + + for (const group of groups) { + const indicatorSummary = buildIndicatorSummary(group.packageModel); + patchMap.set(group.key, indicatorSummary); + if (indicatorSummary.count > 0) { + detailTargets.push({ + ...group, + fallbackSummary: indicatorSummary, + }); + } + } + + if (onProgress && patchMap.size > 0) { + onProgress(new Map(patchMap), { + completed: 0, + total: detailTargets.length, + workspace, + stage: "initial", + }); + } + + let completed = 0; + await runPool(detailTargets, VULNERABILITY_CONCURRENCY, async (target) => { + if (isCancellationRequested(cancellationToken)) { + return; + } + + const summary = await fetchVulnerabilitySummary( + api, + target.packageModel, + target.fallbackSummary, + cancellationToken + ); + + patchMap.set(target.key, summary); + completed += 1; + + if (onProgress) { + onProgress(new Map([[target.key, summary]]), { + completed, + total: detailTargets.length, + workspace, + stage: "details", + }); + } + }); + + return applyVulnerabilityPatch(dependencies, patchMap); +} + +module.exports = { + clearVulnerabilityCache() { + vulnerabilityCache.clear(); + }, + enrichVulnerabilities, + getVulnerabilityCacheSize() { + return vulnerabilityCache.size; + }, +}; diff --git a/util/foundDependencyKey.js b/util/foundDependencyKey.js new file mode 100644 index 0000000..7ddc13f --- /dev/null +++ b/util/foundDependencyKey.js @@ -0,0 +1,21 @@ +// Copyright 2026 Cloudsmith Ltd. All rights reserved. +function getFoundDependencyKey(dependency) { + if (!dependency || !dependency.cloudsmithPackage) { + return null; + } + + const pkg = dependency.cloudsmithPackage; + const workspace = String(pkg.namespace || "").trim().toLowerCase(); + const repo = String(pkg.repository || "").trim().toLowerCase(); + const slug = String(pkg.slug_perm || pkg.slugPerm || pkg.slug || pkg.identifier || "").trim().toLowerCase(); + + if (!workspace || !repo || !slug) { + return null; + } + + return `${workspace}:${repo}:${slug}`; +} + +module.exports = { + getFoundDependencyKey, +};