diff --git a/models/dependencyHealthNode.js b/models/dependencyHealthNode.js
index 27b99ad..a087681 100644
--- a/models/dependencyHealthNode.js
+++ b/models/dependencyHealthNode.js
@@ -1,259 +1,568 @@
-// Dependency health node treeview - represents a single dependency from the project manifest
-// cross-referenced against Cloudsmith
-
+// Copyright 2026 Cloudsmith Ltd. All rights reserved.
const vscode = require("vscode");
const { LicenseClassifier } = require("../util/licenseClassifier");
+const { getFormatIconPath } = require("../util/formatIcons");
+const { canonicalFormat } = require("../util/packageNameNormalizer");
class DependencyHealthNode {
- /**
- * @param {{name: string, version: string, devDependency: boolean, format: string}} dep
- * Parsed dependency from the project manifest.
- * @param {Object|null} cloudsmithMatch
- * Matching package from Cloudsmith API, or null if not found.
- * @param {vscode.ExtensionContext} context
- */
- constructor(dep, cloudsmithMatch, context) {
- this.context = context;
+ constructor(dep, cloudsmithMatchOrContext, maybeContext, maybeOptions) {
+ const hasExplicitCloudsmithMatch = arguments.length >= 3
+ || (
+ cloudsmithMatchOrContext
+ && typeof cloudsmithMatchOrContext === "object"
+ && (
+ Object.prototype.hasOwnProperty.call(cloudsmithMatchOrContext, "status_str")
+ || Object.prototype.hasOwnProperty.call(cloudsmithMatchOrContext, "slug_perm")
+ || Object.prototype.hasOwnProperty.call(cloudsmithMatchOrContext, "namespace")
+ )
+ );
+
+ this.context = hasExplicitCloudsmithMatch ? maybeContext : cloudsmithMatchOrContext;
+ this.options = hasExplicitCloudsmithMatch ? (maybeOptions || {}) : (maybeContext || {});
this.name = dep.name;
this.declaredVersion = dep.version;
- this.format = dep.format;
- this.isDev = dep.devDependency;
- this.isDirect = dep.isDirect !== false; // default to direct if not specified
- this.cloudsmithMatch = cloudsmithMatch;
-
- // Derive state from the Cloudsmith match
+ this.format = dep.format || canonicalFormat(dep.ecosystem);
+ this.ecosystem = dep.ecosystem || this.format;
+ this.sourceFile = dep.sourceFile || null;
+ this.isDev = Boolean(dep.devDependency || dep.isDevelopmentDependency);
+ this.isDirect = dep.isDirect !== false;
+ this.parent = dep.parent || (Array.isArray(dep.parentChain) ? dep.parentChain[dep.parentChain.length - 1] : null);
+ this.parentChain = Array.isArray(dep.parentChain) ? dep.parentChain.slice() : [];
+ this.transitives = Array.isArray(dep.transitives) ? dep.transitives.slice() : [];
+ this.cloudsmithMatch = dep.cloudsmithPackage
+ || dep.cloudsmithMatch
+ || (hasExplicitCloudsmithMatch ? cloudsmithMatchOrContext : null);
+ this.cloudsmithStatus = dep.cloudsmithStatus || (this.cloudsmithMatch ? "FOUND" : null);
+ this.vulnerabilities = dep.vulnerabilities || null;
+ this.licenseData = dep.license || null;
+ this.policy = dep.policy || null;
+ this.upstreamStatus = dep.upstreamStatus || null;
+ this.upstreamDetail = dep.upstreamDetail || null;
+ this._childMode = this.options.childMode || "details";
+ this._treeChildren = Array.isArray(this.options.treeChildren) ? this.options.treeChildren.slice() : [];
+ this._duplicateReference = Boolean(this.options.duplicateReference);
+ this._firstOccurrencePath = this.options.firstOccurrencePath || null;
+ this._dimmedForFilter = Boolean(this.options.dimmedForFilter);
+ this._treeChildFactory = typeof this.options.treeChildFactory === "function"
+ ? this.options.treeChildFactory
+ : null;
+ this.licenseInfo = this._deriveLicenseInfo();
this.state = this._deriveState();
- // Store fields from the Cloudsmith match for command compatibility
- if (cloudsmithMatch) {
- this.namespace = cloudsmithMatch.namespace;
- this.repository = cloudsmithMatch.repository;
- this.slug_perm = { id: "Slug", value: cloudsmithMatch.slug_perm };
- this.slug_perm_raw = cloudsmithMatch.slug_perm;
- this.version = { id: "Version", value: cloudsmithMatch.version };
- this.status_str = { id: "Status", value: cloudsmithMatch.status_str };
- this.self_webapp_url = cloudsmithMatch.self_webapp_url || null;
- this.checksum_sha256 = cloudsmithMatch.checksum_sha256 || null;
- this.version_digest = cloudsmithMatch.version_digest || null;
- this.tags_raw = cloudsmithMatch.tags || {};
- this.cdn_url = cloudsmithMatch.cdn_url || null;
- this.filename = cloudsmithMatch.filename || null;
- this.num_vulnerabilities = cloudsmithMatch.num_vulnerabilities || 0;
- this.max_severity = cloudsmithMatch.max_severity || null;
- this.status_reason = cloudsmithMatch.status_reason || null;
- this.licenseInfo = LicenseClassifier.inspect(cloudsmithMatch);
- this.spdx_license = this.licenseInfo.spdxLicense;
- this.raw_license = this.licenseInfo.rawLicense;
- this.license = this.licenseInfo.displayValue;
- this.license_url = this.licenseInfo.licenseUrl;
+ if (this.cloudsmithMatch) {
+ this.namespace = this.cloudsmithMatch.namespace;
+ this.repository = this.cloudsmithMatch.repository;
+ this.slug_perm = { id: "Slug", value: this.cloudsmithMatch.slug_perm };
+ this.slug_perm_raw = this.cloudsmithMatch.slug_perm;
+ this.version = { id: "Version", value: this.cloudsmithMatch.version };
+ this.status_str = { id: "Status", value: this.cloudsmithMatch.status_str };
+ this.self_webapp_url = this.cloudsmithMatch.self_webapp_url || null;
+ this.checksum_sha256 = this.cloudsmithMatch.checksum_sha256 || null;
+ this.version_digest = this.cloudsmithMatch.version_digest || null;
+ this.tags_raw = this.cloudsmithMatch.tags || {};
+ this.cdn_url = this.cloudsmithMatch.cdn_url || null;
+ this.filename = this.cloudsmithMatch.filename || null;
+ this.num_vulnerabilities = this.cloudsmithMatch.num_vulnerabilities || 0;
+ this.max_severity = this.cloudsmithMatch.max_severity || null;
+ this.status_reason = this.cloudsmithMatch.status_reason || null;
}
+ this.spdx_license = this.licenseInfo.spdxLicense;
+ this.raw_license = this.licenseInfo.rawLicense;
+ this.license = this.licenseInfo.displayValue;
+ this.license_url = this.licenseInfo.licenseUrl;
}
- /**
- * Derive the health state from the Cloudsmith match.
- * @returns {"available"|"quarantined"|"violated"|"not_found"|"syncing"}
- */
- _deriveState() {
- if (!this.cloudsmithMatch) {
- return "not_found";
+ _deriveLicenseInfo() {
+ if (this.licenseData) {
+ return LicenseClassifier.inspect({
+ license: this.licenseData.display || this.licenseData.raw || null,
+ spdx_license: this.licenseData.spdx || null,
+ license_url: this.licenseData.url || null,
+ });
}
- const match = this.cloudsmithMatch;
+ if (this.cloudsmithMatch) {
+ return LicenseClassifier.inspect(this.cloudsmithMatch);
+ }
- if (match.status_str === "Quarantined") {
- return "quarantined";
+ return LicenseClassifier.inspect(null);
+ }
+
+ _deriveState() {
+ if (this.cloudsmithStatus === "CHECKING") {
+ return "checking";
}
- if (match.status_str !== "Completed") {
- return "syncing";
+ if (this.cloudsmithStatus !== "FOUND" || !this.cloudsmithMatch) {
+ return "not_found";
}
- if (match.deny_policy_violated || match.policy_violated) {
+ if (this._isQuarantined()) {
+ return "quarantined";
+ }
+
+ if (
+ this._hasVulnerabilities()
+ || this._hasPolicyViolation()
+ || this._hasRestrictiveLicense()
+ || this._hasWeakCopyleftLicense()
+ ) {
return "violated";
}
return "available";
}
+ _hasVulnerabilities() {
+ return Boolean(this._getVulnerabilityData() && this._getVulnerabilityData().count > 0);
+ }
+
+ _hasCriticalVulnerability() {
+ const vulnerabilities = this._getVulnerabilityData();
+ return Boolean(vulnerabilities && vulnerabilities.count > 0 && vulnerabilities.maxSeverity === "Critical");
+ }
+
+ _hasHighVulnerability() {
+ const vulnerabilities = this._getVulnerabilityData();
+ return Boolean(vulnerabilities && vulnerabilities.count > 0 && vulnerabilities.maxSeverity === "High");
+ }
+
+ _hasMediumOrLowVulnerability() {
+ return this._hasVulnerabilities()
+ && !this._hasCriticalVulnerability()
+ && !this._hasHighVulnerability();
+ }
+
+ _hasRestrictiveLicense() {
+ return Boolean(
+ (this.licenseData && this.licenseData.classification === "restrictive")
+ || this.licenseInfo.tier === "restrictive"
+ );
+ }
+
+ _hasWeakCopyleftLicense() {
+ return Boolean(
+ (this.licenseData && this.licenseData.classification === "weak_copyleft")
+ || this.licenseInfo.tier === "cautious"
+ );
+ }
+
+ _hasPolicyViolation() {
+ const policy = this._getPolicyData();
+ return Boolean(policy && policy.violated);
+ }
+
+ _isQuarantined() {
+ const policy = this._getPolicyData();
+ return Boolean(policy && (policy.quarantined || policy.denied));
+ }
+
+ _getLicenseLabel() {
+ if (this.licenseData) {
+ return this.licenseData.display || this.licenseData.spdx || this.licenseData.raw || null;
+ }
+
+ return this.licenseInfo.displayValue || null;
+ }
+
+ _shouldFlagRestrictiveLicenses() {
+ const config = vscode.workspace.getConfiguration("cloudsmith-vsc");
+ return config.get("flagRestrictiveLicenses") !== false;
+ }
+
+ _getContextValue() {
+ if (this.cloudsmithStatus === "CHECKING") {
+ return "dependencyHealthSyncing";
+ }
+
+ if (this.cloudsmithStatus !== "FOUND") {
+ if (this.upstreamStatus === "reachable") {
+ return "dependencyHealthUpstreamReachable";
+ }
+
+ if (this.upstreamStatus === "no_proxy" || this.upstreamStatus === "unreachable") {
+ return "dependencyHealthUpstreamUnreachable";
+ }
+
+ return "dependencyHealthMissing";
+ }
+
+ if (this._isQuarantined()) {
+ return "dependencyHealthQuarantined";
+ }
+
+ if (this._hasVulnerabilities()) {
+ return "dependencyHealthVulnerable";
+ }
+
+ return "dependencyHealthFound";
+ }
+
_getStateIcon() {
- switch (this.state) {
- case "available":
- return new vscode.ThemeIcon("check", new vscode.ThemeColor("testing.iconPassed"));
- case "quarantined":
- return new vscode.ThemeIcon("error", new vscode.ThemeColor("errorForeground"));
- case "violated":
- return new vscode.ThemeIcon("warning", new vscode.ThemeColor("editorWarning.foreground"));
- case "syncing":
- return new vscode.ThemeIcon("sync");
- case "not_found":
- default:
- return new vscode.ThemeIcon("question", new vscode.ThemeColor("descriptionForeground"));
+ if (this.cloudsmithStatus === "CHECKING") {
+ return new vscode.ThemeIcon("loading~spin");
+ }
+
+ if (this.cloudsmithStatus !== "FOUND") {
+ return getFormatIconPath(this.format, this.context && this.context.extensionPath, {
+ fallbackIcon: new vscode.ThemeIcon("package", new vscode.ThemeColor("descriptionForeground")),
+ });
+ }
+
+ if (this._isQuarantined()) {
+ return new vscode.ThemeIcon("error", new vscode.ThemeColor("errorForeground"));
+ }
+
+ if (this._hasCriticalVulnerability()) {
+ return new vscode.ThemeIcon("error", new vscode.ThemeColor("errorForeground"));
+ }
+
+ if (this._hasHighVulnerability() || this._hasRestrictiveLicense()) {
+ return new vscode.ThemeIcon("warning", new vscode.ThemeColor("charts.orange"));
+ }
+
+ if (this._hasMediumOrLowVulnerability() || this._hasWeakCopyleftLicense() || this._hasPolicyViolation()) {
+ return new vscode.ThemeIcon("warning", new vscode.ThemeColor("charts.yellow"));
}
+
+ return new vscode.ThemeIcon("check", new vscode.ThemeColor("testing.iconPassed"));
+ }
+
+ _buildVersionPrefix() {
+ return this.declaredVersion ? this.declaredVersion : "Unknown version";
}
- _getStateDescription() {
- switch (this.state) {
- case "available":
- return "Available";
- case "quarantined":
- return "Quarantined";
- case "violated":
- return "Policy violation";
- case "syncing":
- return "Syncing";
- case "not_found":
- default:
- return "Not found in Cloudsmith";
+ _buildVulnerabilityDescription() {
+ const vulnerabilities = this._getVulnerabilityData();
+ if (!vulnerabilities || vulnerabilities.count === 0) {
+ return null;
+ }
+
+ if (
+ vulnerabilities.detailsLoaded
+ && vulnerabilities.maxSeverity
+ && vulnerabilities.severityCounts
+ && vulnerabilities.severityCounts[vulnerabilities.maxSeverity]
+ ) {
+ const maxCount = vulnerabilities.severityCounts[vulnerabilities.maxSeverity];
+ return `Vulnerabilities found (${maxCount} ${vulnerabilities.maxSeverity})`;
}
+
+ const summary = vulnerabilities.maxSeverity
+ ? `${vulnerabilities.count} ${vulnerabilities.maxSeverity}`
+ : String(vulnerabilities.count);
+ return `Vulnerabilities found (${summary})`;
+ }
+
+ _buildMissingDescription() {
+ return "Not found in Cloudsmith";
+ }
+
+ _buildDescription() {
+ if (this._duplicateReference) {
+ return `${this._buildVersionPrefix()} (see first occurrence)`;
+ }
+
+ let detail;
+ if (this.cloudsmithStatus === "CHECKING") {
+ detail = "Checking coverage";
+ } else if (this.cloudsmithStatus !== "FOUND") {
+ detail = this._buildMissingDescription();
+ } else if (this._isQuarantined()) {
+ detail = "Quarantined";
+ } else if (this._hasVulnerabilities()) {
+ detail = this._buildVulnerabilityDescription();
+ } else if (this._shouldFlagRestrictiveLicenses() && this._hasRestrictiveLicense()) {
+ detail = this._getLicenseLabel()
+ ? `Restrictive license (${this._getLicenseLabel()})`
+ : "Restrictive license";
+ } else if (this._hasWeakCopyleftLicense()) {
+ detail = this._getLicenseLabel()
+ ? `Weak copyleft license (${this._getLicenseLabel()})`
+ : "Weak copyleft license";
+ } else if (this._hasPolicyViolation()) {
+ detail = "Policy violation";
+ } else {
+ detail = "No issues found";
+ }
+
+ if (this._dimmedForFilter && this.cloudsmithStatus === "FOUND") {
+ detail += " · context";
+ }
+
+ return `${this._buildVersionPrefix()} — ${detail}`;
}
_buildTooltip() {
- const lines = [`${this.name} ${this.declaredVersion}`];
+ const lines = [`${this.name} ${this.declaredVersion || ""}`.trim()];
lines.push(`Format: ${this.format}`);
+ lines.push(`Relationship: ${this._getRelationshipLabel()}`);
if (this.isDev) {
lines.push("Development dependency");
}
lines.push("");
- if (!this.cloudsmithMatch) {
+ if (this.cloudsmithStatus === "CHECKING") {
+ lines.push("Coverage check in progress.");
+ } else if (this.cloudsmithStatus !== "FOUND" || !this.cloudsmithMatch) {
lines.push("Not found in the configured Cloudsmith workspace.");
- lines.push("This package may need to be uploaded or fetched through an upstream.");
- } else {
- const match = this.cloudsmithMatch;
- lines.push(`Cloudsmith version: ${match.version}`);
- lines.push(`Status: ${match.status_str}`);
- if (match.policy_violated) {
- lines.push("Policy violated: yes");
+ if (this.upstreamDetail) {
+ lines.push(this.upstreamDetail);
+ } else {
+ lines.push("This package may need to be uploaded or fetched through an upstream.");
}
- if (match.deny_policy_violated) {
- lines.push("Deny policy violated: yes");
- }
- if (match.license_policy_violated) {
- lines.push("License policy violated: yes");
- }
- if (match.vulnerability_policy_violated) {
- lines.push("Vulnerability policy violated: yes");
- }
- if (match.num_vulnerabilities > 0) {
- lines.push(`Vulnerabilities: ${match.num_vulnerabilities} (${match.max_severity || "Unknown"})`);
+ } else {
+ lines.push(`Found in Cloudsmith (${this.cloudsmithMatch.repository})`);
+ const policy = this._getPolicyData();
+ if (policy && policy.status) {
+ lines.push(`Status: ${policy.status}`);
+ } else if (this.cloudsmithMatch.status_str) {
+ lines.push(`Status: ${this.cloudsmithMatch.status_str}`);
}
- const classification = this.licenseInfo || LicenseClassifier.inspect(match);
- if (classification.displayValue) {
- lines.push(`License: ${classification.label} (${classification.metadata.label})`);
- if (classification.spdxLicense && classification.spdxLicense !== classification.label) {
- lines.push(`Canonical SPDX: ${classification.spdxLicense}`);
- }
- if (classification.overrideApplied) {
- lines.push("License classification includes a local restrictive override.");
+
+ const vulnerabilities = this._getVulnerabilityData();
+ if (vulnerabilities) {
+ if (vulnerabilities && vulnerabilities.count > 0) {
+ const severitySummary = Object.entries(vulnerabilities.severityCounts || {})
+ .map(([severity, count]) => `${count} ${severity}`)
+ .join(", ");
+ const suffix = severitySummary
+ ? ` (${severitySummary})`
+ : vulnerabilities.maxSeverity
+ ? ` (${vulnerabilities.maxSeverity})`
+ : "";
+ lines.push(`Vulnerabilities: ${vulnerabilities.count}${suffix}`);
+
+ if (Array.isArray(vulnerabilities.entries)) {
+ for (const entry of vulnerabilities.entries) {
+ const fixText = entry.fixVersion ? `Fix: ${entry.fixVersion}` : "No fix available";
+ lines.push(` ${entry.cveId} (${entry.severity}) — ${fixText}`);
+ }
+ }
+ } else {
+ lines.push("Vulnerabilities: none known");
}
}
- if (this.state === "quarantined" || this.state === "violated") {
- lines.push("");
- lines.push("Right-click \u2192 Explain quarantine or find safe version");
+ if (this.licenseData) {
+ lines.push(
+ `License: ${this._getLicenseLabel() || "No license detected"} (${formatLicenseClassification(this.licenseData.classification)})`
+ );
+ } else if (this.licenseInfo.displayValue) {
+ lines.push(
+ `License: ${this.licenseInfo.displayValue} (${formatLicenseClassification(classificationFromTier(this.licenseInfo.tier))})`
+ );
+ } else {
+ lines.push("License: No license detected");
}
- }
- return lines.join("\n");
- }
+ if (policy && policy.violated) {
+ lines.push(`Policy violated: ${policy.denied ? "deny" : "yes"}`);
+ }
- _getContextValue() {
- switch (this.state) {
- case "quarantined":
- return "dependencyHealthBlocked";
- case "violated":
- return "dependencyHealthViolated";
- case "available":
- return "dependencyHealth";
- case "not_found":
- return "dependencyHealthNotFound";
- case "syncing":
- return "dependencyHealthSyncing";
- default:
- return "dependencyHealth";
+ if (policy && policy.statusReason) {
+ lines.push(`Policy reason: ${policy.statusReason}`);
+ }
}
- }
-
- /** Sort key: lower = more urgent (quarantined first). */
- get sortOrder() {
- const order = { quarantined: 0, violated: 1, not_found: 2, syncing: 3, available: 4 };
- return order[this.state] != null ? order[this.state] : 5;
- }
-
- getTreeItem() {
- const devLabel = this.isDev ? " (dev)" : "";
- const indirectLabel = !this.isDirect ? " (indirect)" : "";
- const versionLabel = this.declaredVersion ? ` ${this.declaredVersion}` : "";
- const desc = this.state === "quarantined"
- ? `${this._getStateDescription()} \u2014 right-click for details`
- : this._getStateDescription();
+ if (this._duplicateReference && this._firstOccurrencePath) {
+ lines.push("");
+ lines.push(`See first occurrence: ${this._firstOccurrencePath}`);
+ }
- return {
- label: `${this.name}${versionLabel}${devLabel}${indirectLabel}`,
- description: desc,
- tooltip: this._buildTooltip(),
- collapsibleState: this.cloudsmithMatch
- ? vscode.TreeItemCollapsibleState.Collapsed
- : vscode.TreeItemCollapsibleState.None,
- contextValue: this._getContextValue(),
- iconPath: this._getStateIcon(),
- };
+ return lines.join("\n");
}
- getChildren() {
- if (!this.cloudsmithMatch) {
+ _buildDetailsChildren() {
+ if (!this.cloudsmithMatch || this.state === "checking") {
return [];
}
const PackageDetailsNode = require("./packageDetailsNode");
const children = [];
- const match = this.cloudsmithMatch;
- // Status
- children.push(new PackageDetailsNode({ id: "Status", value: match.status_str }, this.context));
+ children.push(new PackageDetailsNode({
+ id: "Status",
+ value: this.policy && this.policy.status ? this.policy.status : this.cloudsmithMatch.status_str,
+ }, this.context));
- // Cloudsmith Version
- children.push(new PackageDetailsNode({ id: "Version", value: match.version }, this.context));
+ children.push(new PackageDetailsNode({
+ id: "Version",
+ value: this.cloudsmithMatch.version,
+ }, this.context));
- // License with classification
const config = vscode.workspace.getConfiguration("cloudsmith-vsc");
if (config.get("showLicenseIndicators") !== false && this.licenseInfo && this.licenseInfo.displayValue) {
const LicenseNode = require("./licenseNode");
children.push(new LicenseNode(this.licenseInfo, this.context));
}
- // Vulnerability summary
- if (match.num_vulnerabilities > 0) {
+ const vulnerabilities = this._getVulnerabilityData();
+ if (vulnerabilities && vulnerabilities.count > 0) {
const VulnerabilitySummaryNode = require("./vulnerabilitySummaryNode");
children.push(new VulnerabilitySummaryNode({
- namespace: match.namespace,
- repository: match.repository,
- slug_perm: match.slug_perm,
- num_vulnerabilities: match.num_vulnerabilities,
- max_severity: match.max_severity,
+ namespace: this.cloudsmithMatch.namespace,
+ repository: this.cloudsmithMatch.repository,
+ slug_perm: this.cloudsmithMatch.slug_perm,
+ num_vulnerabilities: vulnerabilities.count,
+ max_severity: vulnerabilities.maxSeverity,
}, this.context));
}
- // Policy Violated
- const policyValue = match.policy_violated ? "Yes" : "No";
- children.push(new PackageDetailsNode({ id: "Policy violated", value: policyValue }, this.context));
+ const policy = this._getPolicyData();
+ if (policy) {
+ children.push(new PackageDetailsNode({
+ id: "Policy violated",
+ value: policy.violated ? "Yes" : "No",
+ }, this.context));
- // Quarantine Reason (if quarantined)
- if (match.status_str === "Quarantined" && match.status_reason) {
- const truncated = match.status_reason.length > 80
- ? match.status_reason.substring(0, 80) + "..."
- : match.status_reason;
- const reasonNode = new PackageDetailsNode({
- id: "Quarantine reason",
- value: truncated,
- }, this.context);
- children.push(reasonNode);
+ if (policy.statusReason) {
+ children.push(new PackageDetailsNode({
+ id: "Policy reason",
+ value: policy.statusReason,
+ }, this.context));
+ }
}
return children;
}
+
+ _getVulnerabilityData() {
+ if (this.vulnerabilities) {
+ return this.vulnerabilities;
+ }
+
+ if (!this.cloudsmithMatch) {
+ return null;
+ }
+
+ const count = Number(
+ this.cloudsmithMatch.vulnerability_scan_results_count
+ || this.cloudsmithMatch.num_vulnerabilities
+ || 0
+ );
+ if (!Number.isFinite(count) || count <= 0) {
+ return {
+ count: 0,
+ maxSeverity: null,
+ cveIds: [],
+ hasFixAvailable: false,
+ severityCounts: {},
+ entries: [],
+ detailsLoaded: false,
+ };
+ }
+
+ const maxSeverity = this.cloudsmithMatch.max_severity || null;
+ const severityCounts = maxSeverity ? { [maxSeverity]: 1 } : {};
+ return {
+ count,
+ maxSeverity,
+ cveIds: [],
+ hasFixAvailable: false,
+ severityCounts,
+ entries: [],
+ detailsLoaded: false,
+ };
+ }
+
+ _getPolicyData() {
+ if (this.policy) {
+ return this.policy;
+ }
+
+ if (!this.cloudsmithMatch) {
+ return null;
+ }
+
+ const status = String(this.cloudsmithMatch.status_str || "").trim() || null;
+ const quarantined = status === "Quarantined";
+ const denied = quarantined || Boolean(this.cloudsmithMatch.deny_policy_violated);
+ const violated = denied
+ || Boolean(this.cloudsmithMatch.policy_violated)
+ || Boolean(this.cloudsmithMatch.license_policy_violated)
+ || Boolean(this.cloudsmithMatch.vulnerability_policy_violated);
+
+ return {
+ violated,
+ denied,
+ quarantined,
+ status,
+ statusReason: String(this.cloudsmithMatch.status_reason || "").trim() || null,
+ };
+ }
+
+ _getRelationshipLabel() {
+ if (this.isDirect) {
+ return "Direct";
+ }
+
+ const firstParent = this.parentChain[0] || this.parent || "unknown";
+ return `Transitive (via ${firstParent})`;
+ }
+
+ getTreeItem() {
+ const item = new vscode.TreeItem(
+ `${this.name}${this.isDev ? " (dev)" : ""}`,
+ this._getCollapsibleState()
+ );
+ item.description = this._buildDescription();
+ item.tooltip = this._buildTooltip();
+ item.contextValue = this._getContextValue();
+ item.iconPath = this._getStateIcon();
+ return item;
+ }
+
+ _getCollapsibleState() {
+ if (this._childMode === "tree") {
+ if (this._duplicateReference || this._treeChildren.length === 0) {
+ return vscode.TreeItemCollapsibleState.None;
+ }
+ return vscode.TreeItemCollapsibleState.Collapsed;
+ }
+
+ return this.cloudsmithMatch
+ ? vscode.TreeItemCollapsibleState.Collapsed
+ : vscode.TreeItemCollapsibleState.None;
+ }
+
+ getChildren() {
+ if (this._childMode === "tree") {
+ if (!this._treeChildFactory || this._duplicateReference || this._treeChildren.length === 0) {
+ return [];
+ }
+ return this._treeChildFactory(this._treeChildren);
+ }
+
+ return this._buildDetailsChildren();
+ }
+}
+
+function formatLicenseClassification(classification) {
+ switch (classification) {
+ case "permissive":
+ return "Permissive";
+ case "weak_copyleft":
+ return "Weak copyleft";
+ case "restrictive":
+ return "Restrictive";
+ default:
+ return "Unclassified";
+ }
+}
+
+function classificationFromTier(tier) {
+ switch (tier) {
+ case "permissive":
+ return "permissive";
+ case "cautious":
+ return "weak_copyleft";
+ case "restrictive":
+ return "restrictive";
+ default:
+ return "unknown";
+ }
}
module.exports = DependencyHealthNode;
diff --git a/models/dependencySourceGroupNode.js b/models/dependencySourceGroupNode.js
new file mode 100644
index 0000000..baa879f
--- /dev/null
+++ b/models/dependencySourceGroupNode.js
@@ -0,0 +1,35 @@
+// Copyright 2026 Cloudsmith Ltd. All rights reserved.
+const vscode = require("vscode");
+
+class DependencySourceGroupNode {
+ constructor(tree, provider) {
+ this.tree = tree;
+ this.provider = provider;
+ }
+
+ getTreeItem() {
+ const directCount = this.tree.dependencies.filter((dependency) => dependency.isDirect).length;
+ const transitiveCount = this.tree.dependencies.length - directCount;
+ const item = new vscode.TreeItem(
+ this.tree.sourceFile,
+ vscode.TreeItemCollapsibleState.Collapsed
+ );
+ item.description = `${this.tree.dependencies.length} dependencies `
+ + `(${directCount} direct, ${transitiveCount} transitive)`;
+ item.tooltip = [
+ this.tree.sourceFile,
+ `${this.tree.dependencies.length} dependencies`,
+ `${directCount} direct`,
+ `${transitiveCount} transitive`,
+ ].join("\n");
+ item.contextValue = "dependencyHealthSourceGroup";
+ item.iconPath = new vscode.ThemeIcon("folder-library");
+ return item;
+ }
+
+ getChildren() {
+ return this.provider.buildDependencyNodesForTree(this.tree);
+ }
+}
+
+module.exports = DependencySourceGroupNode;
diff --git a/models/dependencySummaryNode.js b/models/dependencySummaryNode.js
new file mode 100644
index 0000000..c3c8a8a
--- /dev/null
+++ b/models/dependencySummaryNode.js
@@ -0,0 +1,175 @@
+// Copyright 2026 Cloudsmith Ltd. All rights reserved.
+const vscode = require("vscode");
+
+class DependencySummaryNode {
+ constructor(summary) {
+ this.summary = {
+ total: 0,
+ direct: 0,
+ transitive: 0,
+ found: 0,
+ notFound: 0,
+ reachableViaUpstream: 0,
+ unreachableViaUpstream: 0,
+ ecosystems: {},
+ coveragePercent: 0,
+ checking: 0,
+ vulnerable: 0,
+ severityCounts: {},
+ restrictiveLicenses: 0,
+ weakCopyleftLicenses: 0,
+ permissiveLicenses: 0,
+ unknownLicenses: 0,
+ policyViolations: 0,
+ quarantined: 0,
+ filterMode: null,
+ filterLabel: null,
+ filteredCount: 0,
+ ...summary,
+ };
+ }
+
+ getTreeItem() {
+ const item = new vscode.TreeItem(buildPrimaryLabel(this.summary), vscode.TreeItemCollapsibleState.None);
+ item.description = buildSecondaryLabel(this.summary);
+ item.tooltip = buildTooltip(this.summary);
+ item.contextValue = "dependencyHealthSummary";
+ item.iconPath = this.summary.checking > 0
+ ? new vscode.ThemeIcon("loading~spin")
+ : new vscode.ThemeIcon("graph");
+ return item;
+ }
+
+ getChildren() {
+ return [];
+ }
+}
+
+function buildPrimaryLabel(summary) {
+ if (summary.filterMode && summary.filterLabel) {
+ return `Showing ${summary.filteredCount} of ${summary.total} dependencies (filtered: ${summary.filterLabel})`;
+ }
+
+ const parts = [
+ `${summary.total} dependencies (${summary.direct} direct, ${summary.transitive} transitive)`,
+ `${summary.coveragePercent}% coverage`,
+ ];
+
+ if (summary.vulnerable > 0) {
+ parts.push(`${summary.vulnerable} vulnerable`);
+ }
+
+ if (summary.restrictiveLicenses > 0) {
+ parts.push(`${summary.restrictiveLicenses} restrictive licenses`);
+ }
+
+ return parts.join(" · ");
+}
+
+function buildSecondaryLabel(summary) {
+ const parts = [];
+ const severityParts = buildSeverityParts(summary.severityCounts);
+
+ if (severityParts.length > 0) {
+ parts.push(severityParts.join(" · "));
+ }
+
+ if (summary.quarantined > 0) {
+ parts.push(`${summary.quarantined} would be quarantined by policy`);
+ } else if (summary.policyViolations > 0) {
+ parts.push(`${summary.policyViolations} policy violations`);
+ }
+
+ if (summary.notFound > 0) {
+ const upstreamParts = [`${summary.notFound} not found in Cloudsmith`];
+ if (summary.reachableViaUpstream > 0) {
+ upstreamParts.push(`${summary.reachableViaUpstream} reachable via configured upstream proxies`);
+ }
+ if (summary.unreachableViaUpstream > 0) {
+ upstreamParts.push(`${summary.unreachableViaUpstream} not reachable`);
+ }
+ parts.push(upstreamParts.join(" · "));
+ }
+
+ if (parts.length > 0) {
+ return parts.join(" · ");
+ }
+
+ const ecosystemEntries = Object.entries(summary.ecosystems || {});
+ if (ecosystemEntries.length > 1) {
+ return ecosystemEntries
+ .map(([ecosystem, count]) => `${formatEcosystemLabel(ecosystem)}: ${count}`)
+ .join(" · ");
+ }
+
+ return "";
+}
+
+function buildSeverityParts(severityCounts) {
+ const order = ["Critical", "High", "Medium", "Low"];
+ return order
+ .filter((severity) => severityCounts && severityCounts[severity] > 0)
+ .map((severity) => `${severityCounts[severity]} ${severity}`);
+}
+
+function buildTooltip(summary) {
+ const lines = [
+ `${summary.total} total dependencies`,
+ `${summary.direct} direct`,
+ `${summary.transitive} transitive`,
+ `${summary.found} covered in Cloudsmith`,
+ `${summary.notFound} not found`,
+ `${summary.coveragePercent}% coverage`,
+ ];
+
+ if (summary.vulnerable > 0) {
+ lines.push(`${summary.vulnerable} vulnerable`);
+ for (const part of buildSeverityParts(summary.severityCounts)) {
+ lines.push(` ${part}`);
+ }
+ }
+
+ if (summary.restrictiveLicenses > 0 || summary.weakCopyleftLicenses > 0 || summary.unknownLicenses > 0) {
+ lines.push("");
+ lines.push("License summary");
+ lines.push(` ${summary.permissiveLicenses} permissive`);
+ lines.push(` ${summary.weakCopyleftLicenses} weak copyleft`);
+ lines.push(` ${summary.restrictiveLicenses} restrictive`);
+ lines.push(` ${summary.unknownLicenses} unknown`);
+ }
+
+ if (summary.policyViolations > 0 || summary.quarantined > 0) {
+ lines.push("");
+ lines.push(`Policy violations: ${summary.policyViolations}`);
+ lines.push(`Would be quarantined: ${summary.quarantined}`);
+ }
+
+ if (summary.notFound > 0) {
+ lines.push("");
+ lines.push(`Reachable via upstream: ${summary.reachableViaUpstream}`);
+ lines.push(`Not reachable: ${summary.unreachableViaUpstream}`);
+ }
+
+ const ecosystemEntries = Object.entries(summary.ecosystems || {});
+ if (ecosystemEntries.length > 0) {
+ lines.push("");
+ for (const [ecosystem, count] of ecosystemEntries) {
+ lines.push(`${formatEcosystemLabel(ecosystem)}: ${count}`);
+ }
+ }
+
+ return lines.join("\n");
+}
+
+function formatEcosystemLabel(ecosystem) {
+ const value = String(ecosystem || "");
+ if (!value) {
+ return "";
+ }
+ if (value === "npm") {
+ return "npm";
+ }
+ return value.charAt(0).toUpperCase() + value.slice(1);
+}
+
+module.exports = DependencySummaryNode;
diff --git a/test/dependencyHealthProvider.test.js b/test/dependencyHealthProvider.test.js
index 4cf3ea2..7e9a0ea 100644
--- a/test/dependencyHealthProvider.test.js
+++ b/test/dependencyHealthProvider.test.js
@@ -1,24 +1,413 @@
const assert = require("assert");
-const { DependencyHealthProvider } = require("../views/dependencyHealthProvider");
+const vscode = require("vscode");
+const {
+ DependencyHealthProvider,
+ matchCoverageCandidates,
+} = require("../views/dependencyHealthProvider");
+const { normalizePackageName } = require("../util/packageNameNormalizer");
suite("DependencyHealthProvider Test Suite", () => {
- test("getChildren() shows the signed-out state when disconnected before the first scan", async () => {
- const context = {
+ function createContext(isConnected = "true") {
+ return {
secrets: {
onDidChange() {},
async get(key) {
if (key === "cloudsmith-vsc.isConnected") {
- return "false";
+ return isConnected;
}
return null;
},
},
+ workspaceState: {
+ get() {
+ return null;
+ },
+ async update() {},
+ },
+ };
+ }
+
+ function createDependency(name, version, format = "npm") {
+ return {
+ name,
+ version,
+ format,
+ ecosystem: format,
+ isDirect: true,
+ parent: null,
+ parentChain: [],
+ transitives: [],
+ cloudsmithStatus: "CHECKING",
+ cloudsmithPackage: null,
+ sourceFile: "package-lock.json",
+ isDevelopmentDependency: false,
+ };
+ }
+
+ function createFoundDependency(name, version) {
+ return {
+ ...createDependency(name, version),
+ cloudsmithStatus: "FOUND",
+ cloudsmithPackage: {
+ namespace: "workspace",
+ repository: "repo",
+ slug_perm: `${name}/${version}`,
+ },
};
+ }
+
+ function cloneTrees(trees) {
+ return JSON.parse(JSON.stringify(trees));
+ }
- const provider = new DependencyHealthProvider(context);
+ function buildCoverageIndex(dependencies) {
+ const index = new Map();
+
+ for (const dependency of dependencies) {
+ const nameKey = normalizePackageName(dependency.name, dependency.format);
+ const versionKey = dependency.version.toLowerCase();
+ if (!index.has(nameKey)) {
+ index.set(nameKey, new Map());
+ }
+ index.get(nameKey).set(versionKey, [{
+ name: dependency.name,
+ version: dependency.version,
+ }]);
+ }
+
+ return index;
+ }
+
+ setup(() => {
+ DependencyHealthProvider.packageIndexCache.clear();
+ });
+
+ test("getChildren() shows the signed-out state when disconnected before the first scan", async () => {
+ const provider = new DependencyHealthProvider(createContext("false"));
const nodes = await provider.getChildren();
assert.strictEqual(nodes.length, 1);
assert.strictEqual(nodes[0].getTreeItem().label, "Connect to Cloudsmith");
});
+
+ test("_runCoverageChecks batches tree rebuilds and refreshes while preserving matches", async () => {
+ const provider = new DependencyHealthProvider(createContext());
+ const dependencies = Array.from({ length: 51 }, (_, index) => createDependency(`package-${index}`, "1.0.0"));
+ const trees = [{
+ ecosystem: "npm",
+ sourceFile: "package-lock.json",
+ dependencies,
+ }];
+
+ provider._fullTrees = cloneTrees(trees);
+ provider._displayTrees = cloneTrees(trees);
+
+ let rebuildCount = 0;
+ let refreshCount = 0;
+ const progressUpdates = [];
+
+ provider._rebuildSummary = () => {
+ rebuildCount += 1;
+ };
+ provider.refresh = () => {
+ refreshCount += 1;
+ };
+ provider._fetchPackageIndex = async () => ({
+ error: null,
+ tooLarge: false,
+ index: buildCoverageIndex(dependencies),
+ });
+
+ await provider._runCoverageChecks(
+ "workspace",
+ "repo",
+ dependencies.length,
+ {
+ report(update) {
+ progressUpdates.push(update);
+ },
+ },
+ { isCancellationRequested: false }
+ );
+
+ assert.strictEqual(rebuildCount, 2);
+ assert.strictEqual(refreshCount, 2);
+ assert.strictEqual(progressUpdates.length, 2);
+ assert.strictEqual(progressUpdates[0].message, "Matching coverage... 50/51");
+ assert.strictEqual(progressUpdates[1].message, "Matching coverage... 51/51");
+ assert.strictEqual(
+ provider._fullTrees[0].dependencies.every((dependency) => dependency.cloudsmithStatus === "FOUND"),
+ true
+ );
+ assert.strictEqual(
+ provider._displayTrees[0].dependencies.every((dependency) => dependency.cloudsmithStatus === "FOUND"),
+ true
+ );
+ });
+
+ test("_runCoverageChecks fetches package indices for multiple formats in parallel", async () => {
+ const provider = new DependencyHealthProvider(createContext());
+ const npmDependency = createDependency("left-pad", "1.0.0", "npm");
+ const pythonDependency = createDependency("requests", "2.31.0", "python");
+
+ provider._fullTrees = [
+ {
+ ecosystem: "npm",
+ sourceFile: "package-lock.json",
+ dependencies: [npmDependency],
+ },
+ {
+ ecosystem: "python",
+ sourceFile: "requirements.txt",
+ dependencies: [pythonDependency],
+ },
+ ];
+ provider._displayTrees = cloneTrees(provider._fullTrees);
+
+ const resolvers = new Map();
+ let inFlight = 0;
+ let maxInFlight = 0;
+
+ provider._rebuildSummary = () => {};
+ provider.refresh = () => {};
+ provider._fetchPackageIndex = async (_workspace, _repo, format) => {
+ inFlight += 1;
+ maxInFlight = Math.max(maxInFlight, inFlight);
+
+ return new Promise((resolve) => {
+ resolvers.set(format, () => {
+ inFlight -= 1;
+ const dependency = format === "npm" ? npmDependency : pythonDependency;
+ resolve({
+ error: null,
+ tooLarge: false,
+ index: buildCoverageIndex([dependency]),
+ });
+ });
+ });
+ };
+
+ const runPromise = provider._runCoverageChecks(
+ "workspace",
+ "repo",
+ 2,
+ { report() {} },
+ { isCancellationRequested: false }
+ );
+
+ await new Promise((resolve) => setImmediate(resolve));
+ assert.strictEqual(maxInFlight, 2);
+
+ resolvers.get("npm")();
+ resolvers.get("python")();
+ await runPromise;
+ });
+
+ test("_fetchPackageIndex fetches remaining pages concurrently after page one", async () => {
+ const provider = new DependencyHealthProvider(createContext());
+ const requestedPages = [];
+ const pageResolvers = new Map();
+
+ provider._fetchSinglePage = async (_workspace, _repo, _format, page) => {
+ requestedPages.push(page);
+ if (page === 1) {
+ return {
+ error: null,
+ pagination: {
+ count: 3,
+ pageTotal: 3,
+ },
+ data: [{
+ name: "page-one",
+ version: "1.0.0",
+ }],
+ };
+ }
+
+ return new Promise((resolve) => {
+ pageResolvers.set(page, resolve);
+ });
+ };
+
+ const fetchPromise = provider._fetchPackageIndex("workspace", "repo", "npm");
+ await new Promise((resolve) => setImmediate(resolve));
+
+ assert.deepStrictEqual(requestedPages, [1, 2, 3]);
+
+ pageResolvers.get(2)({
+ error: null,
+ data: [{
+ name: "page-two",
+ version: "1.0.0",
+ }],
+ });
+ pageResolvers.get(3)({
+ error: null,
+ data: [{
+ name: "page-three",
+ version: "1.0.0",
+ }],
+ });
+
+ const result = await fetchPromise;
+ assert.strictEqual(result.error, null);
+ assert.strictEqual(result.totalCount, 3);
+ assert.strictEqual(result.index.get("page-two").has("1.0.0"), true);
+ assert.strictEqual(result.index.get("page-three").has("1.0.0"), true);
+ });
+
+ test("matchCoverageCandidates returns null when fallback results do not match the dependency name", () => {
+ const match = matchCoverageCandidates(
+ [
+ { name: "left-pad-plus", version: "1.0.0", format: "npm" },
+ { name: "pad-left", version: "1.0.0", format: "npm" },
+ ],
+ createDependency("left-pad", "1.0.0")
+ );
+
+ assert.strictEqual(match, null);
+ });
+
+ test("matchCoverageCandidates falls back to a name match when versions differ", () => {
+ const nameOnlyMatch = { name: "left-pad", version: "1.1.0", format: "npm" };
+ const match = matchCoverageCandidates(
+ [
+ { name: "left-pad-plus", version: "1.0.0", format: "npm" },
+ nameOnlyMatch,
+ ],
+ createDependency("left-pad", "1.0.0")
+ );
+
+ assert.strictEqual(match, nameOnlyMatch);
+ });
+
+ test("_runLicenseEnrichment flushes multiple progress patches in one refresh", async () => {
+ const provider = new DependencyHealthProvider(createContext(), null, {
+ enrichLicenses: async (_dependencies, options = {}) => {
+ options.onProgress(new Map([
+ ["workspace:repo:left-pad/1.0.0", { spdx: "MIT" }],
+ ]));
+ options.onProgress(new Map([
+ ["workspace:repo:left-pad/1.0.0", { spdx: "Apache-2.0" }],
+ ]));
+ },
+ });
+
+ const trees = [{
+ ecosystem: "npm",
+ sourceFile: "package-lock.json",
+ dependencies: [createFoundDependency("left-pad", "1.0.0")],
+ }];
+ provider._fullTrees = cloneTrees(trees);
+ provider._displayTrees = cloneTrees(trees);
+
+ let rebuildCount = 0;
+ let refreshCount = 0;
+ provider._rebuildSummary = () => {
+ rebuildCount += 1;
+ };
+ provider.refresh = () => {
+ refreshCount += 1;
+ };
+
+ await provider._runLicenseEnrichment(provider._fullTrees[0].dependencies, { isCancellationRequested: false });
+
+ assert.strictEqual(rebuildCount, 1);
+ assert.strictEqual(refreshCount, 1);
+ assert.strictEqual(provider._fullTrees[0].dependencies[0].license.spdx, "Apache-2.0");
+ assert.strictEqual(provider._displayTrees[0].dependencies[0].license.spdx, "Apache-2.0");
+ });
+
+ test("pullSingleDependency refreshes coverage after a successful single-package pull", async () => {
+ const originalWithProgress = vscode.window.withProgress;
+ const originalShowInformationMessage = vscode.window.showInformationMessage;
+ const originalShowErrorMessage = vscode.window.showErrorMessage;
+ const notifications = [];
+ let refreshArgs = null;
+
+ vscode.window.withProgress = async (_options, task) => task(
+ { report() {} },
+ {
+ onCancellationRequested() {
+ return { dispose() {} };
+ },
+ }
+ );
+ vscode.window.showInformationMessage = async (message) => {
+ notifications.push(message);
+ };
+ vscode.window.showErrorMessage = async (message) => {
+ notifications.push(`error:${message}`);
+ };
+
+ try {
+ const provider = new DependencyHealthProvider(createContext(), null, {
+ upstreamPullService: {
+ async prepareSingle({ dependency }) {
+ return {
+ workspace: "workspace-a",
+ repository: { slug: "repo-b" },
+ dependency,
+ plan: { skippedDependencies: [] },
+ };
+ },
+ async execute() {
+ return {
+ canceled: false,
+ pullResult: {
+ total: 1,
+ cached: 1,
+ alreadyExisted: 0,
+ notFound: 0,
+ formatMismatched: 0,
+ errors: 0,
+ networkErrors: 0,
+ authFailed: 0,
+ skipped: 0,
+ details: [{
+ status: "cached",
+ dependency: {
+ name: "requests",
+ version: "2.31.0",
+ format: "python",
+ },
+ }],
+ },
+ };
+ },
+ },
+ });
+
+ provider.lastWorkspace = "workspace-a";
+ provider.lastRepo = "repo-a";
+ provider._updateContexts = async () => {};
+ provider.refresh = () => {};
+ provider._refreshSingleDependencyAfterPull = async (workspace, repo, dependency) => {
+ refreshArgs = { workspace, repo, dependency };
+ };
+
+ await provider.pullSingleDependency({
+ name: "requests",
+ version: "2.31.0",
+ format: "python",
+ ecosystem: "python",
+ });
+
+ assert.deepStrictEqual(refreshArgs, {
+ workspace: "workspace-a",
+ repo: "repo-b",
+ dependency: {
+ name: "requests",
+ version: "2.31.0",
+ format: "python",
+ ecosystem: "python",
+ },
+ });
+ assert.deepStrictEqual(notifications, ["requests@2.31.0 cached in repo-b"]);
+ } finally {
+ vscode.window.withProgress = originalWithProgress;
+ vscode.window.showInformationMessage = originalShowInformationMessage;
+ vscode.window.showErrorMessage = originalShowErrorMessage;
+ }
+ });
});
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/fixtures/cargo/Cargo.lock b/test/fixtures/cargo/Cargo.lock
new file mode 100644
index 0000000..27ce744
--- /dev/null
+++ b/test/fixtures/cargo/Cargo.lock
@@ -0,0 +1,25 @@
+[[package]]
+name = "fixture-cargo"
+version = "0.1.0"
+dependencies = [
+ "serde 1.0.0",
+ "tokio 1.37.0"
+]
+
+[[package]]
+name = "serde"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "tokio"
+version = "1.37.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "bytes 1.6.0"
+]
+
+[[package]]
+name = "bytes"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/test/fixtures/cargo/Cargo.toml b/test/fixtures/cargo/Cargo.toml
new file mode 100644
index 0000000..f29e154
--- /dev/null
+++ b/test/fixtures/cargo/Cargo.toml
@@ -0,0 +1,9 @@
+[package]
+name = "fixture-cargo"
+version = "0.1.0"
+
+[dependencies]
+serde = "1.0.0"
+
+[dev-dependencies]
+tokio = { version = "1.37.0", features = ["full"] }
diff --git a/test/fixtures/composer/composer.json b/test/fixtures/composer/composer.json
new file mode 100644
index 0000000..9c39117
--- /dev/null
+++ b/test/fixtures/composer/composer.json
@@ -0,0 +1,8 @@
+{
+ "require": {
+ "laravel/framework": "^11.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.0"
+ }
+}
diff --git a/test/fixtures/composer/composer.lock b/test/fixtures/composer/composer.lock
new file mode 100644
index 0000000..0603532
--- /dev/null
+++ b/test/fixtures/composer/composer.lock
@@ -0,0 +1,28 @@
+{
+ "packages": [
+ {
+ "name": "laravel/framework",
+ "version": "v11.0.0",
+ "require": {
+ "symfony/http-foundation": "^7.0",
+ "php": "^8.2"
+ }
+ },
+ {
+ "name": "symfony/http-foundation",
+ "version": "v7.0.0",
+ "require": {
+ "php": "^8.2"
+ }
+ }
+ ],
+ "packages-dev": [
+ {
+ "name": "phpunit/phpunit",
+ "version": "11.0.0",
+ "require": {
+ "php": "^8.2"
+ }
+ }
+ ]
+}
diff --git a/test/fixtures/dart/pubspec.lock b/test/fixtures/dart/pubspec.lock
new file mode 100644
index 0000000..1384cb4
--- /dev/null
+++ b/test/fixtures/dart/pubspec.lock
@@ -0,0 +1,15 @@
+packages:
+ http:
+ dependency: "direct main"
+ description:
+ name: http
+ source: hosted
+ version: "1.2.1"
+ test:
+ dependency: "direct dev"
+ description:
+ name: test
+ source: hosted
+ version: "1.25.2"
+sdks:
+ dart: ">=3.0.0 <4.0.0"
diff --git a/test/fixtures/dart/pubspec.yaml b/test/fixtures/dart/pubspec.yaml
new file mode 100644
index 0000000..e7d0b60
--- /dev/null
+++ b/test/fixtures/dart/pubspec.yaml
@@ -0,0 +1,7 @@
+name: fixture_dart
+
+dependencies:
+ http: ^1.2.1
+
+dev_dependencies:
+ test: ^1.25.2
diff --git a/test/fixtures/docker/Dockerfile b/test/fixtures/docker/Dockerfile
new file mode 100644
index 0000000..1ee1611
--- /dev/null
+++ b/test/fixtures/docker/Dockerfile
@@ -0,0 +1,5 @@
+ARG BASE_IMAGE=python:3.11-slim
+FROM --platform=linux/amd64 $BASE_IMAGE AS base
+FROM base AS test
+FROM scratch AS export
+FROM alpine:3.19
diff --git a/test/fixtures/docker/docker-compose.yml b/test/fixtures/docker/docker-compose.yml
new file mode 100644
index 0000000..d44c448
--- /dev/null
+++ b/test/fixtures/docker/docker-compose.yml
@@ -0,0 +1,7 @@
+services:
+ api:
+ image: redis:7.2
+ worker:
+ build: .
+ db:
+ image: postgres:16
diff --git a/test/fixtures/go/go.mod b/test/fixtures/go/go.mod
new file mode 100644
index 0000000..8877575
--- /dev/null
+++ b/test/fixtures/go/go.mod
@@ -0,0 +1,8 @@
+module example.com/fixture
+
+go 1.21
+
+require (
+ github.com/gin-gonic/gin v1.9.1
+ github.com/stretchr/testify v1.8.4 // indirect
+)
diff --git a/test/fixtures/gradle/build.gradle b/test/fixtures/gradle/build.gradle
new file mode 100644
index 0000000..1c95cc4
--- /dev/null
+++ b/test/fixtures/gradle/build.gradle
@@ -0,0 +1,8 @@
+plugins {
+ id "java"
+}
+
+dependencies {
+ implementation "org.springframework:spring-core:6.1.0"
+ testImplementation "junit:junit:4.13.2"
+}
diff --git a/test/fixtures/gradle/gradle.lockfile b/test/fixtures/gradle/gradle.lockfile
new file mode 100644
index 0000000..458d7fb
--- /dev/null
+++ b/test/fixtures/gradle/gradle.lockfile
@@ -0,0 +1,2 @@
+org.springframework:spring-core:6.1.0=compileClasspath
+junit:junit:4.13.2=testCompileClasspath
diff --git a/test/fixtures/helm/Chart.lock b/test/fixtures/helm/Chart.lock
new file mode 100644
index 0000000..3f291a3
--- /dev/null
+++ b/test/fixtures/helm/Chart.lock
@@ -0,0 +1,5 @@
+dependencies:
+ - name: redis
+ version: 19.6.0
+digest: sha256:fixture
+generated: "2026-04-05T00:00:00Z"
diff --git a/test/fixtures/helm/Chart.yaml b/test/fixtures/helm/Chart.yaml
new file mode 100644
index 0000000..2c94405
--- /dev/null
+++ b/test/fixtures/helm/Chart.yaml
@@ -0,0 +1,6 @@
+apiVersion: v2
+name: fixture-chart
+version: 0.1.0
+dependencies:
+ - name: redis
+ version: 19.6.0
diff --git a/test/fixtures/hex/mix.exs b/test/fixtures/hex/mix.exs
new file mode 100644
index 0000000..61c6617
--- /dev/null
+++ b/test/fixtures/hex/mix.exs
@@ -0,0 +1,16 @@
+defmodule FixtureHex.MixProject do
+ use Mix.Project
+
+ def project do
+ [
+ app: :fixture_hex,
+ version: "0.1.0"
+ ]
+ end
+
+ defp deps do
+ [
+ {:jason, "~> 1.4"}
+ ]
+ end
+end
diff --git a/test/fixtures/hex/mix.lock b/test/fixtures/hex/mix.lock
new file mode 100644
index 0000000..4847e99
--- /dev/null
+++ b/test/fixtures/hex/mix.lock
@@ -0,0 +1,3 @@
+%{
+ "jason": {:hex, :jason, "1.4.1", "checksum", [:mix], [], "hexpm", "checksum"}
+}
diff --git a/test/fixtures/maven/dependency-tree.txt b/test/fixtures/maven/dependency-tree.txt
new file mode 100644
index 0000000..51150ba
--- /dev/null
+++ b/test/fixtures/maven/dependency-tree.txt
@@ -0,0 +1,3 @@
+[INFO] +- org.springframework.boot:spring-boot-starter-web:jar:3.2.0:compile
+[INFO] | \- org.springframework:spring-core:jar:6.1.0:compile
+[INFO] \- junit:junit:jar:4.13.2:test
diff --git a/test/fixtures/maven/pom.xml b/test/fixtures/maven/pom.xml
new file mode 100644
index 0000000..1146e69
--- /dev/null
+++ b/test/fixtures/maven/pom.xml
@@ -0,0 +1,14 @@
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+ 3.2.0
+
+
+ junit
+ junit
+ test
+
+
+
diff --git a/test/fixtures/npm/package-lock.json b/test/fixtures/npm/package-lock.json
new file mode 100644
index 0000000..5b9e20e
--- /dev/null
+++ b/test/fixtures/npm/package-lock.json
@@ -0,0 +1,29 @@
+{
+ "name": "fixture-app",
+ "lockfileVersion": 3,
+ "packages": {
+ "": {
+ "name": "fixture-app",
+ "version": "1.0.0",
+ "dependencies": {
+ "express": "^4.18.2",
+ "@scope/pkg": "^1.0.0"
+ }
+ },
+ "node_modules/express": {
+ "version": "4.18.2",
+ "dependencies": {
+ "accepts": "~1.3.8"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "1.3.8"
+ },
+ "node_modules/@scope/pkg": {
+ "version": "1.0.0"
+ },
+ "node_modules/another/node_modules/accepts": {
+ "version": "1.3.8"
+ }
+ }
+}
diff --git a/test/fixtures/npm/package.json b/test/fixtures/npm/package.json
new file mode 100644
index 0000000..71c3c31
--- /dev/null
+++ b/test/fixtures/npm/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "fixture-app",
+ "version": "1.0.0",
+ "dependencies": {
+ "express": "^4.18.2",
+ "@scope/pkg": "^1.0.0"
+ }
+}
diff --git a/test/fixtures/npm/pnpm-lock.yaml b/test/fixtures/npm/pnpm-lock.yaml
new file mode 100644
index 0000000..6888293
--- /dev/null
+++ b/test/fixtures/npm/pnpm-lock.yaml
@@ -0,0 +1,20 @@
+lockfileVersion: "9.0"
+importers:
+ .:
+ dependencies:
+ express:
+ specifier: ^4.18.2
+ version: 4.18.2
+ "@scope/pkg":
+ specifier: ^1.0.0
+ version: 1.0.0
+packages:
+ express@4.18.2:
+ dependencies:
+ accepts: 1.3.8
+ accepts@1.3.8:
+ resolution:
+ integrity: sha512-abc
+ "@scope/pkg@1.0.0":
+ resolution:
+ integrity: sha512-def
diff --git a/test/fixtures/npm/yarn.lock b/test/fixtures/npm/yarn.lock
new file mode 100644
index 0000000..69e5716
--- /dev/null
+++ b/test/fixtures/npm/yarn.lock
@@ -0,0 +1,10 @@
+express@^4.18.2:
+ version "4.18.2"
+ dependencies:
+ accepts "~1.3.8"
+
+accepts@~1.3.8:
+ version "1.3.8"
+
+"@scope/pkg@^1.0.0":
+ version "1.0.0"
diff --git a/test/fixtures/nuget/Fixture.csproj b/test/fixtures/nuget/Fixture.csproj
new file mode 100644
index 0000000..cbf45f6
--- /dev/null
+++ b/test/fixtures/nuget/Fixture.csproj
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/test/fixtures/nuget/packages.lock.json b/test/fixtures/nuget/packages.lock.json
new file mode 100644
index 0000000..550fcac
--- /dev/null
+++ b/test/fixtures/nuget/packages.lock.json
@@ -0,0 +1,18 @@
+{
+ "version": 1,
+ "dependencies": {
+ ".NETCoreApp,Version=v8.0": {
+ "Newtonsoft.Json": {
+ "type": "Direct",
+ "resolved": "13.0.3",
+ "dependencies": {
+ "System.Runtime.CompilerServices.Unsafe": "6.0.0"
+ }
+ },
+ "System.Runtime.CompilerServices.Unsafe": {
+ "type": "Transitive",
+ "resolved": "6.0.0"
+ }
+ }
+ }
+}
diff --git a/test/fixtures/python/Pipfile.lock b/test/fixtures/python/Pipfile.lock
new file mode 100644
index 0000000..5687d4c
--- /dev/null
+++ b/test/fixtures/python/Pipfile.lock
@@ -0,0 +1,12 @@
+{
+ "default": {
+ "flask": {
+ "version": "==2.3.0"
+ }
+ },
+ "develop": {
+ "pytest": {
+ "version": "==8.2.0"
+ }
+ }
+}
diff --git a/test/fixtures/python/poetry.lock b/test/fixtures/python/poetry.lock
new file mode 100644
index 0000000..d2d422d
--- /dev/null
+++ b/test/fixtures/python/poetry.lock
@@ -0,0 +1,14 @@
+[[package]]
+name = "flask"
+version = "2.3.0"
+
+[package.dependencies]
+click = ">=8.0"
+
+[[package]]
+name = "requests"
+version = "2.28.0"
+
+[[package]]
+name = "click"
+version = "8.1.7"
diff --git a/test/fixtures/python/pyproject.toml b/test/fixtures/python/pyproject.toml
new file mode 100644
index 0000000..5f49c62
--- /dev/null
+++ b/test/fixtures/python/pyproject.toml
@@ -0,0 +1,15 @@
+[tool.poetry]
+name = "fixture-python"
+version = "0.1.0"
+
+[tool.poetry.dependencies]
+python = "^3.11"
+# Web
+flask = "^2.3.0"
+requests = { version = "^2.28.0", optional = true } # client
+
+[project]
+name = "fixture-python"
+dependencies = [
+ "fastapi==0.111.0", # API
+]
diff --git a/test/fixtures/python/requirements.txt b/test/fixtures/python/requirements.txt
new file mode 100644
index 0000000..56868ab
--- /dev/null
+++ b/test/fixtures/python/requirements.txt
@@ -0,0 +1,2 @@
+flask==2.3.0
+requests>=2.28.0
diff --git a/test/fixtures/python/uv.lock b/test/fixtures/python/uv.lock
new file mode 100644
index 0000000..f003f62
--- /dev/null
+++ b/test/fixtures/python/uv.lock
@@ -0,0 +1,18 @@
+[[package]]
+name = "fixture-python"
+version = "0.1.0"
+source = { editable = "." }
+dependencies = [{ name = "fastapi" }]
+
+[[package]]
+name = "fastapi"
+version = "0.111.0"
+dependencies = ["starlette", "pydantic"]
+
+[[package]]
+name = "starlette"
+version = "0.37.2"
+
+[[package]]
+name = "pydantic"
+version = "2.7.0"
diff --git a/test/fixtures/ruby/Gemfile b/test/fixtures/ruby/Gemfile
new file mode 100644
index 0000000..cc4b5e5
--- /dev/null
+++ b/test/fixtures/ruby/Gemfile
@@ -0,0 +1,3 @@
+source "https://rubygems.org"
+
+gem "rails", "~> 7.1.3"
diff --git a/test/fixtures/ruby/Gemfile.lock b/test/fixtures/ruby/Gemfile.lock
new file mode 100644
index 0000000..035bc10
--- /dev/null
+++ b/test/fixtures/ruby/Gemfile.lock
@@ -0,0 +1,9 @@
+GEM
+ remote: https://rubygems.org/
+ specs:
+ rails (7.1.3)
+ actionpack (= 7.1.3)
+ actionpack (7.1.3)
+
+DEPENDENCIES
+ rails (~> 7.1.3)
diff --git a/test/fixtures/swift/Package.resolved b/test/fixtures/swift/Package.resolved
new file mode 100644
index 0000000..4294e8a
--- /dev/null
+++ b/test/fixtures/swift/Package.resolved
@@ -0,0 +1,14 @@
+{
+ "pins": [
+ {
+ "identity": "alamofire",
+ "kind": "remoteSourceControl",
+ "location": "https://github.com/Alamofire/Alamofire.git",
+ "state": {
+ "revision": "abcdef1234567890",
+ "version": "5.8.0"
+ }
+ }
+ ],
+ "version": 2
+}
diff --git a/test/fixtures/swift/Package.swift b/test/fixtures/swift/Package.swift
new file mode 100644
index 0000000..206e9ed
--- /dev/null
+++ b/test/fixtures/swift/Package.swift
@@ -0,0 +1,9 @@
+// swift-tools-version: 5.9
+import PackageDescription
+
+let package = Package(
+ name: "FixtureSwift",
+ dependencies: [
+ .package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.8.0")
+ ]
+)
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/test/helpers/fixtureWorkspace.js b/test/helpers/fixtureWorkspace.js
new file mode 100644
index 0000000..c074d2d
--- /dev/null
+++ b/test/helpers/fixtureWorkspace.js
@@ -0,0 +1,46 @@
+const fs = require("fs");
+const os = require("os");
+const path = require("path");
+
+async function makeTempWorkspace(prefix = "cloudsmith-lockfile-") {
+ return fs.promises.mkdtemp(path.join(os.tmpdir(), prefix));
+}
+
+async function copyFixtureDir(fixtureName, targetDir) {
+ const sourceDir = path.join(__dirname, "..", "fixtures", fixtureName);
+ await copyDirectory(sourceDir, targetDir);
+}
+
+async function copyDirectory(sourceDir, targetDir) {
+ await fs.promises.mkdir(targetDir, { recursive: true });
+ const entries = await fs.promises.readdir(sourceDir, { withFileTypes: true });
+
+ for (const entry of entries) {
+ const sourcePath = path.join(sourceDir, entry.name);
+ const targetPath = path.join(targetDir, entry.name);
+
+ if (entry.isDirectory()) {
+ await copyDirectory(sourcePath, targetPath);
+ continue;
+ }
+
+ await fs.promises.copyFile(sourcePath, targetPath);
+ }
+}
+
+async function writeTextFile(targetPath, content) {
+ await fs.promises.mkdir(path.dirname(targetPath), { recursive: true });
+ await fs.promises.writeFile(targetPath, content, "utf8");
+}
+
+async function removeDirectory(targetDir) {
+ await fs.promises.rm(targetDir, { recursive: true, force: true });
+}
+
+module.exports = {
+ copyDirectory,
+ copyFixtureDir,
+ makeTempWorkspace,
+ removeDirectory,
+ writeTextFile,
+};
diff --git a/test/lockfileParsers/cargoParser.test.js b/test/lockfileParsers/cargoParser.test.js
new file mode 100644
index 0000000..c046811
--- /dev/null
+++ b/test/lockfileParsers/cargoParser.test.js
@@ -0,0 +1,119 @@
+const assert = require("assert");
+const path = require("path");
+const cargoParser = require("../../util/lockfileParsers/cargoParser");
+const {
+ makeTempWorkspace,
+ removeDirectory,
+ writeTextFile,
+} = require("../helpers/fixtureWorkspace");
+
+suite("cargoParser Test Suite", () => {
+ const fixtureDir = path.join(__dirname, "..", "fixtures", "cargo");
+ const tempDirs = [];
+
+ async function createWorkspace() {
+ const workspace = await makeTempWorkspace("cloudsmith-cargo-parser-");
+ tempDirs.push(workspace);
+ return workspace;
+ }
+
+ suiteTeardown(async () => {
+ await Promise.all(tempDirs.map((tempDir) => removeDirectory(tempDir)));
+ });
+
+ test("resolves Cargo.lock uniquely, skips the root package, and marks direct dependencies from Cargo.toml", async () => {
+ const tree = await cargoParser.resolve({
+ lockfilePath: path.join(fixtureDir, "Cargo.lock"),
+ manifestPath: path.join(fixtureDir, "Cargo.toml"),
+ });
+
+ assert.strictEqual(tree.sourceFile, "Cargo.lock");
+ assert.strictEqual(tree.dependencies.length, 3);
+ assert.strictEqual(tree.dependencies.some((dependency) => dependency.name === "fixture-cargo"), false);
+
+ const serde = tree.dependencies.find((dependency) => dependency.name === "serde");
+ const tokio = tree.dependencies.find((dependency) => dependency.name === "tokio");
+ const bytes = tree.dependencies.find((dependency) => dependency.name === "bytes");
+
+ assert.ok(serde);
+ assert.ok(tokio);
+ assert.ok(bytes);
+ assert.strictEqual(serde.isDirect, true);
+ assert.strictEqual(tokio.isDirect, true);
+ assert.strictEqual(bytes.isDirect, false);
+ assert.deepStrictEqual(bytes.parentChain, ["tokio"]);
+ });
+
+ test("detect returns no matches when Cargo files are missing", async () => {
+ const workspace = await createWorkspace();
+
+ const matches = await cargoParser.detect(workspace);
+
+ assert.deepStrictEqual(matches, []);
+ assert.strictEqual(await cargoParser.canResolve(workspace), false);
+ });
+
+ test("throws for malformed Cargo.lock files", async () => {
+ const workspace = await createWorkspace();
+ const lockfilePath = path.join(workspace, "Cargo.lock");
+ const manifestPath = path.join(workspace, "Cargo.toml");
+ await writeTextFile(lockfilePath, "[[package]]\nname = \"broken\"\n");
+ await writeTextFile(manifestPath, "[dependencies]\nserde = \"1.0.0\"\n");
+
+ await assert.rejects(
+ () => cargoParser.resolve({ lockfilePath, manifestPath }),
+ /Malformed Cargo\.lock: no package entries found/
+ );
+ });
+
+ test("deduplicates large Cargo graphs down to unique packages", async () => {
+ const workspace = await createWorkspace();
+ const lockfilePath = path.join(workspace, "Cargo.lock");
+ const manifestPath = path.join(workspace, "Cargo.toml");
+ const packageCount = 300;
+ const registrySource = "registry+https://github.com/rust-lang/crates.io-index";
+
+ const manifestLines = [
+ "[package]",
+ 'name = "fixture-cargo"',
+ 'version = "0.1.0"',
+ "",
+ "[dependencies]",
+ 'crate-000 = "1.0.0"',
+ ];
+
+ const lockEntries = [];
+ for (let index = 0; index < packageCount; index += 1) {
+ const currentName = `crate-${String(index).padStart(3, "0")}`;
+ const nextName = index + 1 < packageCount
+ ? `crate-${String(index + 1).padStart(3, "0")}`
+ : null;
+ lockEntries.push(
+ [
+ "[[package]]",
+ `name = "${currentName}"`,
+ 'version = "1.0.0"',
+ `source = "${registrySource}"`,
+ nextName
+ ? `dependencies = ["${nextName} 1.0.0"]`
+ : "",
+ "",
+ ].filter(Boolean).join("\n")
+ );
+ }
+
+ await writeTextFile(manifestPath, manifestLines.join("\n"));
+ await writeTextFile(lockfilePath, lockEntries.join("\n"));
+
+ const tree = await cargoParser.resolve({
+ lockfilePath,
+ manifestPath,
+ });
+
+ assert.strictEqual(tree.dependencies.length, packageCount);
+ assert.strictEqual(
+ new Set(tree.dependencies.map((dependency) => `${dependency.name}@${dependency.version}`)).size,
+ packageCount
+ );
+ });
+});
diff --git a/test/lockfileParsers/dockerParser.test.js b/test/lockfileParsers/dockerParser.test.js
new file mode 100644
index 0000000..ec6d670
--- /dev/null
+++ b/test/lockfileParsers/dockerParser.test.js
@@ -0,0 +1,78 @@
+const assert = require("assert");
+const path = require("path");
+const dockerParser = require("../../util/lockfileParsers/dockerParser");
+const {
+ makeTempWorkspace,
+ removeDirectory,
+ writeTextFile,
+} = require("../helpers/fixtureWorkspace");
+
+suite("dockerParser Test Suite", () => {
+ const fixtureDir = path.join(__dirname, "..", "fixtures", "docker");
+ const tempDirs = [];
+
+ async function createWorkspace() {
+ const workspace = await makeTempWorkspace("cloudsmith-docker-parser-");
+ tempDirs.push(workspace);
+ return workspace;
+ }
+
+ suiteTeardown(async () => {
+ await Promise.all(tempDirs.map((tempDir) => removeDirectory(tempDir)));
+ });
+
+ test("parses Dockerfile FROM instructions and skips scratch and stage references", async () => {
+ const tree = await dockerParser.resolve({
+ lockfilePath: path.join(fixtureDir, "Dockerfile"),
+ });
+
+ assert.strictEqual(tree.sourceFile, "Dockerfile");
+ assert.deepStrictEqual(
+ tree.dependencies.map((dependency) => `${dependency.name}:${dependency.version}`),
+ ["python:3.11-slim", "alpine:3.19"]
+ );
+ });
+
+ test("parses docker-compose images and skips build-only services", async () => {
+ const tree = await dockerParser.resolve({
+ lockfilePath: path.join(fixtureDir, "docker-compose.yml"),
+ });
+
+ assert.strictEqual(tree.sourceFile, "docker-compose.yml");
+ assert.deepStrictEqual(
+ tree.dependencies.map((dependency) => `${dependency.name}:${dependency.version}`),
+ ["redis:7.2", "postgres:16"]
+ );
+ });
+
+ test("detect returns no matches when Docker manifests are missing", async () => {
+ const workspace = await createWorkspace();
+
+ const matches = await dockerParser.detect(workspace);
+
+ assert.deepStrictEqual(matches, []);
+ assert.strictEqual(await dockerParser.canResolve(workspace), false);
+ });
+
+ test("detect returns no matches for invalid workspace roots", async () => {
+ const workspace = await createWorkspace();
+ const matches = await dockerParser.detect(path.join(workspace, "missing-workspace"));
+
+ assert.deepStrictEqual(matches, []);
+ });
+
+ test("ignores malformed FROM lines that do not resolve to image references", async () => {
+ const workspace = await createWorkspace();
+ const lockfilePath = path.join(workspace, "Dockerfile");
+ await writeTextFile(lockfilePath, [
+ "ARG BASE_IMAGE",
+ "FROM $BASE_IMAGE",
+ "FROM scratch",
+ "",
+ ].join("\n"));
+
+ const tree = await dockerParser.resolve({ lockfilePath });
+
+ assert.strictEqual(tree.dependencies.length, 0);
+ });
+});
diff --git a/test/lockfileParsers/mavenParser.test.js b/test/lockfileParsers/mavenParser.test.js
new file mode 100644
index 0000000..a95cdba
--- /dev/null
+++ b/test/lockfileParsers/mavenParser.test.js
@@ -0,0 +1,81 @@
+const assert = require("assert");
+const path = require("path");
+const mavenParser = require("../../util/lockfileParsers/mavenParser");
+const {
+ makeTempWorkspace,
+ removeDirectory,
+ writeTextFile,
+} = require("../helpers/fixtureWorkspace");
+
+suite("mavenParser Test Suite", () => {
+ const fixtureDir = path.join(__dirname, "..", "fixtures", "maven");
+ const tempDirs = [];
+
+ async function createWorkspace() {
+ const workspace = await makeTempWorkspace("cloudsmith-maven-parser-");
+ tempDirs.push(workspace);
+ return workspace;
+ }
+
+ suiteTeardown(async () => {
+ await Promise.all(tempDirs.map((tempDir) => removeDirectory(tempDir)));
+ });
+
+ test("hydrates direct dependencies from pom.xml and transitives from dependency-tree.txt", async () => {
+ const tree = await mavenParser.resolve({
+ lockfilePath: path.join(fixtureDir, "dependency-tree.txt"),
+ manifestPath: path.join(fixtureDir, "pom.xml"),
+ });
+
+ assert.strictEqual(tree.sourceFile, "pom.xml");
+ assert.strictEqual(tree.dependencies.length, 3);
+
+ const starter = tree.dependencies.find((dependency) => (
+ dependency.name === "org.springframework.boot:spring-boot-starter-web"
+ ));
+ const springCore = tree.dependencies.find((dependency) => dependency.name === "org.springframework:spring-core");
+ const junit = tree.dependencies.find((dependency) => dependency.name === "junit:junit");
+
+ assert.ok(starter);
+ assert.ok(springCore);
+ assert.ok(junit);
+ assert.strictEqual(starter.isDirect, true);
+ assert.strictEqual(springCore.isDirect, false);
+ assert.strictEqual(junit.version, "4.13.2");
+ assert.strictEqual(junit.isDevelopmentDependency, true);
+ });
+
+ test("detect returns no matches when pom.xml is missing", async () => {
+ const workspace = await createWorkspace();
+
+ const matches = await mavenParser.detect(workspace);
+
+ assert.deepStrictEqual(matches, []);
+ assert.strictEqual(await mavenParser.canResolve(workspace), false);
+ });
+
+ test("ignores malformed dependency tree lines and still returns manifest dependencies", async () => {
+ const workspace = await createWorkspace();
+ const manifestPath = path.join(workspace, "pom.xml");
+ const lockfilePath = path.join(workspace, "dependency-tree.txt");
+ await writeTextFile(manifestPath, [
+ "",
+ " ",
+ " ",
+ " org.springframework.boot",
+ " spring-boot-starter",
+ " 3.2.0",
+ " ",
+ " ",
+ "",
+ "",
+ ].join("\n"));
+ await writeTextFile(lockfilePath, "this is not a Maven dependency tree\n");
+
+ const tree = await mavenParser.resolve({ lockfilePath, manifestPath });
+
+ assert.strictEqual(tree.dependencies.length, 1);
+ assert.strictEqual(tree.dependencies[0].name, "org.springframework.boot:spring-boot-starter");
+ assert.strictEqual(tree.dependencies[0].isDirect, true);
+ });
+});
diff --git a/test/lockfileParsers/npmParser.test.js b/test/lockfileParsers/npmParser.test.js
new file mode 100644
index 0000000..1a5f0ca
--- /dev/null
+++ b/test/lockfileParsers/npmParser.test.js
@@ -0,0 +1,258 @@
+const assert = require("assert");
+const path = require("path");
+const npmParser = require("../../util/lockfileParsers/npmParser");
+const {
+ makeTempWorkspace,
+ removeDirectory,
+ writeTextFile,
+} = require("../helpers/fixtureWorkspace");
+
+suite("npmParser Test Suite", () => {
+ const fixtureDir = path.join(__dirname, "..", "fixtures", "npm");
+ const tempDirs = [];
+
+ async function createWorkspace() {
+ const workspace = await makeTempWorkspace("cloudsmith-npm-parser-");
+ tempDirs.push(workspace);
+ return workspace;
+ }
+
+ suiteTeardown(async () => {
+ await Promise.all(tempDirs.map((tempDir) => removeDirectory(tempDir)));
+ });
+
+ test("resolves package-lock.json with deduplication, scoped packages, and root skipping", async () => {
+ const tree = await npmParser.resolve({
+ lockfilePath: path.join(fixtureDir, "package-lock.json"),
+ manifestPath: path.join(fixtureDir, "package.json"),
+ options: { maxDependenciesToScan: 10000 },
+ });
+
+ assert.strictEqual(tree.sourceFile, "package-lock.json");
+ assert.strictEqual(tree.dependencies.length, 3);
+ assert.strictEqual(tree.dependencies.some((dependency) => dependency.name === "fixture-app"), false);
+
+ const express = tree.dependencies.find((dependency) => dependency.name === "express");
+ const accepts = tree.dependencies.find((dependency) => dependency.name === "accepts");
+ const scoped = tree.dependencies.find((dependency) => dependency.name === "@scope/pkg");
+
+ assert.ok(express);
+ assert.ok(accepts);
+ assert.ok(scoped);
+ assert.strictEqual(express.isDirect, true);
+ assert.strictEqual(accepts.isDirect, false);
+ assert.deepStrictEqual(accepts.parentChain, ["express"]);
+ assert.strictEqual(scoped.version, "1.0.0");
+ });
+
+ test("resolves yarn.lock fixtures", async () => {
+ const tree = await npmParser.resolve({
+ lockfilePath: path.join(fixtureDir, "yarn.lock"),
+ manifestPath: path.join(fixtureDir, "package.json"),
+ options: { maxDependenciesToScan: 10000 },
+ });
+
+ assert.strictEqual(tree.sourceFile, "yarn.lock");
+ assert.strictEqual(tree.dependencies.length, 3);
+ assert.ok(tree.dependencies.some((dependency) => dependency.name === "@scope/pkg"));
+ });
+
+ test("preserves multiple resolved versions for the same yarn package", async () => {
+ const workspace = await createWorkspace();
+ const lockfilePath = path.join(workspace, "yarn.lock");
+ const manifestPath = path.join(workspace, "package.json");
+
+ await writeTextFile(manifestPath, JSON.stringify({
+ name: "fixture-app",
+ version: "1.0.0",
+ dependencies: {
+ "package-a": "^1.0.0",
+ "package-b": "^1.0.0",
+ },
+ }, null, 2));
+
+ await writeTextFile(lockfilePath, [
+ "package-a@^1.0.0:",
+ ' version "1.0.0"',
+ " dependencies:",
+ ' left-pad "^1.0.0"',
+ "",
+ "package-b@^1.0.0:",
+ ' version "1.0.0"',
+ " dependencies:",
+ ' left-pad "^2.0.0"',
+ "",
+ "left-pad@^1.0.0:",
+ ' version "1.0.1"',
+ "",
+ "left-pad@^2.0.0:",
+ ' version "2.0.0"',
+ "",
+ ].join("\n"));
+
+ const tree = await npmParser.resolve({
+ lockfilePath,
+ manifestPath,
+ options: { maxDependenciesToScan: 10000 },
+ });
+
+ const packageKeys = tree.dependencies.map((dependency) => `${dependency.name}@${dependency.version}`);
+ const packageA = tree.dependencies.find((dependency) => dependency.name === "package-a");
+ const packageB = tree.dependencies.find((dependency) => dependency.name === "package-b");
+
+ assert.ok(packageA);
+ assert.ok(packageB);
+ assert.strictEqual(packageA.transitives[0].version, "1.0.1");
+ assert.strictEqual(packageB.transitives[0].version, "2.0.0");
+ assert.ok(packageKeys.includes("left-pad@1.0.1"));
+ assert.ok(packageKeys.includes("left-pad@2.0.0"));
+ assert.strictEqual(packageKeys.filter((key) => key.startsWith("left-pad@")).length, 2);
+ });
+
+ test("resolves pnpm-lock.yaml fixtures", async () => {
+ const tree = await npmParser.resolve({
+ lockfilePath: path.join(fixtureDir, "pnpm-lock.yaml"),
+ manifestPath: path.join(fixtureDir, "package.json"),
+ options: { maxDependenciesToScan: 10000 },
+ });
+
+ assert.strictEqual(tree.sourceFile, "pnpm-lock.yaml");
+ assert.strictEqual(tree.dependencies.length, 3);
+ assert.ok(tree.dependencies.some((dependency) => dependency.name === "accepts"));
+ });
+
+ test("detect returns no matches when npm lockfiles are missing", async () => {
+ const workspace = await createWorkspace();
+
+ const matches = await npmParser.detect(workspace);
+
+ assert.deepStrictEqual(matches, []);
+ assert.strictEqual(await npmParser.canResolve(workspace), false);
+ });
+
+ test("resolve ignores manifests outside the provided workspace folder", async () => {
+ const workspace = await createWorkspace();
+ const outsideWorkspace = await createWorkspace();
+ const lockfilePath = path.join(workspace, "package-lock.json");
+ const manifestPath = path.join(outsideWorkspace, "package.json");
+
+ await writeTextFile(lockfilePath, JSON.stringify({
+ name: "fixture-app",
+ lockfileVersion: 3,
+ packages: {
+ "": {},
+ "node_modules/express": {
+ version: "4.18.2",
+ },
+ },
+ }, null, 2));
+ await writeTextFile(manifestPath, JSON.stringify({
+ dependencies: {
+ express: "^4.18.0",
+ },
+ }, null, 2));
+
+ const tree = await npmParser.resolve({
+ workspaceFolder: workspace,
+ lockfilePath,
+ manifestPath,
+ options: { maxDependenciesToScan: 10000 },
+ });
+
+ const express = tree.dependencies.find((dependency) => dependency.name === "express");
+
+ assert.ok(express);
+ assert.strictEqual(
+ express.isDirect,
+ false,
+ "out-of-workspace manifests should not influence direct dependency classification"
+ );
+ });
+
+ test("throws for malformed package-lock.json files", async () => {
+ const workspace = await createWorkspace();
+ const lockfilePath = path.join(workspace, "package-lock.json");
+ const manifestPath = path.join(workspace, "package.json");
+ await writeTextFile(lockfilePath, "{\n \"name\": \"broken\"\n}\n");
+ await writeTextFile(manifestPath, "{\n \"dependencies\": {}\n}\n");
+
+ await assert.rejects(
+ () => npmParser.resolve({
+ lockfilePath,
+ manifestPath,
+ options: { maxDependenciesToScan: 10000 },
+ }),
+ /missing packages object/
+ );
+ });
+
+ test("adds a warning when the unique dependency count exceeds the scan cap", async () => {
+ const tree = await npmParser.resolve({
+ lockfilePath: path.join(fixtureDir, "package-lock.json"),
+ manifestPath: path.join(fixtureDir, "package.json"),
+ options: { maxDependenciesToScan: 2 },
+ });
+
+ assert.strictEqual(tree.warnings.length, 1);
+ assert.match(tree.warnings[0], /Display is capped at 2 dependencies/);
+ });
+
+ test("includes orphaned package-lock entries once even when duplicate package records share a key", async () => {
+ const workspace = await createWorkspace();
+ const lockfilePath = path.join(workspace, "package-lock.json");
+ const manifestPath = path.join(workspace, "package.json");
+
+ await writeTextFile(manifestPath, JSON.stringify({
+ name: "fixture-app",
+ version: "1.0.0",
+ dependencies: {
+ express: "1.0.0",
+ },
+ }, null, 2));
+
+ await writeTextFile(lockfilePath, JSON.stringify({
+ name: "fixture-app",
+ version: "1.0.0",
+ lockfileVersion: 3,
+ packages: {
+ "": {
+ dependencies: {
+ express: "1.0.0",
+ },
+ },
+ "node_modules/express": {
+ version: "1.0.0",
+ dependencies: {
+ accepts: "1.0.0",
+ shared: "1.0.0",
+ },
+ },
+ "node_modules/accepts": {
+ version: "1.0.0",
+ },
+ "node_modules/shared": {
+ version: "1.0.0",
+ },
+ "node_modules/express/node_modules/shared": {
+ version: "1.0.0",
+ },
+ "node_modules/orphan": {
+ version: "2.0.0",
+ },
+ },
+ }, null, 2));
+
+ const tree = await npmParser.resolve({
+ lockfilePath,
+ manifestPath,
+ options: { maxDependenciesToScan: 10000 },
+ });
+
+ const packageKeys = tree.dependencies.map((dependency) => `${dependency.name}@${dependency.version}`);
+
+ assert.strictEqual(packageKeys.filter((key) => key === "shared@1.0.0").length, 1);
+ assert.strictEqual(packageKeys.filter((key) => key === "orphan@2.0.0").length, 1);
+ assert.ok(packageKeys.includes("express@1.0.0"));
+ assert.ok(packageKeys.includes("accepts@1.0.0"));
+ });
+});
diff --git a/test/lockfileParsers/nugetParser.test.js b/test/lockfileParsers/nugetParser.test.js
new file mode 100644
index 0000000..68454ca
--- /dev/null
+++ b/test/lockfileParsers/nugetParser.test.js
@@ -0,0 +1,28 @@
+const assert = require("assert");
+const path = require("path");
+const nugetParser = require("../../util/lockfileParsers/nugetParser");
+const {
+ makeTempWorkspace,
+ removeDirectory,
+} = require("../helpers/fixtureWorkspace");
+
+suite("nugetParser Test Suite", () => {
+ const tempDirs = [];
+
+ async function createWorkspace() {
+ const workspace = await makeTempWorkspace("cloudsmith-nuget-parser-");
+ tempDirs.push(workspace);
+ return workspace;
+ }
+
+ suiteTeardown(async () => {
+ await Promise.all(tempDirs.map((tempDir) => removeDirectory(tempDir)));
+ });
+
+ test("detect returns no matches for invalid workspace roots", async () => {
+ const workspace = await createWorkspace();
+ const matches = await nugetParser.detect(path.join(workspace, "missing-workspace"));
+
+ assert.deepStrictEqual(matches, []);
+ });
+});
diff --git a/test/lockfileParsers/pythonParser.test.js b/test/lockfileParsers/pythonParser.test.js
new file mode 100644
index 0000000..f040ea1
--- /dev/null
+++ b/test/lockfileParsers/pythonParser.test.js
@@ -0,0 +1,109 @@
+const assert = require("assert");
+const path = require("path");
+const pythonParser = require("../../util/lockfileParsers/pythonParser");
+const {
+ makeTempWorkspace,
+ removeDirectory,
+ writeTextFile,
+} = require("../helpers/fixtureWorkspace");
+
+suite("pythonParser Test Suite", () => {
+ const fixtureDir = path.join(__dirname, "..", "fixtures", "python");
+ const tempDirs = [];
+
+ async function createWorkspace() {
+ const workspace = await makeTempWorkspace("cloudsmith-python-parser-");
+ tempDirs.push(workspace);
+ return workspace;
+ }
+
+ suiteTeardown(async () => {
+ await Promise.all(tempDirs.map((tempDir) => removeDirectory(tempDir)));
+ });
+
+ test("resolves poetry.lock and keeps all package entries while marking directs from pyproject.toml", async () => {
+ const tree = await pythonParser.resolve({
+ lockfilePath: path.join(fixtureDir, "poetry.lock"),
+ manifestPath: path.join(fixtureDir, "pyproject.toml"),
+ });
+
+ assert.strictEqual(tree.sourceFile, "poetry.lock");
+ assert.strictEqual(tree.dependencies.length, 3);
+
+ const flask = tree.dependencies.find((dependency) => dependency.name === "flask");
+ const requests = tree.dependencies.find((dependency) => dependency.name === "requests");
+ const click = tree.dependencies.find((dependency) => dependency.name === "click");
+
+ assert.ok(flask);
+ assert.ok(requests);
+ assert.ok(click);
+ assert.strictEqual(flask.isDirect, true);
+ assert.strictEqual(requests.isDirect, true);
+ assert.strictEqual(click.isDirect, false);
+ assert.deepStrictEqual(click.parentChain, ["flask"]);
+ });
+
+ test("skips the editable uv root package and resolves its transitive dependencies", async () => {
+ const tree = await pythonParser.resolve({
+ lockfilePath: path.join(fixtureDir, "uv.lock"),
+ manifestPath: path.join(fixtureDir, "pyproject.toml"),
+ });
+
+ assert.strictEqual(tree.sourceFile, "uv.lock");
+ assert.strictEqual(tree.dependencies.some((dependency) => dependency.name === "fixture-python"), false);
+ assert.strictEqual(tree.dependencies.length, 3);
+ assert.ok(tree.dependencies.some((dependency) => dependency.name === "fastapi" && dependency.isDirect));
+ assert.ok(tree.dependencies.some((dependency) => dependency.name === "starlette" && !dependency.isDirect));
+ assert.ok(tree.dependencies.some((dependency) => dependency.name === "pydantic" && !dependency.isDirect));
+ });
+
+ test("warns when only requirements.txt is available", async () => {
+ const tree = await pythonParser.resolve({
+ lockfilePath: path.join(fixtureDir, "requirements.txt"),
+ });
+
+ assert.strictEqual(tree.sourceFile, "requirements.txt");
+ assert.strictEqual(tree.dependencies.length, 2);
+ assert.strictEqual(tree.dependencies.every((dependency) => dependency.isDirect), true);
+ assert.strictEqual(tree.warnings.length, 1);
+ assert.match(tree.warnings[0], /requirements\.txt does not encode transitive dependencies/i);
+ });
+
+ test("detect returns no matches when Python dependency files are missing", async () => {
+ const workspace = await createWorkspace();
+
+ const matches = await pythonParser.detect(workspace);
+
+ assert.deepStrictEqual(matches, []);
+ assert.strictEqual(await pythonParser.canResolve(workspace), false);
+ });
+
+ test("resolve rejects lockfiles outside the provided workspace folder", async () => {
+ const workspace = await createWorkspace();
+ const outsideWorkspace = await createWorkspace();
+ const lockfilePath = path.join(outsideWorkspace, "requirements.txt");
+
+ await writeTextFile(lockfilePath, "requests==2.31.0\n");
+
+ await assert.rejects(
+ () => pythonParser.resolve({
+ workspaceFolder: workspace,
+ lockfilePath,
+ }),
+ /Refusing to read files outside the workspace folder/
+ );
+ });
+
+ test("throws for malformed poetry.lock files", async () => {
+ const workspace = await createWorkspace();
+ const lockfilePath = path.join(workspace, "poetry.lock");
+ const manifestPath = path.join(workspace, "pyproject.toml");
+ await writeTextFile(lockfilePath, "[metadata]\nlock-version = \"2.0\"\n");
+ await writeTextFile(manifestPath, "[tool.poetry.dependencies]\nflask = \"^2.3.0\"\n");
+
+ await assert.rejects(
+ () => pythonParser.resolve({ lockfilePath, manifestPath }),
+ /no package entries found/
+ );
+ });
+});
diff --git a/test/lockfileResolver.test.js b/test/lockfileResolver.test.js
new file mode 100644
index 0000000..7493348
--- /dev/null
+++ b/test/lockfileResolver.test.js
@@ -0,0 +1,204 @@
+const assert = require("assert");
+const fs = require("fs");
+const path = require("path");
+const { LockfileResolver } = require("../util/lockfileResolver");
+const { deduplicateDeps } = require("../util/lockfileParsers/shared");
+const { buildPackageIndex, findCoverageMatch } = require("../views/dependencyHealthProvider");
+const {
+ copyFixtureDir,
+ makeTempWorkspace,
+ removeDirectory,
+ writeTextFile,
+} = require("./helpers/fixtureWorkspace");
+
+suite("LockfileResolver Test Suite", () => {
+ const tempDirs = [];
+
+ async function createWorkspace() {
+ const workspace = await makeTempWorkspace();
+ tempDirs.push(workspace);
+ return workspace;
+ }
+
+ suiteTeardown(async () => {
+ await Promise.all(tempDirs.map((tempDir) => removeDirectory(tempDir)));
+ });
+
+ test("detectResolvers finds multiple ecosystems in one workspace", async () => {
+ const workspace = await createWorkspace();
+ await copyFixtureDir("npm", workspace);
+ await copyFixtureDir("docker", workspace);
+ await copyFixtureDir("ruby", workspace);
+
+ const resolvers = await LockfileResolver.detectResolvers(workspace);
+ const resolverKeys = new Set(resolvers.map((resolver) => `${resolver.resolverName}:${resolver.sourceFile}`));
+
+ assert.ok(resolverKeys.has("npmParser:package-lock.json"));
+ assert.ok(resolverKeys.has("dockerParser:Dockerfile"));
+ assert.ok(resolverKeys.has("dockerParser:docker-compose.yml"));
+ assert.ok(resolverKeys.has("rubyParser:Gemfile.lock"));
+ });
+
+ test("resolveAll returns separate dependency trees grouped by source file", async () => {
+ const workspace = await createWorkspace();
+ await copyFixtureDir("npm", workspace);
+ await copyFixtureDir("docker", workspace);
+
+ const trees = await LockfileResolver.resolveAll(workspace, { maxDependenciesToScan: 10000 });
+ const bySource = new Map(trees.map((tree) => [tree.sourceFile, tree]));
+
+ assert.strictEqual(trees.length, 3);
+ assert.ok(bySource.has("package-lock.json"));
+ assert.ok(bySource.has("Dockerfile"));
+ assert.ok(bySource.has("docker-compose.yml"));
+ assert.strictEqual(bySource.get("package-lock.json").ecosystem, "npm");
+ assert.strictEqual(bySource.get("Dockerfile").ecosystem, "docker");
+ assert.strictEqual(bySource.get("docker-compose.yml").ecosystem, "docker");
+ });
+
+ test("deduplicateDeps keeps a single package and prefers direct dependencies", () => {
+ const dependencies = [
+ {
+ ecosystem: "npm",
+ name: "accepts",
+ version: "1.3.8",
+ isDirect: false,
+ parent: "express",
+ parentChain: ["express"],
+ },
+ {
+ ecosystem: "npm",
+ name: "accepts",
+ version: "1.3.8",
+ isDirect: true,
+ parent: null,
+ parentChain: [],
+ },
+ {
+ ecosystem: "npm",
+ name: "express",
+ version: "4.18.2",
+ isDirect: true,
+ parent: null,
+ parentChain: [],
+ },
+ ];
+
+ const deduplicated = deduplicateDeps(dependencies);
+
+ assert.strictEqual(deduplicated.length, 2);
+ const accepts = deduplicated.find((dependency) => dependency.name === "accepts");
+ assert.ok(accepts);
+ assert.strictEqual(accepts.isDirect, true);
+ assert.deepStrictEqual(accepts.parentChain, []);
+ });
+
+ test("coverage matching normalizes Python package names", () => {
+ const cloudsmithPackage = {
+ name: "scikit-learn",
+ version: "1.4.0",
+ format: "python",
+ };
+ const index = buildPackageIndex([cloudsmithPackage], "python");
+
+ const match = findCoverageMatch(index, {
+ name: "scikit_learn",
+ version: "1.4.0",
+ format: "python",
+ });
+
+ assert.strictEqual(match, cloudsmithPackage);
+ });
+
+ test("coverage matching normalizes Python case, hyphen, underscore, and dot variants", () => {
+ const cloudsmithPackage = {
+ name: "Requests-HTML",
+ version: "0.10.0",
+ format: "python",
+ };
+ const index = buildPackageIndex([cloudsmithPackage], "python");
+ const variants = [
+ "requests_html",
+ "requests.html",
+ "REQUESTS-HTML",
+ ];
+
+ for (const variant of variants) {
+ const match = findCoverageMatch(index, {
+ name: variant,
+ version: "0.10.0",
+ format: "python",
+ });
+ assert.strictEqual(match, cloudsmithPackage);
+ }
+ });
+
+ test("coverage matching indexes Maven packages by groupId and artifactId", () => {
+ const cloudsmithPackage = {
+ name: "spring-boot-starter",
+ version: "3.2.0",
+ format: "maven",
+ identifiers: {
+ group_id: "org.springframework.boot",
+ },
+ };
+ const index = buildPackageIndex([cloudsmithPackage], "maven");
+
+ const match = findCoverageMatch(index, {
+ name: "org.springframework.boot:spring-boot-starter",
+ version: "3.2.0",
+ format: "maven",
+ });
+
+ assert.strictEqual(match, cloudsmithPackage);
+ });
+
+ test("secondary parser fixtures can be resolved through the registry", async () => {
+ const fixtureNames = [
+ "gradle",
+ "go",
+ "nuget",
+ "dart",
+ "composer",
+ "helm",
+ "swift",
+ "hex",
+ ];
+
+ for (const fixtureName of fixtureNames) {
+ const workspace = await createWorkspace();
+ await copyFixtureDir(fixtureName, workspace);
+
+ const trees = await LockfileResolver.resolveAll(workspace, { maxDependenciesToScan: 10000 });
+
+ assert.strictEqual(trees.length, 1, `${fixtureName} should resolve to exactly one tree`);
+ assert.ok(trees[0].dependencies.length > 0, `${fixtureName} should resolve at least one dependency`);
+ }
+ });
+
+ test("detectResolvers ignores symlinked lockfiles that point outside the workspace", async () => {
+ const workspace = await createWorkspace();
+ const outsideDir = await createWorkspace();
+ const outsideLockfile = path.join(outsideDir, "package-lock.json");
+ const workspaceLockfile = path.join(workspace, "package-lock.json");
+
+ await writeTextFile(
+ outsideLockfile,
+ JSON.stringify({
+ packages: {
+ "": {
+ dependencies: {},
+ },
+ },
+ })
+ );
+ await fs.promises.symlink(outsideLockfile, workspaceLockfile);
+
+ const resolvers = await LockfileResolver.detectResolvers(workspace);
+
+ assert.strictEqual(
+ resolvers.some((resolver) => resolver.resolverName === "npmParser"),
+ false
+ );
+ });
+});
diff --git a/test/treeVisualization.test.js b/test/treeVisualization.test.js
new file mode 100644
index 0000000..db28eba
--- /dev/null
+++ b/test/treeVisualization.test.js
@@ -0,0 +1,198 @@
+const assert = require("assert");
+const vscode = require("vscode");
+const {
+ DependencyHealthProvider,
+ FILTER_MODES,
+ buildDependencyHealthReport,
+ buildDependencySummary,
+} = require("../views/dependencyHealthProvider");
+
+suite("tree visualization", () => {
+ let originalGetConfiguration;
+
+ setup(() => {
+ originalGetConfiguration = vscode.workspace.getConfiguration;
+ vscode.workspace.getConfiguration = () => ({
+ get(key) {
+ if (key === "dependencyTreeDefaultView") {
+ return "tree";
+ }
+ if (key === "showLicenseIndicators") {
+ return true;
+ }
+ if (key === "flagRestrictiveLicenses") {
+ return true;
+ }
+ return undefined;
+ },
+ });
+ });
+
+ teardown(() => {
+ vscode.workspace.getConfiguration = originalGetConfiguration;
+ });
+
+ function createContext() {
+ return {
+ workspaceState: {
+ get() {
+ return null;
+ },
+ async update() {},
+ },
+ secrets: {
+ onDidChange() {
+ return { dispose() {} };
+ },
+ async get() {
+ return "true";
+ },
+ },
+ };
+ }
+
+ function createFoundPackage(slug) {
+ return {
+ namespace: "workspace-a",
+ repository: "production-npm",
+ slug_perm: slug,
+ status_str: "Completed",
+ version: "1.0.0",
+ license: "MIT",
+ };
+ }
+
+ function createTree() {
+ const vulnerableLeaf = {
+ name: "shared-lib",
+ version: "1.0.0",
+ format: "npm",
+ ecosystem: "npm",
+ isDirect: false,
+ parent: "alpha",
+ parentChain: ["alpha"],
+ transitives: [],
+ cloudsmithStatus: "FOUND",
+ cloudsmithPackage: createFoundPackage("shared"),
+ vulnerabilities: {
+ count: 1,
+ maxSeverity: "High",
+ cveIds: ["CVE-2024-1234"],
+ hasFixAvailable: true,
+ severityCounts: { High: 1 },
+ entries: [{ cveId: "CVE-2024-1234", severity: "High", fixVersion: "1.0.1" }],
+ detailsLoaded: true,
+ },
+ sourceFile: "package-lock.json",
+ };
+
+ const duplicateLeaf = {
+ ...vulnerableLeaf,
+ parent: "beta",
+ parentChain: ["beta"],
+ };
+
+ const alpha = {
+ name: "alpha",
+ version: "2.0.0",
+ format: "npm",
+ ecosystem: "npm",
+ isDirect: true,
+ parent: null,
+ parentChain: [],
+ transitives: [vulnerableLeaf],
+ cloudsmithStatus: "FOUND",
+ cloudsmithPackage: createFoundPackage("alpha"),
+ sourceFile: "package-lock.json",
+ };
+
+ const beta = {
+ name: "beta",
+ version: "3.0.0",
+ format: "npm",
+ ecosystem: "npm",
+ isDirect: true,
+ parent: null,
+ parentChain: [],
+ transitives: [duplicateLeaf],
+ cloudsmithStatus: "FOUND",
+ cloudsmithPackage: createFoundPackage("beta"),
+ sourceFile: "package-lock.json",
+ };
+
+ return {
+ ecosystem: "npm",
+ sourceFile: "package-lock.json",
+ dependencies: [alpha, beta, vulnerableLeaf],
+ };
+ }
+
+ test("tree mode expands direct dependencies and collapses duplicate diamonds", () => {
+ const provider = new DependencyHealthProvider(createContext(), null);
+ const tree = createTree();
+ provider._displayTrees = [tree];
+ provider._fullTrees = [tree];
+ provider._viewMode = "tree";
+ provider._rebuildSummary();
+
+ const rootNodes = provider.buildDependencyNodesForTree(tree);
+ assert.strictEqual(rootNodes.length, 2);
+ assert.strictEqual(rootNodes[0].getTreeItem().collapsibleState, vscode.TreeItemCollapsibleState.Collapsed);
+
+ const alphaChildren = rootNodes[0].getChildren();
+ assert.strictEqual(alphaChildren.length, 1);
+ assert.strictEqual(alphaChildren[0].name, "shared-lib");
+ assert.strictEqual(alphaChildren[0].getTreeItem().collapsibleState, vscode.TreeItemCollapsibleState.None);
+
+ const betaChildren = rootNodes[1].getChildren();
+ assert.strictEqual(betaChildren.length, 1);
+ assert.match(betaChildren[0].getTreeItem().description, /see first occurrence/);
+ assert.strictEqual(betaChildren[0].getTreeItem().collapsibleState, vscode.TreeItemCollapsibleState.None);
+ });
+
+ test("filtered tree keeps only the ancestor path to vulnerable dependencies", async () => {
+ const provider = new DependencyHealthProvider(createContext(), null);
+ const tree = createTree();
+ tree.dependencies[1] = {
+ ...tree.dependencies[1],
+ transitives: [],
+ };
+ provider._displayTrees = [tree];
+ provider._fullTrees = [tree];
+ provider._viewMode = "tree";
+ await provider.setFilterMode(FILTER_MODES.VULNERABLE);
+
+ const rootNodes = provider.buildDependencyNodesForTree(tree);
+ assert.strictEqual(rootNodes.length, 1);
+ assert.strictEqual(rootNodes[0].name, "alpha");
+ assert.match(rootNodes[0].getTreeItem().description, /context/);
+ assert.strictEqual(rootNodes[0].getChildren()[0].name, "shared-lib");
+ });
+
+ test("dependency health report includes vulnerability and upstream sections", () => {
+ const tree = createTree();
+ const uncovered = {
+ name: "missing-lib",
+ version: "0.1.0",
+ format: "npm",
+ ecosystem: "npm",
+ isDirect: true,
+ parent: null,
+ parentChain: [],
+ transitives: [],
+ cloudsmithStatus: "NOT_FOUND",
+ upstreamStatus: "reachable",
+ upstreamDetail: "npm proxy on production",
+ sourceFile: "package-lock.json",
+ };
+ tree.dependencies.push(uncovered);
+
+ const summary = buildDependencySummary([tree], [tree], {});
+ const report = buildDependencyHealthReport("fixture-app", tree.dependencies, summary, "2026-04-05");
+
+ assert.match(report, /## Vulnerable Dependencies/);
+ assert.match(report, /\| shared-lib \| 1.0.0 \| Transitive \| High \| CVE-2024-1234 \| Yes \(1.0.1\) \|/);
+ assert.match(report, /## Uncovered Dependencies/);
+ assert.match(report, /\| missing-lib \| 0.1.0 \| npm \| Reachable \| npm proxy on production \|/);
+ });
+});
diff --git a/test/upstreamGapAnalyzer.test.js b/test/upstreamGapAnalyzer.test.js
new file mode 100644
index 0000000..fa2e8d4
--- /dev/null
+++ b/test/upstreamGapAnalyzer.test.js
@@ -0,0 +1,136 @@
+const assert = require("assert");
+const {
+ analyzeUpstreamGaps,
+} = require("../util/upstreamGapAnalyzer");
+
+suite("upstreamGapAnalyzer", () => {
+ function createState(entries = {}) {
+ return {
+ groupedUpstreams: new Map(Object.entries(entries)),
+ };
+ }
+
+ test("classifies uncovered dependencies as reachable when a matching proxy exists", async () => {
+ const dependencies = [
+ {
+ name: "accepts",
+ version: "1.3.8",
+ format: "npm",
+ ecosystem: "npm",
+ cloudsmithStatus: "NOT_FOUND",
+ },
+ ];
+
+ const enriched = await analyzeUpstreamGaps(dependencies, "workspace-a", ["production"], {
+ upstreamChecker: {
+ async getRepositoryUpstreamState() {
+ return createState({
+ npm: [
+ { name: "npm", is_active: true },
+ ],
+ });
+ },
+ },
+ });
+
+ assert.strictEqual(enriched[0].upstreamStatus, "reachable");
+ assert.strictEqual(enriched[0].upstreamDetail, "npm proxy on production");
+ });
+
+ test("classifies supported formats with no proxy as no_proxy", async () => {
+ const dependencies = [
+ {
+ name: "requests",
+ version: "2.31.0",
+ format: "python",
+ ecosystem: "python",
+ cloudsmithStatus: "NOT_FOUND",
+ },
+ ];
+
+ const enriched = await analyzeUpstreamGaps(dependencies, "workspace-a", ["production"], {
+ upstreamChecker: {
+ async getRepositoryUpstreamState() {
+ return createState();
+ },
+ },
+ });
+
+ assert.strictEqual(enriched[0].upstreamStatus, "no_proxy");
+ assert.strictEqual(enriched[0].upstreamDetail, "No upstream proxy configured for python");
+ });
+
+ test("classifies unsupported formats as unreachable", async () => {
+ const dependencies = [
+ {
+ name: "custom-lib",
+ version: "1.0.0",
+ format: "custom",
+ ecosystem: "custom",
+ cloudsmithStatus: "NOT_FOUND",
+ },
+ ];
+
+ const enriched = await analyzeUpstreamGaps(dependencies, "workspace-a", ["production"], {
+ upstreamChecker: {
+ async getRepositoryUpstreamState() {
+ return createState();
+ },
+ },
+ });
+
+ assert.strictEqual(enriched[0].upstreamStatus, "unreachable");
+ assert.strictEqual(enriched[0].upstreamDetail, "Not available through Cloudsmith");
+ });
+
+ test("limits upstream repository lookups to five concurrent requests and emits one final patch", async () => {
+ const dependencies = [
+ {
+ name: "accepts",
+ version: "1.3.8",
+ format: "npm",
+ ecosystem: "npm",
+ cloudsmithStatus: "NOT_FOUND",
+ },
+ ];
+ const repositories = Array.from({ length: 12 }, (_, index) => `repo-${index + 1}`);
+ const progressEvents = [];
+ let inFlight = 0;
+ let maxInFlight = 0;
+
+ const enriched = await analyzeUpstreamGaps(dependencies, "workspace-a", repositories, {
+ onProgress: (patchMap, meta) => {
+ progressEvents.push({
+ size: patchMap.size,
+ completed: meta.completed,
+ total: meta.total,
+ });
+ },
+ upstreamChecker: {
+ async getRepositoryUpstreamState(_workspace, repo) {
+ inFlight += 1;
+ maxInFlight = Math.max(maxInFlight, inFlight);
+ await new Promise((resolve) => setTimeout(resolve, 5));
+ inFlight -= 1;
+
+ if (repo === "repo-9") {
+ return createState({
+ npm: [
+ { name: "npm", is_active: true },
+ ],
+ });
+ }
+
+ return createState();
+ },
+ },
+ });
+
+ assert.ok(maxInFlight <= 5);
+ assert.strictEqual(progressEvents.filter((event) => event.size > 0).length, 1);
+ assert.strictEqual(progressEvents[progressEvents.length - 1].completed, repositories.length);
+ assert.strictEqual(progressEvents[progressEvents.length - 1].size, 1);
+ assert.strictEqual(enriched[0].upstreamStatus, "reachable");
+ assert.strictEqual(enriched[0].upstreamDetail, "npm proxy on repo-9");
+ });
+});
diff --git a/test/upstreamPullService.test.js b/test/upstreamPullService.test.js
new file mode 100644
index 0000000..bad0b01
--- /dev/null
+++ b/test/upstreamPullService.test.js
@@ -0,0 +1,312 @@
+const assert = require("assert");
+const {
+ buildRegistryTriggerPlan,
+} = require("../util/registryEndpoints");
+const {
+ UpstreamPullService,
+} = require("../util/upstreamPullService");
+
+function createResponse(status, body, headers = {}) {
+ return {
+ status,
+ headers: {
+ get(name) {
+ const lowerName = String(name || "").toLowerCase();
+ return headers[lowerName] || headers[name] || null;
+ },
+ },
+ async text() {
+ return body;
+ },
+ };
+}
+
+suite("UpstreamPullService", () => {
+ test("builds canonical registry trigger URLs for supported formats", () => {
+ const mavenPlan = buildRegistryTriggerPlan("workspace", "repo", {
+ name: "com.example:demo-app",
+ version: "1.2.3",
+ format: "maven",
+ });
+ assert.strictEqual(
+ mavenPlan.request.url,
+ "https://dl.cloudsmith.io/basic/workspace/repo/maven/com/example/demo-app/1.2.3/demo-app-1.2.3.pom"
+ );
+
+ const npmPlan = buildRegistryTriggerPlan("workspace", "repo", {
+ name: "@scope/widget",
+ version: "4.5.6",
+ format: "npm",
+ });
+ assert.strictEqual(
+ npmPlan.request.url,
+ "https://npm.cloudsmith.io/workspace/repo/%40scope/widget/-/widget-4.5.6.tgz"
+ );
+
+ const goPlan = buildRegistryTriggerPlan("workspace", "repo", {
+ name: "github.com/MyOrg/MyModule",
+ version: "v1.0.0",
+ format: "go",
+ });
+ assert.strictEqual(
+ goPlan.request.url,
+ "https://golang.cloudsmith.io/workspace/repo/github.com/!my!org/!my!module/@v/v1.0.0.info"
+ );
+
+ const cargoPlan = buildRegistryTriggerPlan("workspace", "repo", {
+ name: "serde",
+ version: "1.0.0",
+ format: "cargo",
+ });
+ assert.strictEqual(
+ cargoPlan.request.url,
+ "https://cargo.cloudsmith.io/workspace/repo/se/rd/serde"
+ );
+ });
+
+ test("prepare builds a mixed-ecosystem confirmation with skipped formats", async () => {
+ const warnings = [];
+ const service = new UpstreamPullService({}, {
+ fetchRepositories: async () => [{ slug: "repo", name: "Repo" }],
+ upstreamChecker: {
+ async getRepositoryUpstreamState() {
+ return {
+ groupedUpstreams: new Map([
+ ["maven", [{ name: "Maven Central", is_active: true }]],
+ ]),
+ };
+ },
+ },
+ showQuickPick: async (items) => items[0],
+ showWarningMessage: async (message, _options, action) => {
+ warnings.push(message);
+ return action;
+ },
+ showErrorMessage: async () => {},
+ showInformationMessage: async () => {},
+ });
+
+ const prepared = await service.prepare({
+ workspace: "workspace",
+ repositoryHint: "repo",
+ dependencies: [
+ {
+ name: "com.example:demo-app",
+ version: "1.2.3",
+ format: "maven",
+ cloudsmithStatus: "NOT_FOUND",
+ },
+ {
+ name: "requests",
+ version: "2.31.0",
+ format: "python",
+ cloudsmithStatus: "NOT_FOUND",
+ },
+ ],
+ });
+
+ assert.ok(prepared);
+ assert.strictEqual(prepared.plan.pullableDependencies.length, 1);
+ assert.strictEqual(prepared.plan.skippedDependencies.length, 1);
+ assert.strictEqual(warnings.length, 1);
+ assert.match(warnings[0], /Pull 1 of 2 dependencies through repo\?/);
+ assert.match(warnings[0], /1 Maven will be pulled\./);
+ assert.match(
+ warnings[0],
+ /1 Python will be skipped \(no matching upstream is configured on this repository\)\./
+ );
+ });
+
+ test("pulls Python dependencies via same-host redirects using manual auth-preserving requests", async () => {
+ const calls = [];
+ const initialIndexUrl = "https://dl.cloudsmith.io/basic/workspace/repo/python/simple/requests/";
+ const redirectedIndexUrl = "https://dl.cloudsmith.io/basic/workspace/repo/python/simple/requests/index.html";
+ const artifactUrl = "https://dl.cloudsmith.io/basic/workspace/repo/python/packages/requests-2.31.0-py3-none-any.whl";
+ const authorizationHeader = `Basic ${Buffer.from("token:api-key").toString("base64")}`;
+ const service = new UpstreamPullService({}, {
+ credentialManager: {
+ async getApiKey() {
+ return "api-key";
+ },
+ },
+ fetchImpl: async (url, options) => {
+ calls.push({ url, options });
+ if (url === initialIndexUrl) {
+ return createResponse(302, "", {
+ location: redirectedIndexUrl,
+ });
+ }
+ if (url === redirectedIndexUrl) {
+ return createResponse(200, 'requests');
+ }
+ if (url === artifactUrl) {
+ return createResponse(200, "");
+ }
+ throw new Error(`Unexpected URL: ${url}`);
+ },
+ showErrorMessage: async () => {},
+ showInformationMessage: async () => {},
+ showWarningMessage: async () => {},
+ });
+
+ const result = await service.execute({
+ workspace: "workspace",
+ repository: { slug: "repo" },
+ plan: {
+ pullableDependencies: [{
+ name: "requests",
+ version: "2.31.0",
+ format: "python",
+ cloudsmithStatus: "NOT_FOUND",
+ }],
+ skippedDependencies: [],
+ },
+ });
+
+ assert.strictEqual(result.canceled, false);
+ assert.strictEqual(result.pullResult.cached, 1);
+ assert.strictEqual(calls.length, 3);
+ assert.deepStrictEqual(
+ calls.map((call) => call.url),
+ [initialIndexUrl, redirectedIndexUrl, artifactUrl]
+ );
+ assert.strictEqual(calls.every((call) => call.options.redirect === "manual"), true);
+ assert.strictEqual(
+ calls.every((call) => call.options.headers.Authorization === authorizationHeader),
+ true
+ );
+ });
+
+ test("rejects redirects to untrusted hosts before forwarding credentials", async () => {
+ const calls = [];
+ const service = new UpstreamPullService({}, {
+ credentialManager: {
+ async getApiKey() {
+ return "api-key";
+ },
+ },
+ fetchImpl: async (url, options) => {
+ calls.push({ url, options });
+ return createResponse(302, "", {
+ location: "https://example.com/requests-2.31.0.whl",
+ });
+ },
+ showErrorMessage: async () => {},
+ showInformationMessage: async () => {},
+ showWarningMessage: async () => {},
+ });
+
+ const result = await service.execute({
+ workspace: "workspace",
+ repository: { slug: "repo" },
+ plan: {
+ pullableDependencies: [{
+ name: "requests",
+ version: "2.31.0",
+ format: "python",
+ cloudsmithStatus: "NOT_FOUND",
+ }],
+ skippedDependencies: [],
+ },
+ });
+
+ assert.strictEqual(result.canceled, false);
+ assert.strictEqual(result.pullResult.cached, 0);
+ assert.strictEqual(result.pullResult.errors, 1);
+ assert.strictEqual(calls.length, 1);
+ assert.match(result.pullResult.details[0].errorMessage, /redirect target was rejected/i);
+ });
+
+ test("stops after three authentication failures before expanding concurrency", async () => {
+ const calls = [];
+ const errors = [];
+ const service = new UpstreamPullService({}, {
+ credentialManager: {
+ async getApiKey() {
+ return "api-key";
+ },
+ },
+ fetchImpl: async (url) => {
+ calls.push(url);
+ return createResponse(401, "");
+ },
+ showErrorMessage: async (message) => {
+ errors.push(message);
+ },
+ showInformationMessage: async () => {},
+ showWarningMessage: async () => {},
+ });
+
+ const dependencies = Array.from({ length: 5 }, (_, index) => ({
+ name: `package-${index}`,
+ version: "1.0.0",
+ format: "npm",
+ cloudsmithStatus: "NOT_FOUND",
+ }));
+
+ const result = await service.execute({
+ workspace: "workspace",
+ repository: { slug: "repo" },
+ plan: {
+ pullableDependencies: dependencies,
+ skippedDependencies: [],
+ },
+ });
+
+ assert.strictEqual(calls.length, 3);
+ assert.strictEqual(result.pullResult.errors, 5);
+ assert.strictEqual(result.pullResult.authFailed, 5);
+ assert.deepStrictEqual(errors, [
+ "Authentication failed. Check your API key in Cloudsmith settings.",
+ ]);
+ });
+
+ test("prepareSingle only offers repositories with a matching upstream", async () => {
+ const quickPickCalls = [];
+ let warningCalls = 0;
+ const service = new UpstreamPullService({}, {
+ fetchRepositories: async () => [
+ { slug: "repo-a", name: "Repo A" },
+ { slug: "repo-b", name: "Repo B" },
+ ],
+ upstreamChecker: {
+ async getRepositoryUpstreamState(_workspace, repo) {
+ return {
+ groupedUpstreams: new Map([
+ ["python", repo === "repo-b" ? [{ name: "PyPI", is_active: true }] : []],
+ ]),
+ };
+ },
+ },
+ showQuickPick: async (items) => {
+ quickPickCalls.push(items);
+ return items[0];
+ },
+ showErrorMessage: async () => {},
+ showInformationMessage: async () => {},
+ showWarningMessage: async () => {
+ warningCalls += 1;
+ },
+ });
+
+ const prepared = await service.prepareSingle({
+ workspace: "workspace",
+ repositoryHint: "repo-b",
+ dependency: {
+ name: "requests",
+ version: "2.31.0",
+ format: "python",
+ cloudsmithStatus: "NOT_FOUND",
+ },
+ });
+
+ assert.ok(prepared);
+ assert.strictEqual(prepared.repository.slug, "repo-b");
+ assert.strictEqual(prepared.plan.pullableDependencies.length, 1);
+ assert.strictEqual(quickPickCalls.length, 1);
+ assert.strictEqual(quickPickCalls[0].length, 1);
+ assert.strictEqual(quickPickCalls[0][0].label, "repo-b");
+ assert.match(quickPickCalls[0][0].detail, /Python upstream \(PyPI\)/);
+ assert.strictEqual(warningCalls, 0);
+ });
+});
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/formatIcons.js b/util/formatIcons.js
new file mode 100644
index 0000000..720fa51
--- /dev/null
+++ b/util/formatIcons.js
@@ -0,0 +1,82 @@
+// Copyright 2026 Cloudsmith Ltd. All rights reserved.
+const fs = require("fs");
+const vscode = require("vscode");
+const { canonicalFormat } = require("./packageNameNormalizer");
+
+const FORMAT_ICON_KEYS = Object.freeze({
+ cargo: "cargo",
+ composer: "composer",
+ conda: "conda",
+ dart: "dart",
+ docker: "docker",
+ elixir: "elixir",
+ gem: "ruby",
+ go: "go",
+ golang: "go",
+ gradle: "maven",
+ helm: "helm",
+ hex: "elixir",
+ maven: "maven",
+ npm: "npm",
+ nuget: "nuget",
+ php: "php",
+ pypi: "python",
+ python: "python",
+ ruby: "ruby",
+ rust: "rust",
+ swift: "swift",
+});
+
+const warnedMissingIcons = new Set();
+
+function getFormatIconPath(format, extensionPath, options = {}) {
+ const fallbackIcon = Object.prototype.hasOwnProperty.call(options, "fallbackIcon")
+ ? options.fallbackIcon
+ : new vscode.ThemeIcon("package");
+ const normalizedFormat = canonicalFormat(format);
+ if (!normalizedFormat || !extensionPath) {
+ return fallbackIcon;
+ }
+
+ const iconKey = FORMAT_ICON_KEYS[normalizedFormat] || normalizedFormat;
+ const iconPath = resolveThemedIconPath(extensionPath, iconKey);
+ if (iconPath) {
+ return iconPath;
+ }
+
+ warnMissingIconOnce(normalizedFormat);
+ return fallbackIcon;
+}
+
+function resolveThemedIconPath(extensionPath, iconKey) {
+ if (!extensionPath || !iconKey) {
+ return null;
+ }
+
+ const extensionUri = vscode.Uri.file(extensionPath);
+ const dark = vscode.Uri.joinPath(extensionUri, "media", "vscode_icons", `file_type_${iconKey}.svg`);
+ if (!fs.existsSync(dark.fsPath)) {
+ return null;
+ }
+
+ const lightCandidate = vscode.Uri.joinPath(extensionUri, "media", "vscode_icons", `file_type_light_${iconKey}.svg`);
+ return {
+ light: fs.existsSync(lightCandidate.fsPath) ? lightCandidate : dark,
+ dark,
+ };
+}
+
+function warnMissingIconOnce(format) {
+ const normalizedFormat = canonicalFormat(format);
+ if (!normalizedFormat || warnedMissingIcons.has(normalizedFormat)) {
+ return;
+ }
+
+ warnedMissingIcons.add(normalizedFormat);
+ console.warn(`No format icon found for ecosystem '${normalizedFormat}', using generic icon`);
+}
+
+module.exports = {
+ FORMAT_ICON_KEYS,
+ getFormatIconPath,
+};
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,
+};
diff --git a/util/lockfileParsers/cargoParser.js b/util/lockfileParsers/cargoParser.js
new file mode 100644
index 0000000..06199ff
--- /dev/null
+++ b/util/lockfileParsers/cargoParser.js
@@ -0,0 +1,283 @@
+// Copyright 2026 Cloudsmith Ltd. All rights reserved.
+const path = require("path");
+const {
+ buildTree,
+ createDependency,
+ deduplicateDeps,
+ flattenDependencies,
+ getSourceFileName,
+ getWorkspacePath,
+ parseKeyValueLine,
+ pathExists,
+ readUtf8,
+} = require("./shared");
+const { parseCargoTomlManifest } = require("./manifestHelpers");
+
+const cargoParser = {
+ name: "cargoParser",
+ ecosystem: "cargo",
+
+ async canResolve(workspaceFolder) {
+ const rootPath = getWorkspacePath(workspaceFolder);
+ return (await pathExists(path.join(rootPath, "Cargo.lock"), workspaceFolder))
+ || (await pathExists(path.join(rootPath, "Cargo.toml"), workspaceFolder));
+ },
+
+ async detect(workspaceFolder) {
+ const rootPath = getWorkspacePath(workspaceFolder);
+ const lockfilePath = await pathExists(path.join(rootPath, "Cargo.lock"), workspaceFolder)
+ ? path.join(rootPath, "Cargo.lock")
+ : null;
+ const manifestPath = await pathExists(path.join(rootPath, "Cargo.toml"), workspaceFolder)
+ ? path.join(rootPath, "Cargo.toml")
+ : null;
+ if (!lockfilePath && !manifestPath) {
+ return [];
+ }
+ return [{
+ resolverName: this.name,
+ ecosystem: this.ecosystem,
+ lockfilePath,
+ manifestPath,
+ sourceFile: getSourceFileName(lockfilePath || manifestPath),
+ }];
+ },
+
+ async resolve({ lockfilePath, manifestPath, workspaceFolder }) {
+ const manifestDependencies = manifestPath && await pathExists(manifestPath, workspaceFolder)
+ ? parseCargoTomlManifest(await readUtf8(manifestPath, workspaceFolder))
+ : [];
+ const sourceFile = getSourceFileName(lockfilePath || manifestPath);
+
+ if (!lockfilePath) {
+ return buildTree("cargo", sourceFile, manifestDependencies.map((dependency) => createDependency({
+ name: dependency.name,
+ version: dependency.version,
+ ecosystem: "cargo",
+ isDirect: true,
+ parent: null,
+ parentChain: [],
+ transitives: [],
+ sourceFile,
+ isDevelopmentDependency: dependency.isDevelopmentDependency,
+ })));
+ }
+
+ const records = parseCargoLock(await readUtf8(lockfilePath, workspaceFolder));
+ if (records.length === 0) {
+ throw new Error("Malformed Cargo.lock: no package entries found");
+ }
+ const directNames = new Set(manifestDependencies.map((dependency) => dependency.name.toLowerCase()));
+ const recordsByName = new Map();
+ const incomingCounts = new Map();
+
+ for (const record of records) {
+ if (!recordsByName.has(record.name.toLowerCase())) {
+ recordsByName.set(record.name.toLowerCase(), []);
+ }
+ recordsByName.get(record.name.toLowerCase()).push(record);
+ for (const dependency of record.dependencies) {
+ incomingCounts.set(
+ dependency.name.toLowerCase(),
+ (incomingCounts.get(dependency.name.toLowerCase()) || 0) + 1
+ );
+ }
+ }
+
+ const rootRecords = manifestDependencies.length > 0
+ ? manifestDependencies.map((dependency) => selectCargoRecord(recordsByName, dependency.name, dependency.version)).filter(Boolean)
+ : records.filter((record) => !incomingCounts.get(record.name.toLowerCase()));
+
+ const directRoots = deduplicateDeps(rootRecords.map((record) => buildCargoDependency(
+ record,
+ [],
+ recordsByName,
+ new Set(),
+ sourceFile,
+ directNames
+ )));
+
+ let dependencies = deduplicateDeps(flattenDependencies(directRoots));
+ for (const record of records) {
+ const key = `${record.name.toLowerCase()}@${record.version.toLowerCase()}`;
+ if (dependencies.some((dependency) => `${dependency.name.toLowerCase()}@${dependency.version.toLowerCase()}` === key)) {
+ continue;
+ }
+ dependencies.push(createDependency({
+ name: record.name,
+ version: record.version,
+ ecosystem: "cargo",
+ isDirect: directNames.has(record.name.toLowerCase()),
+ parent: null,
+ parentChain: [],
+ transitives: [],
+ sourceFile,
+ isDevelopmentDependency: false,
+ }));
+ }
+
+ return buildTree("cargo", sourceFile, dependencies);
+ },
+};
+
+function parseCargoLock(content) {
+ const records = [];
+ let current = null;
+ let inDependenciesArray = false;
+
+ const flushCurrent = () => {
+ if (!current || !current.name || !current.version) {
+ current = null;
+ inDependenciesArray = false;
+ return;
+ }
+ const source = String(current.source || "").trim();
+ if (source && !source.startsWith("path+") && !source.startsWith("git+")) {
+ records.push(current);
+ }
+ current = null;
+ inDependenciesArray = false;
+ };
+
+ for (const rawLine of String(content || "").split(/\r?\n/)) {
+ const line = rawLine.trim();
+ if (!line || line.startsWith("#")) {
+ continue;
+ }
+ if (line === "[[package]]") {
+ flushCurrent();
+ current = { name: "", version: "", source: "", dependencies: [] };
+ continue;
+ }
+ if (!current) {
+ continue;
+ }
+
+ if (inDependenciesArray) {
+ if (line === "]") {
+ inDependenciesArray = false;
+ continue;
+ }
+ const match = line.trim().replace(/,$/, "").replace(/^"|"$/g, "").match(/^([^ ]+)(?: ([^ ]+))?/);
+ if (match) {
+ current.dependencies.push({
+ name: match[1],
+ version: match[2] ? match[2].replace(/^\(/, "").replace(/\)$/, "") : "",
+ });
+ }
+ continue;
+ }
+
+ if (line.startsWith("name =")) {
+ current.name = parseKeyValueLine(line).value.replace(/^"|"$/g, "");
+ continue;
+ }
+ if (line.startsWith("version =")) {
+ current.version = parseKeyValueLine(line).value.replace(/^"|"$/g, "");
+ continue;
+ }
+ if (line.startsWith("source =")) {
+ current.source = parseKeyValueLine(line).value.replace(/^"|"$/g, "");
+ continue;
+ }
+ if (line.startsWith("dependencies = [")) {
+ inDependenciesArray = true;
+ const inline = line.slice(line.indexOf("[") + 1, line.lastIndexOf("]"));
+ if (inline.trim()) {
+ for (const item of inline.split(",")) {
+ const cleaned = item.trim().replace(/^"|"$/g, "");
+ if (!cleaned) {
+ continue;
+ }
+ const match = cleaned.match(/^([^ ]+)(?: ([^ ]+))?/);
+ if (match) {
+ current.dependencies.push({ name: match[1], version: match[2] || "" });
+ }
+ }
+ inDependenciesArray = false;
+ }
+ }
+ }
+
+ flushCurrent();
+ return deduplicateCargoRecords(records);
+}
+
+function deduplicateCargoRecords(records) {
+ const seen = new Set();
+ const results = [];
+ for (const record of records) {
+ const key = `${record.name.toLowerCase()}@${record.version.toLowerCase()}`;
+ if (seen.has(key)) {
+ continue;
+ }
+ seen.add(key);
+ results.push(record);
+ }
+ return results;
+}
+
+function selectCargoRecord(recordsByName, name, version) {
+ const candidates = recordsByName.get(name.toLowerCase()) || [];
+ if (candidates.length === 0) {
+ return null;
+ }
+ if (version) {
+ const exactMatch = candidates.find((record) => record.version === version);
+ if (exactMatch) {
+ return exactMatch;
+ }
+ }
+ return candidates[0];
+}
+
+function buildCargoDependency(record, parentChain, recordsByName, visiting, sourceFile, directNames) {
+ const key = `${record.name.toLowerCase()}@${record.version.toLowerCase()}`;
+ if (visiting.has(key)) {
+ return createDependency({
+ name: record.name,
+ version: record.version,
+ ecosystem: "cargo",
+ isDirect: parentChain.length === 0 || directNames.has(record.name.toLowerCase()),
+ parent: parentChain[parentChain.length - 1] || null,
+ parentChain,
+ transitives: [],
+ sourceFile,
+ isDevelopmentDependency: false,
+ });
+ }
+
+ const nextVisiting = new Set(visiting);
+ nextVisiting.add(key);
+ const nextParentChain = parentChain.concat(record.name);
+ const transitives = [];
+
+ for (const dependency of record.dependencies) {
+ const childRecord = selectCargoRecord(recordsByName, dependency.name, dependency.version);
+ if (!childRecord) {
+ continue;
+ }
+ transitives.push(buildCargoDependency(
+ childRecord,
+ nextParentChain,
+ recordsByName,
+ nextVisiting,
+ sourceFile,
+ directNames
+ ));
+ }
+
+ return createDependency({
+ name: record.name,
+ version: record.version,
+ ecosystem: "cargo",
+ isDirect: parentChain.length === 0 || directNames.has(record.name.toLowerCase()),
+ parent: parentChain[parentChain.length - 1] || null,
+ parentChain,
+ transitives: deduplicateDeps(transitives),
+ sourceFile,
+ isDevelopmentDependency: false,
+ });
+}
+
+module.exports = cargoParser;
diff --git a/util/lockfileParsers/composerParser.js b/util/lockfileParsers/composerParser.js
new file mode 100644
index 0000000..0f250fc
--- /dev/null
+++ b/util/lockfileParsers/composerParser.js
@@ -0,0 +1,174 @@
+// Copyright 2026 Cloudsmith Ltd. All rights reserved.
+const path = require("path");
+const {
+ buildTree,
+ createDependency,
+ deduplicateDeps,
+ flattenDependencies,
+ getSourceFileName,
+ getWorkspacePath,
+ readJson,
+ pathExists,
+ readUtf8,
+} = require("./shared");
+const { parseComposerManifest } = require("./manifestHelpers");
+
+const composerParser = {
+ name: "composerParser",
+ ecosystem: "composer",
+
+ async canResolve(workspaceFolder) {
+ const rootPath = getWorkspacePath(workspaceFolder);
+ return (await pathExists(path.join(rootPath, "composer.lock"), workspaceFolder))
+ || (await pathExists(path.join(rootPath, "composer.json"), workspaceFolder));
+ },
+
+ async detect(workspaceFolder) {
+ const rootPath = getWorkspacePath(workspaceFolder);
+ const lockfilePath = await pathExists(path.join(rootPath, "composer.lock"), workspaceFolder)
+ ? path.join(rootPath, "composer.lock")
+ : null;
+ const manifestPath = await pathExists(path.join(rootPath, "composer.json"), workspaceFolder)
+ ? path.join(rootPath, "composer.json")
+ : null;
+ if (!lockfilePath && !manifestPath) {
+ return [];
+ }
+ return [{
+ resolverName: this.name,
+ ecosystem: this.ecosystem,
+ lockfilePath,
+ manifestPath,
+ sourceFile: getSourceFileName(lockfilePath || manifestPath),
+ }];
+ },
+
+ async resolve({ lockfilePath, manifestPath, workspaceFolder }) {
+ const sourceFile = getSourceFileName(lockfilePath || manifestPath);
+ const manifestDependencies = manifestPath && await pathExists(manifestPath, workspaceFolder)
+ ? parseComposerManifest(await readUtf8(manifestPath, workspaceFolder))
+ : [];
+
+ if (!lockfilePath) {
+ return buildTree("composer", sourceFile, manifestDependencies.map((dependency) => createDependency({
+ name: dependency.name,
+ version: dependency.version,
+ ecosystem: "composer",
+ isDirect: true,
+ parent: null,
+ parentChain: [],
+ transitives: [],
+ sourceFile,
+ isDevelopmentDependency: dependency.isDevelopmentDependency,
+ })));
+ }
+
+ const root = await readJson(lockfilePath);
+ const records = [];
+
+ for (const record of [...(root.packages || []), ...(root["packages-dev"] || [])]) {
+ if (!record || !record.name) {
+ continue;
+ }
+ records.push({
+ name: record.name,
+ version: record.version || "",
+ dependencies: Object.keys(record.require || {}).filter((name) => name.includes("/") && !name.startsWith("ext-") && !name.startsWith("lib-") && name !== "php"),
+ });
+ }
+
+ const directNames = new Set(manifestDependencies.map((dependency) => dependency.name.toLowerCase()));
+ const recordsByName = new Map(records.map((record) => [record.name.toLowerCase(), record]));
+ const incomingCounts = new Map();
+ for (const record of records) {
+ for (const dependencyName of record.dependencies) {
+ incomingCounts.set(dependencyName.toLowerCase(), (incomingCounts.get(dependencyName.toLowerCase()) || 0) + 1);
+ }
+ }
+
+ const rootRecords = directNames.size > 0
+ ? [...directNames].map((name) => recordsByName.get(name)).filter(Boolean)
+ : records.filter((record) => !incomingCounts.get(record.name.toLowerCase()));
+
+ const directRoots = deduplicateDeps(rootRecords.map((record) => buildComposerDependency(
+ record,
+ [],
+ recordsByName,
+ new Set(),
+ sourceFile,
+ directNames
+ )));
+ let dependencies = deduplicateDeps(flattenDependencies(directRoots));
+
+ for (const record of records) {
+ const key = `${record.name.toLowerCase()}@${record.version.toLowerCase()}`;
+ if (dependencies.some((dependency) => `${dependency.name.toLowerCase()}@${dependency.version.toLowerCase()}` === key)) {
+ continue;
+ }
+ dependencies.push(createDependency({
+ name: record.name,
+ version: record.version,
+ ecosystem: "composer",
+ isDirect: directNames.has(record.name.toLowerCase()),
+ parent: null,
+ parentChain: [],
+ transitives: [],
+ sourceFile,
+ isDevelopmentDependency: false,
+ }));
+ }
+
+ return buildTree("composer", sourceFile, dependencies);
+ },
+};
+
+function buildComposerDependency(record, parentChain, recordsByName, visiting, sourceFile, directNames) {
+ const key = `${record.name.toLowerCase()}@${record.version.toLowerCase()}`;
+ if (visiting.has(key)) {
+ return createDependency({
+ name: record.name,
+ version: record.version,
+ ecosystem: "composer",
+ isDirect: parentChain.length === 0 || directNames.has(record.name.toLowerCase()),
+ parent: parentChain[parentChain.length - 1] || null,
+ parentChain,
+ transitives: [],
+ sourceFile,
+ isDevelopmentDependency: false,
+ });
+ }
+
+ const nextVisiting = new Set(visiting);
+ nextVisiting.add(key);
+ const nextParentChain = parentChain.concat(record.name);
+ const transitives = [];
+
+ for (const dependencyName of record.dependencies) {
+ const childRecord = recordsByName.get(dependencyName.toLowerCase());
+ if (!childRecord) {
+ continue;
+ }
+ transitives.push(buildComposerDependency(
+ childRecord,
+ nextParentChain,
+ recordsByName,
+ nextVisiting,
+ sourceFile,
+ directNames
+ ));
+ }
+
+ return createDependency({
+ name: record.name,
+ version: record.version,
+ ecosystem: "composer",
+ isDirect: parentChain.length === 0 || directNames.has(record.name.toLowerCase()),
+ parent: parentChain[parentChain.length - 1] || null,
+ parentChain,
+ transitives: deduplicateDeps(transitives),
+ sourceFile,
+ isDevelopmentDependency: false,
+ });
+}
+
+module.exports = composerParser;
diff --git a/util/lockfileParsers/dartParser.js b/util/lockfileParsers/dartParser.js
new file mode 100644
index 0000000..7e3689b
--- /dev/null
+++ b/util/lockfileParsers/dartParser.js
@@ -0,0 +1,124 @@
+// Copyright 2026 Cloudsmith Ltd. All rights reserved.
+const path = require("path");
+const {
+ buildTree,
+ countIndent,
+ createDependency,
+ getSourceFileName,
+ getWorkspacePath,
+ pathExists,
+ readUtf8,
+ stripYamlComment,
+} = require("./shared");
+const { parsePubspecManifest } = require("./manifestHelpers");
+
+const dartParser = {
+ name: "dartParser",
+ ecosystem: "dart",
+
+ async canResolve(workspaceFolder) {
+ const rootPath = getWorkspacePath(workspaceFolder);
+ return (await pathExists(path.join(rootPath, "pubspec.lock"), workspaceFolder))
+ || (await pathExists(path.join(rootPath, "pubspec.yaml"), workspaceFolder));
+ },
+
+ async detect(workspaceFolder) {
+ const rootPath = getWorkspacePath(workspaceFolder);
+ const lockfilePath = await pathExists(path.join(rootPath, "pubspec.lock"), workspaceFolder)
+ ? path.join(rootPath, "pubspec.lock")
+ : null;
+ const manifestPath = await pathExists(path.join(rootPath, "pubspec.yaml"), workspaceFolder)
+ ? path.join(rootPath, "pubspec.yaml")
+ : null;
+ if (!lockfilePath && !manifestPath) {
+ return [];
+ }
+ return [{
+ resolverName: this.name,
+ ecosystem: this.ecosystem,
+ lockfilePath,
+ manifestPath,
+ sourceFile: getSourceFileName(lockfilePath || manifestPath),
+ }];
+ },
+
+ async resolve({ lockfilePath, manifestPath, workspaceFolder }) {
+ const sourceFile = getSourceFileName(lockfilePath || manifestPath);
+ if (!lockfilePath) {
+ return buildTree("dart", sourceFile, parsePubspecManifest(await readUtf8(manifestPath, workspaceFolder)).map((dependency) => createDependency({
+ name: dependency.name,
+ version: dependency.version,
+ ecosystem: "dart",
+ isDirect: true,
+ parent: null,
+ parentChain: [],
+ transitives: [],
+ sourceFile,
+ isDevelopmentDependency: dependency.isDevelopmentDependency,
+ })));
+ }
+
+ const dependencies = [];
+ let inPackages = false;
+ let current = null;
+
+ const flushCurrent = () => {
+ if (!current || !current.name) {
+ current = null;
+ return;
+ }
+ dependencies.push(createDependency({
+ name: current.name,
+ version: current.version,
+ ecosystem: "dart",
+ isDirect: !String(current.dependencyType || "").toLowerCase().includes("transitive"),
+ parent: null,
+ parentChain: [],
+ transitives: [],
+ sourceFile,
+ isDevelopmentDependency: String(current.dependencyType || "").toLowerCase().includes("dev"),
+ }));
+ current = null;
+ };
+
+ for (const rawLine of String(await readUtf8(lockfilePath, workspaceFolder)).split(/\r?\n/)) {
+ const line = stripYamlComment(rawLine).trim();
+ if (!line) {
+ continue;
+ }
+
+ const indent = countIndent(rawLine);
+ if (indent === 0 && line === "packages:") {
+ inPackages = true;
+ continue;
+ }
+ if (indent === 0 && line.endsWith(":") && line !== "packages:") {
+ inPackages = false;
+ flushCurrent();
+ continue;
+ }
+ if (!inPackages) {
+ continue;
+ }
+ if (indent === 2 && line.endsWith(":")) {
+ flushCurrent();
+ current = { name: line.slice(0, -1), version: "", dependencyType: "" };
+ continue;
+ }
+ if (!current) {
+ continue;
+ }
+ if (indent === 4 && line.startsWith("dependency:")) {
+ current.dependencyType = line.slice("dependency:".length).trim().replace(/^["']|["']$/g, "");
+ }
+ if (indent === 4 && line.startsWith("version:")) {
+ current.version = line.slice("version:".length).trim().replace(/^["']|["']$/g, "");
+ }
+ }
+
+ flushCurrent();
+ return buildTree("dart", sourceFile, dependencies);
+ },
+};
+
+module.exports = dartParser;
diff --git a/util/lockfileParsers/dockerParser.js b/util/lockfileParsers/dockerParser.js
new file mode 100644
index 0000000..7abf4d7
--- /dev/null
+++ b/util/lockfileParsers/dockerParser.js
@@ -0,0 +1,300 @@
+// Copyright 2026 Cloudsmith Ltd. All rights reserved.
+const path = require("path");
+const {
+ buildTree,
+ countIndent,
+ createDependency,
+ getSourceFileName,
+ getWorkspacePath,
+ readUtf8,
+ resolveWorkspaceFilePath,
+ stripYamlComment,
+} = require("./shared");
+
+const dockerParser = {
+ name: "dockerParser",
+ ecosystem: "docker",
+
+ async canResolve(workspaceFolder) {
+ const matches = await this.detect(workspaceFolder);
+ return matches.length > 0;
+ },
+
+ async detect(workspaceFolder) {
+ const rootPath = getWorkspacePath(workspaceFolder);
+ const safeRootPath = await resolveWorkspaceFilePath(rootPath, workspaceFolder);
+ if (!safeRootPath) {
+ return [];
+ }
+ const entries = [];
+ const allFiles = await require("fs").promises.readdir(safeRootPath);
+
+ for (const fileName of allFiles.sort()) {
+ const isDockerfile = fileName === "Dockerfile" || fileName.startsWith("Dockerfile.");
+ const isComposeFile = [
+ "docker-compose.yml",
+ "docker-compose.yaml",
+ "compose.yml",
+ "compose.yaml",
+ ].includes(fileName);
+ if (!isDockerfile && !isComposeFile) {
+ continue;
+ }
+ entries.push({
+ resolverName: this.name,
+ ecosystem: this.ecosystem,
+ lockfilePath: path.join(safeRootPath, fileName),
+ manifestPath: null,
+ sourceFile: fileName,
+ });
+ }
+
+ return entries;
+ },
+
+ async resolve({ lockfilePath, workspaceFolder }) {
+ const sourceFile = getSourceFileName(lockfilePath);
+ const content = await readUtf8(lockfilePath, workspaceFolder);
+ const dependencies = isComposeFileName(sourceFile)
+ ? parseCompose(content, sourceFile)
+ : parseDockerfile(content, sourceFile);
+ return buildTree("docker", sourceFile, dependencies);
+ },
+};
+
+function parseDockerfile(content, sourceFile) {
+ const dependencies = [];
+ const stageAliases = new Set();
+ const argDefaults = new Map();
+
+ for (const instruction of toLogicalDockerLines(content)) {
+ const cleaned = stripDockerComment(instruction).trim();
+ if (!cleaned) {
+ continue;
+ }
+
+ if (/^ARG\s+/i.test(cleaned)) {
+ const definition = cleaned.replace(/^ARG\s+/i, "");
+ const [name, value] = definition.split("=", 2);
+ if (name && value) {
+ argDefaults.set(name.trim(), resolveDockerArgs(value.trim(), argDefaults));
+ }
+ continue;
+ }
+
+ if (!/^FROM\s+/i.test(cleaned)) {
+ continue;
+ }
+
+ const parsed = parseFromInstruction(cleaned, argDefaults, stageAliases);
+ if (!parsed) {
+ continue;
+ }
+ if (parsed.alias) {
+ stageAliases.add(parsed.alias.toLowerCase());
+ }
+ if (!parsed.isDependency) {
+ continue;
+ }
+
+ dependencies.push(createDependency({
+ name: parsed.name,
+ version: parsed.version,
+ ecosystem: "docker",
+ isDirect: true,
+ parent: null,
+ parentChain: [],
+ transitives: [],
+ sourceFile,
+ isDevelopmentDependency: false,
+ }));
+ }
+
+ return dependencies;
+}
+
+function parseCompose(content, sourceFile) {
+ const dependencies = [];
+ let servicesIndent = null;
+ let currentService = null;
+
+ const flushCurrentService = () => {
+ if (!currentService) {
+ return;
+ }
+ if (!currentService.hasBuild && currentService.image) {
+ const parsed = parseDockerImageReference(currentService.image);
+ if (parsed && parsed.name.toLowerCase() !== "scratch") {
+ dependencies.push(createDependency({
+ name: parsed.name,
+ version: parsed.version,
+ ecosystem: "docker",
+ isDirect: true,
+ parent: null,
+ parentChain: [],
+ transitives: [],
+ sourceFile,
+ isDevelopmentDependency: false,
+ }));
+ }
+ }
+ currentService = null;
+ };
+
+ for (const rawLine of String(content || "").split(/\r?\n/)) {
+ const cleaned = stripYamlComment(rawLine).trim();
+ if (!cleaned) {
+ continue;
+ }
+
+ const indent = countIndent(rawLine);
+ if (cleaned === "services:") {
+ flushCurrentService();
+ servicesIndent = indent;
+ continue;
+ }
+ if (servicesIndent != null && indent <= servicesIndent && cleaned.endsWith(":")) {
+ flushCurrentService();
+ servicesIndent = null;
+ }
+ if (servicesIndent == null || indent <= servicesIndent) {
+ continue;
+ }
+
+ if (indent === servicesIndent + 2 && cleaned.endsWith(":")) {
+ flushCurrentService();
+ currentService = { indent, hasBuild: false, image: "" };
+ continue;
+ }
+
+ if (!currentService || indent <= currentService.indent || cleaned.startsWith("- ")) {
+ continue;
+ }
+
+ if (cleaned.startsWith("build:")) {
+ currentService.hasBuild = true;
+ continue;
+ }
+ if (cleaned.startsWith("image:")) {
+ currentService.image = unquote(cleaned.slice("image:".length).trim());
+ }
+ }
+
+ flushCurrentService();
+ return dependencies;
+}
+
+function toLogicalDockerLines(content) {
+ const lines = [];
+ let current = "";
+ for (const rawLine of String(content || "").split(/\r?\n/)) {
+ const trimmed = rawLine.trimEnd();
+ if (!trimmed) {
+ if (current) {
+ lines.push(current);
+ current = "";
+ }
+ continue;
+ }
+
+ const continues = trimmed.endsWith("\\");
+ const segment = continues ? trimmed.slice(0, -1).trimEnd() : trimmed;
+ current += current ? ` ${segment}` : segment;
+ if (!continues) {
+ lines.push(current);
+ current = "";
+ }
+ }
+ if (current) {
+ lines.push(current);
+ }
+ return lines;
+}
+
+function stripDockerComment(line) {
+ let inSingleQuote = false;
+ let inDoubleQuote = false;
+
+ for (let index = 0; index < line.length; index += 1) {
+ const char = line[index];
+ const previous = index > 0 ? line[index - 1] : "";
+ if (char === "'" && !inDoubleQuote && previous !== "\\") {
+ inSingleQuote = !inSingleQuote;
+ continue;
+ }
+ if (char === "\"" && !inSingleQuote && previous !== "\\") {
+ inDoubleQuote = !inDoubleQuote;
+ continue;
+ }
+ if (char === "#" && !inSingleQuote && !inDoubleQuote) {
+ return line.slice(0, index);
+ }
+ }
+ return line;
+}
+
+function resolveDockerArgs(value, args) {
+ return String(value || "")
+ .replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)(?:(:?[-+?])([^}]*))?}/g, (_match, name, operator, fallback) => {
+ if (args.has(name)) {
+ return args.get(name);
+ }
+ return operator === "-" || operator === ":-" ? fallback : _match;
+ })
+ .replace(/\$([A-Za-z_][A-Za-z0-9_]*)/g, (match, name) => (args.has(name) ? args.get(name) : match));
+}
+
+function parseFromInstruction(line, argDefaults, stageAliases) {
+ const parts = line.split(/\s+/).filter(Boolean);
+ let index = 1;
+ while (parts[index] && parts[index].startsWith("--")) {
+ index += 1;
+ }
+ const imageToken = parts[index];
+ if (!imageToken) {
+ return null;
+ }
+ const alias = parts[index + 1] && /^AS$/i.test(parts[index + 1]) ? parts[index + 2] : "";
+ const resolvedImage = resolveDockerArgs(unquote(imageToken), argDefaults).trim();
+ if (!resolvedImage) {
+ return null;
+ }
+ const stageReference = stageAliases.has(resolvedImage.toLowerCase());
+ const parsed = parseDockerImageReference(resolvedImage);
+ if (!parsed) {
+ return null;
+ }
+ return {
+ ...parsed,
+ alias: alias ? unquote(alias) : "",
+ isDependency: !stageReference && parsed.name.toLowerCase() !== "scratch",
+ };
+}
+
+function parseDockerImageReference(reference) {
+ const raw = unquote(reference);
+ if (!raw || raw.includes("$")) {
+ return null;
+ }
+ const withoutDigest = raw.split("@")[0];
+ const digest = raw.includes("@") ? raw.split("@")[1] : "";
+ const lastSlash = withoutDigest.lastIndexOf("/");
+ const lastColon = withoutDigest.lastIndexOf(":");
+ const hasTag = lastColon > lastSlash;
+ const name = hasTag ? withoutDigest.slice(0, lastColon) : withoutDigest;
+ const version = hasTag ? withoutDigest.slice(lastColon + 1) : digest || "latest";
+ if (!name) {
+ return null;
+ }
+ return { name, version };
+}
+
+function unquote(value) {
+ return String(value || "").trim().replace(/^["']|["']$/g, "");
+}
+
+function isComposeFileName(fileName) {
+ return ["docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml"].includes(fileName);
+}
+
+module.exports = dockerParser;
diff --git a/util/lockfileParsers/goParser.js b/util/lockfileParsers/goParser.js
new file mode 100644
index 0000000..2ec141b
--- /dev/null
+++ b/util/lockfileParsers/goParser.js
@@ -0,0 +1,82 @@
+// Copyright 2026 Cloudsmith Ltd. All rights reserved.
+const path = require("path");
+const {
+ buildTree,
+ createDependency,
+ getSourceFileName,
+ getWorkspacePath,
+ pathExists,
+ readUtf8,
+} = require("./shared");
+
+const goParser = {
+ name: "goParser",
+ ecosystem: "go",
+
+ async canResolve(workspaceFolder) {
+ return pathExists(path.join(getWorkspacePath(workspaceFolder), "go.mod"), workspaceFolder);
+ },
+
+ async detect(workspaceFolder) {
+ const rootPath = getWorkspacePath(workspaceFolder);
+ const manifestPath = path.join(rootPath, "go.mod");
+ if (!(await pathExists(manifestPath, workspaceFolder))) {
+ return [];
+ }
+ return [{
+ resolverName: this.name,
+ ecosystem: this.ecosystem,
+ lockfilePath: manifestPath,
+ manifestPath,
+ sourceFile: "go.mod",
+ }];
+ },
+
+ async resolve({ manifestPath, workspaceFolder }) {
+ const dependencies = [];
+ const sourceFile = getSourceFileName(manifestPath);
+ let inRequireBlock = false;
+
+ for (const rawLine of String(await readUtf8(manifestPath, workspaceFolder)).split(/\r?\n/)) {
+ const line = rawLine.trim();
+ if (!line || line.startsWith("//")) {
+ continue;
+ }
+ if (line === "require (") {
+ inRequireBlock = true;
+ continue;
+ }
+ if (line === ")" && inRequireBlock) {
+ inRequireBlock = false;
+ continue;
+ }
+
+ const lineToParse = line.startsWith("require ") ? line.slice("require ".length).trim() : line;
+ if (!inRequireBlock && !line.startsWith("require ")) {
+ continue;
+ }
+
+ const cleaned = lineToParse.split("//")[0].trim();
+ const parts = cleaned.split(/\s+/);
+ if (parts.length < 2) {
+ continue;
+ }
+
+ dependencies.push(createDependency({
+ name: parts[0],
+ version: parts[1].replace(/^v/, ""),
+ ecosystem: "go",
+ isDirect: !rawLine.includes("// indirect"),
+ parent: null,
+ parentChain: [],
+ transitives: [],
+ sourceFile,
+ isDevelopmentDependency: rawLine.includes("// indirect"),
+ }));
+ }
+
+ return buildTree("go", sourceFile, dependencies);
+ },
+};
+
+module.exports = goParser;
diff --git a/util/lockfileParsers/gradleParser.js b/util/lockfileParsers/gradleParser.js
new file mode 100644
index 0000000..180bba9
--- /dev/null
+++ b/util/lockfileParsers/gradleParser.js
@@ -0,0 +1,124 @@
+// Copyright 2026 Cloudsmith Ltd. All rights reserved.
+const path = require("path");
+const {
+ buildTree,
+ createDependency,
+ deduplicateDeps,
+ getSourceFileName,
+ getWorkspacePath,
+ pathExists,
+ readUtf8,
+} = require("./shared");
+const { parseBuildGradleManifest } = require("./manifestHelpers");
+
+const BUILD_FILES = ["build.gradle", "build.gradle.kts"];
+
+const gradleParser = {
+ name: "gradleParser",
+ ecosystem: "gradle",
+
+ async canResolve(workspaceFolder) {
+ const rootPath = getWorkspacePath(workspaceFolder);
+ for (const buildFile of BUILD_FILES) {
+ if (await pathExists(path.join(rootPath, buildFile), workspaceFolder)) {
+ return true;
+ }
+ }
+ return false;
+ },
+
+ async detect(workspaceFolder) {
+ const rootPath = getWorkspacePath(workspaceFolder);
+ for (const buildFile of BUILD_FILES) {
+ const manifestPath = path.join(rootPath, buildFile);
+ if (!(await pathExists(manifestPath, workspaceFolder))) {
+ continue;
+ }
+ const lockfilePath = await pathExists(path.join(rootPath, "gradle.lockfile"), workspaceFolder)
+ ? path.join(rootPath, "gradle.lockfile")
+ : null;
+ return [{
+ resolverName: this.name,
+ ecosystem: this.ecosystem,
+ lockfilePath,
+ manifestPath,
+ sourceFile: buildFile,
+ }];
+ }
+ return [];
+ },
+
+ async resolve({ lockfilePath, manifestPath, workspaceFolder }) {
+ const directDependencies = parseBuildGradleManifest(await readUtf8(manifestPath, workspaceFolder));
+ const sourceFile = getSourceFileName(manifestPath);
+
+ if (!lockfilePath) {
+ return buildTree("gradle", sourceFile, directDependencies.map((dependency) => createDependency({
+ name: dependency.name,
+ version: dependency.version,
+ ecosystem: "gradle",
+ isDirect: true,
+ parent: null,
+ parentChain: [],
+ transitives: [],
+ sourceFile,
+ isDevelopmentDependency: dependency.isDevelopmentDependency,
+ })));
+ }
+
+ const lockVersions = parseGradleLockfile(await readUtf8(lockfilePath, workspaceFolder));
+ const dependencies = [];
+
+ for (const directDependency of directDependencies) {
+ const resolvedVersion = lockVersions.get(directDependency.name) || directDependency.version;
+ dependencies.push(createDependency({
+ name: directDependency.name,
+ version: resolvedVersion,
+ ecosystem: "gradle",
+ isDirect: true,
+ parent: null,
+ parentChain: [],
+ transitives: [],
+ sourceFile,
+ isDevelopmentDependency: directDependency.isDevelopmentDependency,
+ }));
+ }
+
+ for (const [name, version] of lockVersions.entries()) {
+ dependencies.push(createDependency({
+ name,
+ version,
+ ecosystem: "gradle",
+ isDirect: directDependencies.some((dependency) => dependency.name === name),
+ parent: null,
+ parentChain: [],
+ transitives: [],
+ sourceFile,
+ isDevelopmentDependency: false,
+ }));
+ }
+
+ return buildTree("gradle", sourceFile, deduplicateDeps(dependencies));
+ },
+};
+
+function parseGradleLockfile(content) {
+ const versions = new Map();
+
+ for (const rawLine of String(content || "").split(/\r?\n/)) {
+ const line = rawLine.trim();
+ if (!line || line.startsWith("#")) {
+ continue;
+ }
+ const entry = line.split("=", 1)[0].trim();
+ const parts = entry.split(":");
+ if (parts.length < 3) {
+ continue;
+ }
+ versions.set(`${parts[0]}:${parts[1]}`, parts[2]);
+ }
+
+ return versions;
+}
+
+module.exports = gradleParser;
diff --git a/util/lockfileParsers/helmParser.js b/util/lockfileParsers/helmParser.js
new file mode 100644
index 0000000..abc5a61
--- /dev/null
+++ b/util/lockfileParsers/helmParser.js
@@ -0,0 +1,62 @@
+// Copyright 2026 Cloudsmith Ltd. All rights reserved.
+const path = require("path");
+const {
+ buildTree,
+ createDependency,
+ getSourceFileName,
+ getWorkspacePath,
+ pathExists,
+ readUtf8,
+} = require("./shared");
+const { parseChartManifest } = require("./manifestHelpers");
+
+const helmParser = {
+ name: "helmParser",
+ ecosystem: "helm",
+
+ async canResolve(workspaceFolder) {
+ const rootPath = getWorkspacePath(workspaceFolder);
+ return (await pathExists(path.join(rootPath, "Chart.lock"), workspaceFolder))
+ || (await pathExists(path.join(rootPath, "Chart.yaml"), workspaceFolder));
+ },
+
+ async detect(workspaceFolder) {
+ const rootPath = getWorkspacePath(workspaceFolder);
+ const lockfilePath = await pathExists(path.join(rootPath, "Chart.lock"), workspaceFolder)
+ ? path.join(rootPath, "Chart.lock")
+ : null;
+ const manifestPath = await pathExists(path.join(rootPath, "Chart.yaml"), workspaceFolder)
+ ? path.join(rootPath, "Chart.yaml")
+ : null;
+ if (!lockfilePath && !manifestPath) {
+ return [];
+ }
+ return [{
+ resolverName: this.name,
+ ecosystem: this.ecosystem,
+ lockfilePath,
+ manifestPath,
+ sourceFile: getSourceFileName(lockfilePath || manifestPath),
+ }];
+ },
+
+ async resolve({ lockfilePath, manifestPath, workspaceFolder }) {
+ const sourcePath = lockfilePath || manifestPath;
+ const sourceFile = getSourceFileName(sourcePath);
+ const dependencies = parseChartManifest(await readUtf8(sourcePath, workspaceFolder)).map((dependency) => createDependency({
+ name: dependency.name,
+ version: dependency.version,
+ ecosystem: "helm",
+ isDirect: true,
+ parent: null,
+ parentChain: [],
+ transitives: [],
+ sourceFile,
+ isDevelopmentDependency: false,
+ }));
+
+ return buildTree("helm", sourceFile, dependencies);
+ },
+};
+
+module.exports = helmParser;
diff --git a/util/lockfileParsers/hexParser.js b/util/lockfileParsers/hexParser.js
new file mode 100644
index 0000000..a0808b2
--- /dev/null
+++ b/util/lockfileParsers/hexParser.js
@@ -0,0 +1,86 @@
+// Copyright 2026 Cloudsmith Ltd. All rights reserved.
+const path = require("path");
+const {
+ buildTree,
+ createDependency,
+ getSourceFileName,
+ getWorkspacePath,
+ pathExists,
+ readUtf8,
+} = require("./shared");
+const { parseMixExsManifest } = require("./manifestHelpers");
+
+const hexParser = {
+ name: "hexParser",
+ ecosystem: "hex",
+
+ async canResolve(workspaceFolder) {
+ const rootPath = getWorkspacePath(workspaceFolder);
+ return (await pathExists(path.join(rootPath, "mix.lock"), workspaceFolder))
+ || (await pathExists(path.join(rootPath, "mix.exs"), workspaceFolder));
+ },
+
+ async detect(workspaceFolder) {
+ const rootPath = getWorkspacePath(workspaceFolder);
+ const lockfilePath = await pathExists(path.join(rootPath, "mix.lock"), workspaceFolder)
+ ? path.join(rootPath, "mix.lock")
+ : null;
+ const manifestPath = await pathExists(path.join(rootPath, "mix.exs"), workspaceFolder)
+ ? path.join(rootPath, "mix.exs")
+ : null;
+ if (!lockfilePath && !manifestPath) {
+ return [];
+ }
+ return [{
+ resolverName: this.name,
+ ecosystem: this.ecosystem,
+ lockfilePath,
+ manifestPath,
+ sourceFile: getSourceFileName(lockfilePath || manifestPath),
+ }];
+ },
+
+ async resolve({ lockfilePath, manifestPath, workspaceFolder }) {
+ const sourceFile = getSourceFileName(lockfilePath || manifestPath);
+ if (!lockfilePath) {
+ return buildTree("hex", sourceFile, parseMixExsManifest(await readUtf8(manifestPath, workspaceFolder)).map((dependency) => createDependency({
+ name: dependency.name,
+ version: dependency.version,
+ ecosystem: "hex",
+ isDirect: true,
+ parent: null,
+ parentChain: [],
+ transitives: [],
+ sourceFile,
+ isDevelopmentDependency: false,
+ })));
+ }
+
+ const directNames = manifestPath && await pathExists(manifestPath, workspaceFolder)
+ ? new Set(parseMixExsManifest(await readUtf8(manifestPath, workspaceFolder)).map((dependency) => dependency.name.toLowerCase()))
+ : new Set();
+ const entryPattern = /"([^"]+)"\s*:\s*\{\s*:hex,\s*(?::"[^"]+"|:[^,]+)\s*,\s*"([^"]+)"/g;
+ const dependencies = [];
+ for (const match of String(await readUtf8(lockfilePath, workspaceFolder)).matchAll(entryPattern)) {
+ dependencies.push(createDependency({
+ name: match[1],
+ version: match[2],
+ ecosystem: "hex",
+ isDirect: directNames.size === 0 || directNames.has(match[1].toLowerCase()),
+ parent: null,
+ parentChain: [],
+ transitives: [],
+ sourceFile,
+ isDevelopmentDependency: false,
+ }));
+ }
+
+ if (dependencies.length === 0) {
+ throw new Error("Malformed mix.lock: no Hex package entries found");
+ }
+
+ return buildTree("hex", sourceFile, dependencies);
+ },
+};
+
+module.exports = hexParser;
diff --git a/util/lockfileParsers/manifestHelpers.js b/util/lockfileParsers/manifestHelpers.js
new file mode 100644
index 0000000..3aa59d9
--- /dev/null
+++ b/util/lockfileParsers/manifestHelpers.js
@@ -0,0 +1,650 @@
+// Copyright 2026 Cloudsmith Ltd. All rights reserved.
+const {
+ escapeRegExp,
+ normalizeVersion,
+ parseInlineTomlValue,
+ parseKeyValueLine,
+ parseQuotedArray,
+ stripTomlComment,
+ stripYamlComment,
+} = require("./shared");
+
+function parsePackageJsonManifest(content) {
+ let parsed;
+ try {
+ parsed = JSON.parse(content);
+ } catch {
+ return {
+ dependencies: [],
+ directNames: new Set(),
+ devNames: new Set(),
+ };
+ }
+
+ const dependencies = [];
+ const directNames = new Set();
+ const devNames = new Set();
+
+ const addSection = (sectionName, isDevelopmentDependency) => {
+ const section = parsed[sectionName];
+ if (!section || typeof section !== "object") {
+ return;
+ }
+
+ for (const [name, version] of Object.entries(section)) {
+ dependencies.push({
+ name,
+ version: normalizeVersion(version),
+ isDevelopmentDependency,
+ });
+ if (isDevelopmentDependency) {
+ devNames.add(name);
+ } else {
+ directNames.add(name);
+ }
+ }
+ };
+
+ addSection("dependencies", false);
+ addSection("devDependencies", true);
+ addSection("optionalDependencies", false);
+ addSection("peerDependencies", false);
+
+ return {
+ dependencies,
+ directNames,
+ devNames,
+ };
+}
+
+function parsePyprojectManifest(content) {
+ const lines = String(content || "").split(/\r?\n/);
+ const dependencies = [];
+ const directNames = new Set();
+ const devNames = new Set();
+ let projectName = "";
+ let section = "";
+ let collectingProjectDependencies = false;
+ let projectDependenciesBuffer = "";
+
+ const flushProjectDependencies = () => {
+ if (!projectDependenciesBuffer) {
+ return;
+ }
+ for (const item of parseQuotedArray(projectDependenciesBuffer)) {
+ const parsed = parseRequirementSpec(item);
+ if (!parsed) {
+ continue;
+ }
+ dependencies.push({
+ ...parsed,
+ isDevelopmentDependency: false,
+ });
+ directNames.add(parsed.name);
+ }
+ projectDependenciesBuffer = "";
+ collectingProjectDependencies = false;
+ };
+
+ for (const rawLine of lines) {
+ const withoutComment = stripTomlComment(rawLine);
+ const line = withoutComment.trim();
+ if (!line || line.startsWith("#")) {
+ continue;
+ }
+
+ if (collectingProjectDependencies) {
+ projectDependenciesBuffer += projectDependenciesBuffer ? ` ${line}` : line;
+ if (projectDependenciesBuffer.includes("]")) {
+ flushProjectDependencies();
+ }
+ continue;
+ }
+
+ if (line.startsWith("[") && line.endsWith("]")) {
+ section = line;
+ continue;
+ }
+
+ if (section === "[project]" && line.startsWith("name =")) {
+ projectName = unquote(parseKeyValueLine(line).value);
+ continue;
+ }
+
+ if (section === "[tool.poetry]" && line.startsWith("name =")) {
+ projectName = unquote(parseKeyValueLine(line).value);
+ continue;
+ }
+
+ if (section === "[project]" && line.startsWith("dependencies")) {
+ projectDependenciesBuffer = parseKeyValueLine(line).value;
+ if (projectDependenciesBuffer.includes("]")) {
+ flushProjectDependencies();
+ } else {
+ collectingProjectDependencies = true;
+ }
+ continue;
+ }
+
+ if (
+ section === "[tool.poetry.dependencies]"
+ || section === "[tool.poetry.dev-dependencies]"
+ || /^\[tool\.poetry\.group\.[^.]+\.dependencies]$/.test(section)
+ ) {
+ const parts = parseKeyValueLine(line);
+ if (!parts) {
+ continue;
+ }
+
+ const name = parts.key;
+ if (!name || name.toLowerCase() === "python") {
+ continue;
+ }
+
+ const rawValue = parts.value;
+ const version = rawValue.startsWith("{")
+ ? normalizeVersion(parseInlineTomlValue(rawValue, "version"))
+ : normalizeVersion(unquote(rawValue));
+ const isDevelopmentDependency = section !== "[tool.poetry.dependencies]";
+
+ dependencies.push({
+ name,
+ version: version === "*" ? "" : version,
+ isDevelopmentDependency,
+ });
+
+ if (isDevelopmentDependency) {
+ devNames.add(name);
+ } else {
+ directNames.add(name);
+ }
+ }
+ }
+
+ return {
+ projectName,
+ dependencies,
+ directNames,
+ devNames,
+ };
+}
+
+function parseRequirementSpec(spec) {
+ const rawSpec = String(spec || "").trim().replace(/^["']|["']$/g, "");
+ if (!rawSpec) {
+ return null;
+ }
+
+ const withoutMarker = rawSpec.split(";")[0].trim();
+ const match = withoutMarker.match(/^([A-Za-z0-9_.-]+)(?:\[[^\]]+])?\s*(.*)$/);
+ if (!match) {
+ return null;
+ }
+
+ return {
+ name: match[1],
+ version: normalizeVersion(match[2] || ""),
+ };
+}
+
+function parseCargoTomlManifest(content) {
+ const dependencies = [];
+ let section = "";
+
+ for (const rawLine of String(content || "").split(/\r?\n/)) {
+ const line = stripTomlComment(rawLine).trim();
+ if (!line) {
+ continue;
+ }
+
+ if (line.startsWith("[") && line.endsWith("]")) {
+ section = line;
+ continue;
+ }
+
+ if (![
+ "[dependencies]",
+ "[dev-dependencies]",
+ "[build-dependencies]",
+ "[workspace.dependencies]",
+ ].includes(section)) {
+ continue;
+ }
+
+ const parts = parseKeyValueLine(line);
+ if (!parts) {
+ continue;
+ }
+
+ const declaredName = parts.key;
+ const rawValue = parts.value;
+ const actualName = parseInlineTomlValue(rawValue, "package") || declaredName;
+ const version = rawValue.startsWith("{")
+ ? normalizeVersion(parseInlineTomlValue(rawValue, "version"))
+ : normalizeVersion(unquote(rawValue));
+
+ dependencies.push({
+ name: actualName,
+ version,
+ isDevelopmentDependency: section !== "[dependencies]" && section !== "[workspace.dependencies]",
+ });
+ }
+
+ return dependencies;
+}
+
+function parseGemfileManifest(content) {
+ const dependencies = [];
+ const pattern = /^\s*gem\s+["']([^"']+)["'](?:\s*,\s*["']([^"']+)["'])?/;
+
+ for (const rawLine of String(content || "").split(/\r?\n/)) {
+ const line = stripRubyComment(rawLine).trim();
+ const match = line.match(pattern);
+ if (!match) {
+ continue;
+ }
+ dependencies.push({
+ name: match[1],
+ version: normalizeVersion(match[2] || ""),
+ isDevelopmentDependency: false,
+ });
+ }
+
+ return dependencies;
+}
+
+function parseBuildGradleManifest(content) {
+ const dependencies = [];
+ const lines = String(content || "").split(/\r?\n/);
+ let inDependenciesBlock = false;
+ let braceDepth = 0;
+ let dependencyBlockDepth = 0;
+
+ for (const rawLine of lines) {
+ const line = stripJavaLikeComment(rawLine).trim();
+ if (!line) {
+ braceDepth += countBraces(rawLine);
+ continue;
+ }
+
+ if (!inDependenciesBlock && /^dependencies\s*\{/.test(line)) {
+ inDependenciesBlock = true;
+ dependencyBlockDepth = braceDepth + 1;
+ } else if (!inDependenciesBlock && line === "dependencies") {
+ inDependenciesBlock = true;
+ dependencyBlockDepth = braceDepth + 1;
+ } else if (inDependenciesBlock) {
+ const parsed = parseGradleDependencyLine(line);
+ if (parsed) {
+ dependencies.push(parsed);
+ }
+ }
+
+ braceDepth += countBraces(rawLine);
+ if (inDependenciesBlock && braceDepth < dependencyBlockDepth) {
+ inDependenciesBlock = false;
+ dependencyBlockDepth = 0;
+ }
+ }
+
+ return dedupeManifestDeps(dependencies);
+}
+
+function parseGradleDependencyLine(line) {
+ const match = line.match(/^\s*([A-Za-z][A-Za-z0-9_-]*)\s*\(?\s*["']([^"']+)["']/);
+ if (!match) {
+ return null;
+ }
+
+ const configuration = match[1].toLowerCase();
+ const coordinates = match[2].split(":").filter(Boolean);
+ if (coordinates.length < 2) {
+ return null;
+ }
+
+ return {
+ name: `${coordinates[0]}:${coordinates[1]}`,
+ version: coordinates[2] ? normalizeVersion(coordinates[2]) : "",
+ isDevelopmentDependency: configuration.includes("test"),
+ };
+}
+
+function parseCsprojManifest(content) {
+ const dependencies = [];
+ const inlinePattern = /]*)\/>/gi;
+ const blockPattern = /]*)>([\s\S]*?)<\/PackageReference>/gi;
+
+ const parseAttributes = (attributesText, blockText) => {
+ const includeMatch = attributesText.match(/\b(?:Include|Update)="([^"]+)"/i);
+ if (!includeMatch) {
+ return;
+ }
+
+ let version = "";
+ const attributeVersionMatch = attributesText.match(/\bVersion="([^"]+)"/i);
+ if (attributeVersionMatch) {
+ version = attributeVersionMatch[1];
+ } else if (blockText) {
+ const nestedVersionMatch = blockText.match(/\s*([^<]+)\s*<\/Version>/i);
+ if (nestedVersionMatch) {
+ version = nestedVersionMatch[1];
+ }
+ }
+
+ dependencies.push({
+ name: includeMatch[1].trim(),
+ version: normalizeVersion(version),
+ isDevelopmentDependency: false,
+ });
+ };
+
+ for (const match of content.matchAll(inlinePattern)) {
+ parseAttributes(match[1], "");
+ }
+
+ for (const match of content.matchAll(blockPattern)) {
+ parseAttributes(match[1], match[2]);
+ }
+
+ return dedupeManifestDeps(dependencies);
+}
+
+function parsePubspecManifest(content) {
+ const dependencies = [];
+ let section = "";
+
+ for (const rawLine of String(content || "").split(/\r?\n/)) {
+ const lineWithoutComment = stripYamlComment(rawLine);
+ const line = lineWithoutComment.trim();
+ if (!line) {
+ continue;
+ }
+
+ const indent = rawLine.search(/\S/);
+ if (indent === 0 && line.endsWith(":")) {
+ section = line.slice(0, -1);
+ continue;
+ }
+
+ if (!["dependencies", "dev_dependencies"].includes(section) || indent !== 2) {
+ continue;
+ }
+
+ if (line.startsWith("-")) {
+ continue;
+ }
+
+ const name = line.split(":", 1)[0].trim();
+ const rawValue = line.includes(":") ? line.split(":").slice(1).join(":").trim() : "";
+ const version = rawValue.startsWith("{")
+ ? normalizeVersion(parseYamlInlineValue(rawValue, "version"))
+ : normalizeVersion(unquote(rawValue));
+
+ if (!name) {
+ continue;
+ }
+
+ dependencies.push({
+ name,
+ version,
+ isDevelopmentDependency: section === "dev_dependencies",
+ });
+ }
+
+ return dedupeManifestDeps(dependencies);
+}
+
+function parseComposerManifest(content) {
+ let parsed;
+ try {
+ parsed = JSON.parse(content);
+ } catch {
+ return [];
+ }
+
+ const dependencies = [];
+ for (const [name, version] of Object.entries(parsed.require || {})) {
+ if (!isComposerPackageName(name)) {
+ continue;
+ }
+ dependencies.push({ name, version: normalizeVersion(version), isDevelopmentDependency: false });
+ }
+ for (const [name, version] of Object.entries(parsed["require-dev"] || {})) {
+ if (!isComposerPackageName(name)) {
+ continue;
+ }
+ dependencies.push({ name, version: normalizeVersion(version), isDevelopmentDependency: true });
+ }
+ return dedupeManifestDeps(dependencies);
+}
+
+function parseChartManifest(content) {
+ return parseSimpleYamlDependencyList(content, "dependencies");
+}
+
+function parsePackageSwiftManifest(content) {
+ const dependencies = [];
+ const pattern = /\.package\s*\(([\s\S]*?)\)/g;
+
+ for (const match of content.matchAll(pattern)) {
+ const declaration = match[1];
+ const identityMatch = declaration.match(/\b(?:name|id|identity)\s*:\s*"([^"]+)"/);
+ const urlMatch = declaration.match(/\burl\s*:\s*"([^"]+)"/);
+ const versionMatch = declaration.match(/\b(?:from|exact|branch|revision)\s*:\s*"([^"]+)"/);
+ const name = normalizeSwiftIdentity(identityMatch ? identityMatch[1] : urlMatch ? urlMatch[1] : "");
+ if (!name) {
+ continue;
+ }
+ dependencies.push({
+ name,
+ version: normalizeVersion(versionMatch ? versionMatch[1] : ""),
+ isDevelopmentDependency: false,
+ });
+ }
+
+ return dedupeManifestDeps(dependencies);
+}
+
+function normalizeSwiftIdentity(name) {
+ return String(name || "")
+ .trim()
+ .split("/")
+ .filter(Boolean)
+ .pop()
+ ?.replace(/\.git$/i, "")
+ .toLowerCase() || "";
+}
+
+function parseMixExsManifest(content) {
+ const dependencies = [];
+ const depsBlockMatch = content.match(/defp\s+deps\s+do\s*\[([\s\S]*?)\]\s*end/m);
+ if (!depsBlockMatch) {
+ return [];
+ }
+
+ const pattern = /\{\s*:([A-Za-z0-9_]+)\s*,\s*"([^"]*)"/g;
+ for (const match of depsBlockMatch[1].matchAll(pattern)) {
+ dependencies.push({
+ name: match[1],
+ version: normalizeVersion(match[2]),
+ isDevelopmentDependency: false,
+ });
+ }
+ return dedupeManifestDeps(dependencies);
+}
+
+function parsePomManifest(content) {
+ const dependencies = [];
+ const dependencyBlocks = content.match(/[\s\S]*?<\/dependency>/gi) || [];
+
+ for (const block of dependencyBlocks) {
+ const groupId = matchXmlValue(block, "groupId");
+ const artifactId = matchXmlValue(block, "artifactId");
+ const scope = matchXmlValue(block, "scope");
+ if (!groupId || !artifactId) {
+ continue;
+ }
+
+ let version = matchXmlValue(block, "version");
+ if (version && /\$\{[^}]+}/.test(version)) {
+ version = "";
+ }
+
+ dependencies.push({
+ name: `${groupId}:${artifactId}`,
+ version: normalizeVersion(version),
+ isDevelopmentDependency: scope === "test",
+ });
+ }
+
+ return dedupeManifestDeps(dependencies);
+}
+
+function parseSimpleYamlDependencyList(content, sectionName) {
+ const dependencies = [];
+ let inSection = false;
+ let currentDependency = null;
+
+ const flushCurrent = () => {
+ if (!currentDependency || !currentDependency.name) {
+ currentDependency = null;
+ return;
+ }
+ dependencies.push({
+ name: currentDependency.name,
+ version: normalizeVersion(currentDependency.version),
+ isDevelopmentDependency: false,
+ });
+ currentDependency = null;
+ };
+
+ for (const rawLine of String(content || "").split(/\r?\n/)) {
+ const lineWithoutComment = stripYamlComment(rawLine);
+ const line = lineWithoutComment.trim();
+ if (!line) {
+ continue;
+ }
+
+ const indent = rawLine.search(/\S/);
+ if (indent === 0 && line === `${sectionName}:`) {
+ inSection = true;
+ continue;
+ }
+ if (indent === 0 && line.endsWith(":") && line !== `${sectionName}:`) {
+ inSection = false;
+ flushCurrent();
+ continue;
+ }
+ if (!inSection) {
+ continue;
+ }
+
+ if (indent === 2 && line.startsWith("- ")) {
+ flushCurrent();
+ currentDependency = { name: "", version: "" };
+ const remainder = line.slice(2).trim();
+ if (remainder.startsWith("name:")) {
+ currentDependency.name = remainder.slice("name:".length).trim();
+ }
+ continue;
+ }
+
+ if (!currentDependency) {
+ continue;
+ }
+
+ if (indent >= 4 && line.startsWith("name:")) {
+ currentDependency.name = line.slice("name:".length).trim();
+ }
+ if (indent >= 4 && line.startsWith("version:")) {
+ currentDependency.version = line.slice("version:".length).trim();
+ }
+ }
+
+ flushCurrent();
+ return dedupeManifestDeps(dependencies);
+}
+
+function dedupeManifestDeps(dependencies) {
+ const seen = new Set();
+ const results = [];
+ for (const dependency of dependencies) {
+ const key = `${dependency.name.toLowerCase()}:${dependency.version.toLowerCase()}:${dependency.isDevelopmentDependency}`;
+ if (seen.has(key)) {
+ continue;
+ }
+ seen.add(key);
+ results.push(dependency);
+ }
+ return results;
+}
+
+function parseYamlInlineValue(block, key) {
+ const match = String(block || "").match(new RegExp(`${escapeRegExp(key)}\\s*:\\s*([^,}]+)`));
+ return match ? unquote(match[1].trim()) : "";
+}
+
+function unquote(value) {
+ return String(value || "").trim().replace(/^["']|["']$/g, "");
+}
+
+function stripRubyComment(line) {
+ let inSingleQuote = false;
+ let inDoubleQuote = false;
+ for (let index = 0; index < line.length; index += 1) {
+ const char = line[index];
+ const previous = index > 0 ? line[index - 1] : "";
+ if (char === "'" && !inDoubleQuote && previous !== "\\") {
+ inSingleQuote = !inSingleQuote;
+ continue;
+ }
+ if (char === "\"" && !inSingleQuote && previous !== "\\") {
+ inDoubleQuote = !inDoubleQuote;
+ continue;
+ }
+ if (char === "#" && !inSingleQuote && !inDoubleQuote) {
+ return line.slice(0, index);
+ }
+ }
+ return line;
+}
+
+function stripJavaLikeComment(line) {
+ return String(line || "").replace(/\/\/.*$/, "").trimEnd();
+}
+
+function countBraces(line) {
+ const openBraces = (line.match(/\{/g) || []).length;
+ const closeBraces = (line.match(/\}/g) || []).length;
+ return openBraces - closeBraces;
+}
+
+function isComposerPackageName(name) {
+ return typeof name === "string"
+ && name.includes("/")
+ && !name.startsWith("ext-")
+ && !name.startsWith("lib-")
+ && name !== "php";
+}
+
+function matchXmlValue(block, tagName) {
+ const match = String(block || "").match(new RegExp(`<${tagName}>\\s*([^<]+)\\s*${tagName}>`, "i"));
+ return match ? match[1].trim() : "";
+}
+
+module.exports = {
+ normalizeSwiftIdentity,
+ parseBuildGradleManifest,
+ parseCargoTomlManifest,
+ parseChartManifest,
+ parseComposerManifest,
+ parseCsprojManifest,
+ parseGemfileManifest,
+ parseMixExsManifest,
+ parsePackageJsonManifest,
+ parsePackageSwiftManifest,
+ parsePomManifest,
+ parsePubspecManifest,
+ parsePyprojectManifest,
+ parseRequirementSpec,
+};
diff --git a/util/lockfileParsers/mavenParser.js b/util/lockfileParsers/mavenParser.js
new file mode 100644
index 0000000..173a006
--- /dev/null
+++ b/util/lockfileParsers/mavenParser.js
@@ -0,0 +1,181 @@
+// Copyright 2026 Cloudsmith Ltd. All rights reserved.
+const path = require("path");
+const {
+ buildTree,
+ createDependency,
+ deduplicateDeps,
+ flattenDependencies,
+ getSourceFileName,
+ getWorkspacePath,
+ pathExists,
+ readUtf8,
+} = require("./shared");
+const { parsePomManifest } = require("./manifestHelpers");
+
+const TREE_FILE_CANDIDATES = [
+ "dependency-tree.txt",
+ path.join("target", "dependency-tree.txt"),
+ path.join(".mvn", "dependency-tree.txt"),
+];
+
+const mavenParser = {
+ name: "mavenParser",
+ ecosystem: "maven",
+
+ async canResolve(workspaceFolder) {
+ return await pathExists(path.join(getWorkspacePath(workspaceFolder), "pom.xml"), workspaceFolder);
+ },
+
+ async detect(workspaceFolder) {
+ const rootPath = getWorkspacePath(workspaceFolder);
+ const pomPath = path.join(rootPath, "pom.xml");
+ if (!(await pathExists(pomPath, workspaceFolder))) {
+ return [];
+ }
+
+ let lockfilePath = null;
+ for (const candidate of TREE_FILE_CANDIDATES) {
+ const candidatePath = path.join(rootPath, candidate);
+ if (await pathExists(candidatePath, workspaceFolder)) {
+ lockfilePath = candidatePath;
+ break;
+ }
+ }
+
+ return [{
+ resolverName: this.name,
+ ecosystem: this.ecosystem,
+ lockfilePath,
+ manifestPath: pomPath,
+ sourceFile: "pom.xml",
+ }];
+ },
+
+ async resolve({ lockfilePath, manifestPath, workspaceFolder }) {
+ const directDependencies = parsePomManifest(await readUtf8(manifestPath, workspaceFolder))
+ .map((dependency) => createDependency({
+ name: dependency.name,
+ version: dependency.version,
+ ecosystem: "maven",
+ isDirect: true,
+ parent: null,
+ parentChain: [],
+ transitives: [],
+ sourceFile: getSourceFileName(manifestPath),
+ isDevelopmentDependency: dependency.isDevelopmentDependency,
+ }));
+
+ if (!lockfilePath) {
+ return buildTree("maven", getSourceFileName(manifestPath), directDependencies);
+ }
+
+ const treeRoots = parseDependencyTree(await readUtf8(lockfilePath, workspaceFolder));
+ const hydratedDirectDependencies = directDependencies.map((dependency) => {
+ const matchingTreeNode = treeRoots.find((node) => (
+ node.name === dependency.name
+ && (!dependency.version || node.version === dependency.version || !node.version)
+ )) || treeRoots.find((node) => node.name === dependency.name);
+
+ if (!matchingTreeNode) {
+ return dependency;
+ }
+
+ return {
+ ...dependency,
+ version: dependency.version || matchingTreeNode.version,
+ transitives: matchingTreeNode.children.map((child) => toMavenDependency(child, [dependency.name], getSourceFileName(manifestPath))),
+ };
+ });
+
+ let dependencies = deduplicateDeps(flattenDependencies(hydratedDirectDependencies));
+ for (const rootNode of treeRoots) {
+ appendTreeNodeIfMissing(rootNode, dependencies, getSourceFileName(manifestPath));
+ }
+
+ return buildTree("maven", getSourceFileName(manifestPath), dependencies);
+ },
+};
+
+function parseDependencyTree(content) {
+ const roots = [];
+ const stack = [];
+
+ for (const rawLine of String(content || "").split(/\r?\n/)) {
+ const body = rawLine.replace(/^\[INFO\]\s*/, "");
+ if (!body.trim()) {
+ continue;
+ }
+
+ const markerIndex = body.search(/[+\\]-/);
+ if (markerIndex === -1) {
+ continue;
+ }
+
+ const depth = Math.floor(markerIndex / 3);
+ const coordinates = body.slice(markerIndex + 2).trim().replace(/\s+\(\*\)$/, "");
+ const node = parseMavenCoordinate(coordinates);
+ if (!node) {
+ continue;
+ }
+
+ while (stack.length > depth) {
+ stack.pop();
+ }
+
+ if (stack.length === 0) {
+ roots.push(node);
+ } else {
+ stack[stack.length - 1].children.push(node);
+ }
+ stack.push(node);
+ }
+
+ return roots;
+}
+
+function parseMavenCoordinate(coordinates) {
+ const withoutScopeHint = coordinates.split(" -> ")[0].trim();
+ const parts = withoutScopeHint.split(":");
+ if (parts.length < 4) {
+ return null;
+ }
+
+ const resolvedVersion = coordinates.includes(" -> ")
+ ? coordinates.split(" -> ")[1].trim().split(" ")[0]
+ : parts[3];
+
+ return {
+ name: `${parts[0]}:${parts[1]}`,
+ version: resolvedVersion || "",
+ children: [],
+ };
+}
+
+function toMavenDependency(node, parentChain, sourceFile) {
+ return createDependency({
+ name: node.name,
+ version: node.version,
+ ecosystem: "maven",
+ isDirect: parentChain.length === 0,
+ parent: parentChain[parentChain.length - 1] || null,
+ parentChain,
+ transitives: deduplicateDeps(node.children.map((child) => toMavenDependency(child, parentChain.concat(node.name), sourceFile))),
+ sourceFile,
+ isDevelopmentDependency: false,
+ });
+}
+
+function appendTreeNodeIfMissing(node, dependencies, sourceFile) {
+ const key = `${node.name.toLowerCase()}@${node.version.toLowerCase()}`;
+ const exists = dependencies.some((dependency) => (
+ `${dependency.name.toLowerCase()}@${dependency.version.toLowerCase()}` === key
+ ));
+ if (!exists) {
+ dependencies.push(toMavenDependency(node, [], sourceFile));
+ }
+ for (const child of node.children) {
+ appendTreeNodeIfMissing(child, dependencies, sourceFile);
+ }
+}
+
+module.exports = mavenParser;
diff --git a/util/lockfileParsers/npmParser.js b/util/lockfileParsers/npmParser.js
new file mode 100644
index 0000000..8df4970
--- /dev/null
+++ b/util/lockfileParsers/npmParser.js
@@ -0,0 +1,836 @@
+// Copyright 2026 Cloudsmith Ltd. All rights reserved.
+const path = require("path");
+const {
+ buildTree,
+ createDependency,
+ deduplicateDeps,
+ flattenDependencies,
+ getSourceFileName,
+ getWorkspacePath,
+ normalizeVersion,
+ readJson,
+ pathExists,
+ readUtf8,
+ statSafe,
+ stripYamlComment,
+} = require("./shared");
+const { parsePackageJsonManifest } = require("./manifestHelpers");
+
+const LOCKFILE_NAMES = ["package-lock.json", "yarn.lock", "pnpm-lock.yaml"];
+
+const npmParser = {
+ name: "npmParser",
+ ecosystem: "npm",
+
+ async canResolve(workspaceFolder) {
+ const matches = await this.detect(workspaceFolder);
+ return matches.length > 0;
+ },
+
+ async detect(workspaceFolder) {
+ const rootPath = getWorkspacePath(workspaceFolder);
+ for (const fileName of LOCKFILE_NAMES) {
+ const lockfilePath = path.join(rootPath, fileName);
+ if (await pathExists(lockfilePath, workspaceFolder)) {
+ const manifestPath = await pathExists(path.join(rootPath, "package.json"), workspaceFolder)
+ ? path.join(rootPath, "package.json")
+ : null;
+ return [{
+ resolverName: this.name,
+ ecosystem: this.ecosystem,
+ lockfilePath,
+ manifestPath,
+ sourceFile: fileName,
+ }];
+ }
+ }
+ return [];
+ },
+
+ async resolve({ lockfilePath, manifestPath, workspaceFolder, options = {} }) {
+ const sourceFile = getSourceFileName(lockfilePath);
+ const manifest = manifestPath && await pathExists(manifestPath, workspaceFolder)
+ ? parsePackageJsonManifest(await readUtf8(manifestPath, workspaceFolder))
+ : { dependencies: [], directNames: new Set(), devNames: new Set() };
+
+ if (sourceFile === "package-lock.json") {
+ return parsePackageLock(lockfilePath, manifest, workspaceFolder, options);
+ }
+
+ if (sourceFile === "yarn.lock") {
+ return parseYarnLock(lockfilePath, manifest, workspaceFolder, options);
+ }
+
+ if (sourceFile === "pnpm-lock.yaml") {
+ return parsePnpmLock(lockfilePath, manifest, workspaceFolder, options);
+ }
+
+ throw new Error(`Unsupported npm lockfile: ${sourceFile}`);
+ },
+};
+
+async function parsePackageLock(lockfilePath, manifest, workspaceFolder, options) {
+ const warnings = [];
+ const stats = await statSafe(lockfilePath, workspaceFolder);
+ if (stats && stats.size > 50 * 1024 * 1024) {
+ warnings.push("Large package-lock.json detected. Parsing may take longer than usual.");
+ }
+
+ const root = await readJson(lockfilePath, workspaceFolder);
+ const packages = root && typeof root === "object" && root.packages && typeof root.packages === "object"
+ ? root.packages
+ : null;
+
+ if (!packages) {
+ throw new Error("Malformed package-lock.json: missing packages object");
+ }
+
+ const rootEntry = packages[""] || {};
+ const rootDependencyMap = {
+ ...(rootEntry.dependencies || {}),
+ ...(rootEntry.optionalDependencies || {}),
+ ...(rootEntry.devDependencies || {}),
+ };
+
+ const mergedManifestVersionHints = new Map();
+ for (const dependency of manifest.dependencies) {
+ mergedManifestVersionHints.set(dependency.name, dependency.version);
+ }
+ for (const [name, version] of Object.entries(rootDependencyMap)) {
+ if (!mergedManifestVersionHints.has(name)) {
+ mergedManifestVersionHints.set(name, normalizeVersion(version));
+ }
+ }
+
+ const uniqueEntries = new Map();
+ const nameIndex = new Map();
+ const nameIndexKeys = new Map();
+
+ for (const [packagePath, packageInfo] of Object.entries(packages)) {
+ if (packagePath === "" || !packageInfo || typeof packageInfo !== "object") {
+ continue;
+ }
+
+ const name = extractPackageLockName(packagePath);
+ const version = String(packageInfo.version || "").trim();
+ if (!name || !version) {
+ continue;
+ }
+
+ const key = `${name.toLowerCase()}@${version.toLowerCase()}`;
+ const existing = uniqueEntries.get(key);
+ const dependencies = {
+ ...(packageInfo.dependencies || {}),
+ ...(packageInfo.optionalDependencies || {}),
+ };
+ const merged = existing || {
+ key,
+ name,
+ version,
+ dependencies: {},
+ };
+ Object.assign(merged.dependencies, dependencies);
+ uniqueEntries.set(key, merged);
+
+ const existingByName = nameIndex.get(name) || [];
+ const existingKeys = nameIndexKeys.get(name) || new Set();
+ if (!existingKeys.has(key)) {
+ existingKeys.add(key);
+ existingByName.push(merged);
+ nameIndex.set(name, existingByName);
+ nameIndexKeys.set(name, existingKeys);
+ }
+ }
+
+ const directNames = manifest.directNames.size > 0 || manifest.devNames.size > 0
+ ? new Set([...manifest.directNames, ...manifest.devNames])
+ : new Set(Object.keys(rootDependencyMap));
+
+ const directRoots = [];
+ const seenDirectKeys = new Set();
+
+ for (const directName of directNames) {
+ const entry = selectEntryByName(nameIndex, directName, mergedManifestVersionHints.get(directName));
+ const dependency = buildNpmDependency(entry, directName, [], nameIndex, new Set(), {
+ sourceFile: getSourceFileName(lockfilePath),
+ directNames,
+ devNames: manifest.devNames,
+ });
+ const key = `${dependency.name.toLowerCase()}@${dependency.version.toLowerCase()}`;
+ if (!seenDirectKeys.has(key)) {
+ seenDirectKeys.add(key);
+ directRoots.push(dependency);
+ }
+ }
+
+ let dependencies = deduplicateDeps(flattenDependencies(directRoots));
+ const addedKeys = new Set();
+ collectDependencyKeys(dependencies, addedKeys);
+ for (const entry of uniqueEntries.values()) {
+ const key = `${entry.name.toLowerCase()}@${entry.version.toLowerCase()}`;
+ if (addedKeys.has(key)) {
+ continue;
+ }
+ addedKeys.add(key);
+ dependencies.push(createDependency({
+ name: entry.name,
+ version: entry.version,
+ ecosystem: "npm",
+ isDirect: false,
+ parent: null,
+ parentChain: [],
+ transitives: [],
+ sourceFile: getSourceFileName(lockfilePath),
+ isDevelopmentDependency: manifest.devNames.has(entry.name),
+ }));
+ }
+
+ if (options.maxDependenciesToScan && dependencies.length > options.maxDependenciesToScan) {
+ warnings.push(
+ `Large npm dependency tree (${dependencies.length} unique packages). ` +
+ `Display is capped at ${options.maxDependenciesToScan} dependencies.`
+ );
+ }
+
+ return buildTree("npm", getSourceFileName(lockfilePath), dependencies, warnings);
+}
+
+function collectDependencyKeys(dependencies, addedKeys) {
+ for (const dependency of Array.isArray(dependencies) ? dependencies : []) {
+ addedKeys.add(`${dependency.name.toLowerCase()}@${dependency.version.toLowerCase()}`);
+ if (Array.isArray(dependency.transitives) && dependency.transitives.length > 0) {
+ collectDependencyKeys(dependency.transitives, addedKeys);
+ }
+ }
+}
+
+function buildNpmDependency(entry, fallbackName, parentChain, nameIndex, visiting, context) {
+ if (!entry) {
+ return createDependency({
+ name: fallbackName,
+ version: "",
+ ecosystem: "npm",
+ isDirect: parentChain.length === 0,
+ parent: parentChain[parentChain.length - 1] || null,
+ parentChain,
+ transitives: [],
+ sourceFile: context.sourceFile,
+ isDevelopmentDependency: context.devNames.has(fallbackName),
+ });
+ }
+
+ if (visiting.has(entry.key)) {
+ return createDependency({
+ name: entry.name,
+ version: entry.version,
+ ecosystem: "npm",
+ isDirect: parentChain.length === 0,
+ parent: parentChain[parentChain.length - 1] || null,
+ parentChain,
+ transitives: [],
+ sourceFile: context.sourceFile,
+ isDevelopmentDependency: context.devNames.has(entry.name),
+ });
+ }
+
+ const nextVisiting = new Set(visiting);
+ nextVisiting.add(entry.key);
+ const nextParentChain = parentChain.concat(entry.name);
+ const transitives = [];
+
+ for (const [dependencyName, versionHint] of Object.entries(entry.dependencies || {})) {
+ const childEntry = selectEntryByName(nameIndex, dependencyName, normalizeVersion(versionHint));
+ if (childEntry && nextVisiting.has(childEntry.key)) {
+ continue;
+ }
+ transitives.push(buildNpmDependency(
+ childEntry,
+ dependencyName,
+ nextParentChain,
+ nameIndex,
+ nextVisiting,
+ context
+ ));
+ }
+
+ return createDependency({
+ name: entry.name,
+ version: entry.version,
+ ecosystem: "npm",
+ isDirect: parentChain.length === 0 || context.directNames.has(entry.name),
+ parent: parentChain[parentChain.length - 1] || null,
+ parentChain,
+ transitives: deduplicateDeps(transitives),
+ sourceFile: context.sourceFile,
+ isDevelopmentDependency: context.devNames.has(entry.name),
+ });
+}
+
+function selectEntryByName(nameIndex, dependencyName, versionHint) {
+ const entries = nameIndex.get(dependencyName) || [];
+ if (entries.length === 0) {
+ return null;
+ }
+
+ const normalizedHint = normalizeVersion(versionHint);
+ if (normalizedHint) {
+ const exactMatch = entries.find((entry) => entry.version === normalizedHint);
+ if (exactMatch) {
+ return exactMatch;
+ }
+ }
+
+ return entries[0];
+}
+
+function extractPackageLockName(packagePath) {
+ const marker = "node_modules/";
+ const lastMarkerIndex = packagePath.lastIndexOf(marker);
+ const relativePath = lastMarkerIndex === -1
+ ? packagePath
+ : packagePath.slice(lastMarkerIndex + marker.length);
+ const segments = relativePath.split("/").filter(Boolean);
+ if (segments.length === 0) {
+ return "";
+ }
+ if (segments[0].startsWith("@") && segments.length >= 2) {
+ return `${segments[0]}/${segments[1]}`;
+ }
+ return segments[0];
+}
+
+async function parseYarnLock(lockfilePath, manifest, workspaceFolder, options) {
+ const content = await readUtf8(lockfilePath, workspaceFolder);
+ const parsed = parseYarnEntries(content);
+ if (parsed.entries.size === 0) {
+ throw new Error("Malformed yarn.lock: no package entries found");
+ }
+
+ const sourceFile = getSourceFileName(lockfilePath);
+ const manifestVersionHints = new Map();
+ for (const dependency of manifest.dependencies) {
+ manifestVersionHints.set(dependency.name, dependency.version);
+ }
+ const directNames = manifest.directNames.size > 0 || manifest.devNames.size > 0
+ ? new Set([...manifest.directNames, ...manifest.devNames])
+ : new Set([...parsed.entries.values()].map((entry) => entry.name));
+
+ const directRoots = [];
+ for (const directName of directNames) {
+ const entry = selectYarnEntry(parsed, directName, manifestVersionHints.get(directName));
+ directRoots.push(buildYarnDependency(
+ entry,
+ directName,
+ [],
+ parsed,
+ new Set(),
+ sourceFile,
+ manifest.devNames
+ ));
+ }
+
+ let dependencies = deduplicateDeps(flattenDependencies(directRoots));
+ for (const entry of parsed.entries.values()) {
+ const key = `${entry.name.toLowerCase()}@${entry.version.toLowerCase()}`;
+ if (dependencies.some((dependency) => `${dependency.name.toLowerCase()}@${dependency.version.toLowerCase()}` === key)) {
+ continue;
+ }
+ dependencies.push(createDependency({
+ name: entry.name,
+ version: entry.version,
+ ecosystem: "npm",
+ isDirect: false,
+ parent: null,
+ parentChain: [],
+ transitives: [],
+ sourceFile,
+ isDevelopmentDependency: manifest.devNames.has(entry.name),
+ }));
+ }
+
+ const warnings = [];
+ if (options.maxDependenciesToScan && dependencies.length > options.maxDependenciesToScan) {
+ warnings.push(
+ `Large npm dependency tree (${dependencies.length} unique packages). ` +
+ `Display is capped at ${options.maxDependenciesToScan} dependencies.`
+ );
+ }
+
+ return buildTree("npm", sourceFile, dependencies, warnings);
+}
+
+function buildYarnDependency(entry, fallbackName, parentChain, parsedEntries, visiting, sourceFile, devNames) {
+ if (!entry) {
+ return createDependency({
+ name: fallbackName,
+ version: "",
+ ecosystem: "npm",
+ isDirect: parentChain.length === 0,
+ parent: parentChain[parentChain.length - 1] || null,
+ parentChain,
+ transitives: [],
+ sourceFile,
+ isDevelopmentDependency: devNames.has(fallbackName),
+ });
+ }
+
+ const key = `${entry.name.toLowerCase()}@${entry.version.toLowerCase()}`;
+ if (visiting.has(key)) {
+ return createDependency({
+ name: entry.name,
+ version: entry.version,
+ ecosystem: "npm",
+ isDirect: parentChain.length === 0,
+ parent: parentChain[parentChain.length - 1] || null,
+ parentChain,
+ transitives: [],
+ sourceFile,
+ isDevelopmentDependency: devNames.has(entry.name),
+ });
+ }
+
+ const nextVisiting = new Set(visiting);
+ nextVisiting.add(key);
+ const nextParentChain = parentChain.concat(entry.name);
+ const transitives = [];
+
+ for (const dependencyName of Object.keys(entry.dependencies || {})) {
+ const versionHint = entry.dependencies[dependencyName];
+ transitives.push(buildYarnDependency(
+ selectYarnEntry(parsedEntries, dependencyName, versionHint),
+ dependencyName,
+ nextParentChain,
+ parsedEntries,
+ nextVisiting,
+ sourceFile,
+ devNames
+ ));
+ }
+
+ return createDependency({
+ name: entry.name,
+ version: entry.version,
+ ecosystem: "npm",
+ isDirect: parentChain.length === 0,
+ parent: parentChain[parentChain.length - 1] || null,
+ parentChain,
+ transitives: deduplicateDeps(transitives),
+ sourceFile,
+ isDevelopmentDependency: devNames.has(entry.name),
+ });
+}
+
+function parseYarnEntries(content) {
+ const entries = new Map();
+ const entriesByName = new Map();
+ const selectorIndex = new Map();
+ const lines = String(content || "").split(/\r?\n/);
+ let currentEntry = null;
+ let inDependencies = false;
+
+ const flushCurrent = () => {
+ if (currentEntry && currentEntry.name && currentEntry.version) {
+ const key = `${currentEntry.name.toLowerCase()}@${currentEntry.version.toLowerCase()}`;
+ const existing = entries.get(key);
+ if (existing) {
+ Object.assign(existing.dependencies, currentEntry.dependencies);
+ for (const selector of currentEntry.selectors || []) {
+ if (!existing.selectors.includes(selector)) {
+ existing.selectors.push(selector);
+ }
+ selectorIndex.set(selector, key);
+ }
+ } else {
+ currentEntry.key = key;
+ entries.set(key, currentEntry);
+ if (!entriesByName.has(currentEntry.name)) {
+ entriesByName.set(currentEntry.name, []);
+ }
+ entriesByName.get(currentEntry.name).push(currentEntry);
+ for (const selector of currentEntry.selectors || []) {
+ selectorIndex.set(selector, key);
+ }
+ }
+ }
+ currentEntry = null;
+ inDependencies = false;
+ };
+
+ for (const rawLine of lines) {
+ const line = rawLine.replace(/\r$/, "");
+ const trimmed = line.trim();
+ if (!trimmed) {
+ flushCurrent();
+ continue;
+ }
+ if (trimmed === "__metadata:") {
+ flushCurrent();
+ continue;
+ }
+
+ if (!line.startsWith(" ")) {
+ flushCurrent();
+ const header = trimmed.replace(/:$/, "");
+ const selectors = header.split(",").map((selector) => selector.trim().replace(/^["']|["']$/g, ""));
+ const primarySelector = selectors[0] || "";
+ const name = parseYarnSelectorName(primarySelector);
+ if (!name) {
+ continue;
+ }
+ currentEntry = {
+ name,
+ version: "",
+ dependencies: {},
+ selectors,
+ };
+ continue;
+ }
+
+ if (!currentEntry) {
+ continue;
+ }
+
+ if (trimmed === "dependencies:") {
+ inDependencies = true;
+ continue;
+ }
+
+ const versionMatch = trimmed.match(/^version\s+"([^"]+)"/);
+ if (versionMatch) {
+ currentEntry.version = versionMatch[1];
+ inDependencies = false;
+ continue;
+ }
+
+ if (inDependencies) {
+ const dependencyMatch = trimmed.match(/^("?[^"\s]+"?)\s+"([^"]+)"/);
+ if (!dependencyMatch) {
+ continue;
+ }
+ const dependencyName = dependencyMatch[1].replace(/^["']|["']$/g, "");
+ currentEntry.dependencies[dependencyName] = dependencyMatch[2];
+ }
+ }
+
+ flushCurrent();
+ return {
+ entries,
+ entriesByName,
+ selectorIndex,
+ };
+}
+
+function selectYarnEntry(parsedEntries, dependencyName, versionHint) {
+ if (!parsedEntries || !dependencyName) {
+ return null;
+ }
+
+ const normalizedName = String(dependencyName || "").trim();
+ if (!normalizedName) {
+ return null;
+ }
+
+ const normalizedHint = String(versionHint || "").trim().replace(/^["']|["']$/g, "");
+ if (normalizedHint) {
+ const exactSelectorKey = `${normalizedName}@${normalizedHint}`;
+ const selectedKey = parsedEntries.selectorIndex.get(exactSelectorKey);
+ if (selectedKey && parsedEntries.entries.has(selectedKey)) {
+ return parsedEntries.entries.get(selectedKey);
+ }
+ }
+
+ const candidates = parsedEntries.entriesByName.get(normalizedName) || [];
+ if (candidates.length === 0) {
+ return null;
+ }
+
+ if (normalizedHint) {
+ const exactVersionMatch = candidates.find((entry) => entry.version === normalizedHint);
+ if (exactVersionMatch) {
+ return exactVersionMatch;
+ }
+ }
+
+ return candidates[0];
+}
+
+function parseYarnSelectorName(selector) {
+ const normalizedSelector = selector.trim().replace(/^["']|["']$/g, "");
+ if (!normalizedSelector) {
+ return "";
+ }
+
+ if (normalizedSelector.startsWith("@")) {
+ const secondAt = normalizedSelector.indexOf("@", 1);
+ return secondAt === -1 ? normalizedSelector : normalizedSelector.slice(0, secondAt);
+ }
+
+ const atIndex = normalizedSelector.indexOf("@");
+ return atIndex === -1 ? normalizedSelector : normalizedSelector.slice(0, atIndex);
+}
+
+async function parsePnpmLock(lockfilePath, manifest, workspaceFolder, options) {
+ const parsed = parsePnpmEntries(await readUtf8(lockfilePath, workspaceFolder));
+ if (parsed.packageEntries.size === 0) {
+ throw new Error("Malformed pnpm-lock.yaml: no package entries found");
+ }
+
+ const sourceFile = getSourceFileName(lockfilePath);
+ const directNames = manifest.directNames.size > 0 || manifest.devNames.size > 0
+ ? new Set([...manifest.directNames, ...manifest.devNames])
+ : new Set(parsed.directVersions.keys());
+ const directRoots = [];
+
+ for (const directName of directNames) {
+ const entry = selectPnpmEntry(parsed.packageEntries, directName, parsed.directVersions.get(directName));
+ directRoots.push(buildPnpmDependency(
+ entry,
+ directName,
+ [],
+ parsed.packageEntries,
+ new Set(),
+ sourceFile,
+ manifest.devNames
+ ));
+ }
+
+ let dependencies = deduplicateDeps(flattenDependencies(directRoots));
+ for (const entry of parsed.packageEntries.values()) {
+ const key = `${entry.name.toLowerCase()}@${entry.version.toLowerCase()}`;
+ if (dependencies.some((dependency) => `${dependency.name.toLowerCase()}@${dependency.version.toLowerCase()}` === key)) {
+ continue;
+ }
+ dependencies.push(createDependency({
+ name: entry.name,
+ version: entry.version,
+ ecosystem: "npm",
+ isDirect: false,
+ parent: null,
+ parentChain: [],
+ transitives: [],
+ sourceFile,
+ isDevelopmentDependency: manifest.devNames.has(entry.name),
+ }));
+ }
+
+ const warnings = [];
+ if (options.maxDependenciesToScan && dependencies.length > options.maxDependenciesToScan) {
+ warnings.push(
+ `Large npm dependency tree (${dependencies.length} unique packages). ` +
+ `Display is capped at ${options.maxDependenciesToScan} dependencies.`
+ );
+ }
+
+ return buildTree("npm", sourceFile, dependencies, warnings);
+}
+
+function buildPnpmDependency(entry, fallbackName, parentChain, packageEntries, visiting, sourceFile, devNames) {
+ if (!entry) {
+ return createDependency({
+ name: fallbackName,
+ version: "",
+ ecosystem: "npm",
+ isDirect: parentChain.length === 0,
+ parent: parentChain[parentChain.length - 1] || null,
+ parentChain,
+ transitives: [],
+ sourceFile,
+ isDevelopmentDependency: devNames.has(fallbackName),
+ });
+ }
+
+ const key = `${entry.name.toLowerCase()}@${entry.version.toLowerCase()}`;
+ if (visiting.has(key)) {
+ return createDependency({
+ name: entry.name,
+ version: entry.version,
+ ecosystem: "npm",
+ isDirect: parentChain.length === 0,
+ parent: parentChain[parentChain.length - 1] || null,
+ parentChain,
+ transitives: [],
+ sourceFile,
+ isDevelopmentDependency: devNames.has(entry.name),
+ });
+ }
+
+ const nextVisiting = new Set(visiting);
+ nextVisiting.add(key);
+ const nextParentChain = parentChain.concat(entry.name);
+ const transitives = [];
+
+ for (const [dependencyName, versionHint] of Object.entries(entry.dependencies || {})) {
+ transitives.push(buildPnpmDependency(
+ selectPnpmEntry(packageEntries, dependencyName, versionHint),
+ dependencyName,
+ nextParentChain,
+ packageEntries,
+ nextVisiting,
+ sourceFile,
+ devNames
+ ));
+ }
+
+ return createDependency({
+ name: entry.name,
+ version: entry.version,
+ ecosystem: "npm",
+ isDirect: parentChain.length === 0,
+ parent: parentChain[parentChain.length - 1] || null,
+ parentChain,
+ transitives: deduplicateDeps(transitives),
+ sourceFile,
+ isDevelopmentDependency: devNames.has(entry.name),
+ });
+}
+
+function selectPnpmEntry(packageEntries, dependencyName, versionHint) {
+ const normalizedHint = normalizeVersion(versionHint).split("(")[0].trim();
+ const entries = [...packageEntries.values()].filter((entry) => entry.name === dependencyName);
+ if (entries.length === 0) {
+ return null;
+ }
+ if (normalizedHint) {
+ const exactMatch = entries.find((entry) => entry.version === normalizedHint);
+ if (exactMatch) {
+ return exactMatch;
+ }
+ }
+ return entries[0];
+}
+
+function parsePnpmEntries(content) {
+ const packageEntries = new Map();
+ const directVersions = new Map();
+ const lines = String(content || "").split(/\r?\n/);
+ let section = "";
+ let currentPackage = null;
+ let currentPackageSubsection = "";
+ let inImporter = false;
+ let importerSection = "";
+ let currentImporterPackage = "";
+
+ const flushPackage = () => {
+ if (!currentPackage || !currentPackage.name || !currentPackage.version) {
+ currentPackage = null;
+ currentPackageSubsection = "";
+ return;
+ }
+ packageEntries.set(`${currentPackage.name.toLowerCase()}@${currentPackage.version.toLowerCase()}`, currentPackage);
+ currentPackage = null;
+ currentPackageSubsection = "";
+ };
+
+ for (const rawLine of lines) {
+ const lineWithoutComment = stripYamlComment(rawLine);
+ const line = lineWithoutComment.trimEnd();
+ const trimmed = line.trim();
+ if (!trimmed) {
+ continue;
+ }
+
+ const indent = rawLine.search(/\S/);
+ if (indent === 0 && trimmed === "importers:") {
+ flushPackage();
+ section = "importers";
+ inImporter = false;
+ continue;
+ }
+ if (indent === 0 && trimmed === "packages:") {
+ flushPackage();
+ section = "packages";
+ continue;
+ }
+ if (indent === 0 && trimmed.endsWith(":") && !["importers:", "packages:"].includes(trimmed)) {
+ flushPackage();
+ section = "";
+ continue;
+ }
+
+ if (section === "importers") {
+ if (indent === 2 && trimmed.endsWith(":")) {
+ inImporter = true;
+ importerSection = "";
+ currentImporterPackage = "";
+ continue;
+ }
+ if (!inImporter) {
+ continue;
+ }
+ if (indent === 4 && trimmed.endsWith(":")) {
+ importerSection = trimmed.slice(0, -1);
+ currentImporterPackage = "";
+ continue;
+ }
+ if (!["dependencies", "devDependencies", "optionalDependencies"].includes(importerSection)) {
+ continue;
+ }
+ if (indent === 6 && trimmed.endsWith(":")) {
+ currentImporterPackage = trimmed.slice(0, -1).replace(/^["']|["']$/g, "");
+ continue;
+ }
+ if (indent === 8 && trimmed.startsWith("version:") && currentImporterPackage) {
+ directVersions.set(
+ currentImporterPackage,
+ normalizeVersion(trimmed.slice("version:".length).trim()).split("(")[0].trim()
+ );
+ }
+ continue;
+ }
+
+ if (section === "packages") {
+ if (indent === 2 && trimmed.endsWith(":")) {
+ flushPackage();
+ const parsedKey = parsePnpmPackageKey(trimmed.slice(0, -1));
+ if (!parsedKey) {
+ continue;
+ }
+ currentPackage = {
+ ...parsedKey,
+ dependencies: {},
+ };
+ continue;
+ }
+ if (!currentPackage) {
+ continue;
+ }
+ if (indent === 4 && trimmed.endsWith(":")) {
+ currentPackageSubsection = trimmed.slice(0, -1);
+ continue;
+ }
+ if (!["dependencies", "optionalDependencies"].includes(currentPackageSubsection)) {
+ continue;
+ }
+ if (indent === 6 && trimmed.includes(":")) {
+ const parts = trimmed.split(":", 2);
+ currentPackage.dependencies[parts[0].trim()] = normalizeVersion(parts[1].trim()).split("(")[0].trim();
+ }
+ }
+ }
+
+ flushPackage();
+ return {
+ packageEntries,
+ directVersions,
+ };
+}
+
+function parsePnpmPackageKey(rawKey) {
+ const cleaned = rawKey.replace(/^\/+/, "").trim().replace(/^["']|["']$/g, "");
+ if (!cleaned) {
+ return null;
+ }
+
+ const withoutPeerSuffix = cleaned.split("(")[0];
+ const atIndex = withoutPeerSuffix.lastIndexOf("@");
+ if (atIndex <= 0) {
+ return null;
+ }
+
+ return {
+ name: withoutPeerSuffix.slice(0, atIndex),
+ version: withoutPeerSuffix.slice(atIndex + 1),
+ };
+}
+
+module.exports = npmParser;
diff --git a/util/lockfileParsers/nugetParser.js b/util/lockfileParsers/nugetParser.js
new file mode 100644
index 0000000..95f8c7c
--- /dev/null
+++ b/util/lockfileParsers/nugetParser.js
@@ -0,0 +1,190 @@
+// Copyright 2026 Cloudsmith Ltd. All rights reserved.
+const fs = require("fs");
+const path = require("path");
+const {
+ buildTree,
+ createDependency,
+ deduplicateDeps,
+ flattenDependencies,
+ getSourceFileName,
+ getWorkspacePath,
+ pathExists,
+ readJson,
+ readUtf8,
+ resolveWorkspaceFilePath,
+} = require("./shared");
+const { parseCsprojManifest } = require("./manifestHelpers");
+
+const nugetParser = {
+ name: "nugetParser",
+ ecosystem: "nuget",
+
+ async canResolve(workspaceFolder) {
+ const matches = await this.detect(workspaceFolder);
+ return matches.length > 0;
+ },
+
+ async detect(workspaceFolder) {
+ const rootPath = getWorkspacePath(workspaceFolder);
+ const safeRootPath = await resolveWorkspaceFilePath(rootPath, workspaceFolder);
+ if (!safeRootPath) {
+ return [];
+ }
+ const entries = await fs.promises.readdir(safeRootPath);
+ const csprojPath = entries.find((entry) => entry.toLowerCase().endsWith(".csproj"));
+ const lockfilePath = await pathExists(path.join(safeRootPath, "packages.lock.json"), workspaceFolder)
+ ? path.join(safeRootPath, "packages.lock.json")
+ : null;
+ const manifestPath = csprojPath ? path.join(safeRootPath, csprojPath) : null;
+
+ if (!lockfilePath && !manifestPath) {
+ return [];
+ }
+
+ return [{
+ resolverName: this.name,
+ ecosystem: this.ecosystem,
+ lockfilePath,
+ manifestPath,
+ sourceFile: getSourceFileName(lockfilePath || manifestPath),
+ }];
+ },
+
+ async resolve({ lockfilePath, manifestPath, workspaceFolder }) {
+ const sourceFile = getSourceFileName(lockfilePath || manifestPath);
+ const manifestDependencies = manifestPath && await pathExists(manifestPath, workspaceFolder)
+ ? parseCsprojManifest(await readUtf8(manifestPath, workspaceFolder))
+ : [];
+ const directNames = new Set(manifestDependencies.map((dependency) => dependency.name.toLowerCase()));
+
+ if (!lockfilePath) {
+ return buildTree("nuget", sourceFile, manifestDependencies.map((dependency) => createDependency({
+ name: dependency.name,
+ version: dependency.version,
+ ecosystem: "nuget",
+ isDirect: true,
+ parent: null,
+ parentChain: [],
+ transitives: [],
+ sourceFile,
+ isDevelopmentDependency: dependency.isDevelopmentDependency,
+ })));
+ }
+
+ const root = await readJson(lockfilePath, workspaceFolder);
+ const dependencyRoot = root && root.dependencies && typeof root.dependencies === "object"
+ ? root.dependencies
+ : null;
+ if (!dependencyRoot) {
+ throw new Error("Malformed packages.lock.json: missing dependencies object");
+ }
+
+ const recordsByName = new Map();
+ for (const frameworkDependencies of Object.values(dependencyRoot)) {
+ if (!frameworkDependencies || typeof frameworkDependencies !== "object") {
+ continue;
+ }
+ for (const [name, details] of Object.entries(frameworkDependencies)) {
+ const dependencies = details && details.dependencies && typeof details.dependencies === "object"
+ ? Object.keys(details.dependencies)
+ : [];
+ const existing = recordsByName.get(name.toLowerCase());
+ recordsByName.set(name.toLowerCase(), {
+ name,
+ version: details.resolved || "",
+ dependencies: deduplicateStringValues([...(existing ? existing.dependencies : []), ...dependencies]),
+ isDirect: Boolean(existing && existing.isDirect) || String(details.type || "").toLowerCase() === "direct",
+ });
+ }
+ }
+
+ const rootRecords = manifestDependencies.length > 0
+ ? manifestDependencies.map((dependency) => recordsByName.get(dependency.name.toLowerCase())).filter(Boolean)
+ : [...recordsByName.values()].filter((record) => record.isDirect);
+
+ const directRoots = deduplicateDeps(rootRecords.map((record) => buildNugetDependency(
+ record,
+ [],
+ recordsByName,
+ new Set(),
+ sourceFile,
+ directNames
+ )));
+ let dependencies = deduplicateDeps(flattenDependencies(directRoots));
+
+ for (const record of recordsByName.values()) {
+ const key = `${record.name.toLowerCase()}@${record.version.toLowerCase()}`;
+ if (dependencies.some((dependency) => `${dependency.name.toLowerCase()}@${dependency.version.toLowerCase()}` === key)) {
+ continue;
+ }
+ dependencies.push(createDependency({
+ name: record.name,
+ version: record.version,
+ ecosystem: "nuget",
+ isDirect: directNames.has(record.name.toLowerCase()) || record.isDirect,
+ parent: null,
+ parentChain: [],
+ transitives: [],
+ sourceFile,
+ isDevelopmentDependency: false,
+ }));
+ }
+
+ return buildTree("nuget", sourceFile, dependencies);
+ },
+};
+
+function buildNugetDependency(record, parentChain, recordsByName, visiting, sourceFile, directNames) {
+ const key = `${record.name.toLowerCase()}@${record.version.toLowerCase()}`;
+ if (visiting.has(key)) {
+ return createDependency({
+ name: record.name,
+ version: record.version,
+ ecosystem: "nuget",
+ isDirect: parentChain.length === 0 || directNames.has(record.name.toLowerCase()) || record.isDirect,
+ parent: parentChain[parentChain.length - 1] || null,
+ parentChain,
+ transitives: [],
+ sourceFile,
+ isDevelopmentDependency: false,
+ });
+ }
+
+ const nextVisiting = new Set(visiting);
+ nextVisiting.add(key);
+ const nextParentChain = parentChain.concat(record.name);
+ const transitives = [];
+
+ for (const dependencyName of record.dependencies) {
+ const childRecord = recordsByName.get(dependencyName.toLowerCase());
+ if (!childRecord) {
+ continue;
+ }
+ transitives.push(buildNugetDependency(
+ childRecord,
+ nextParentChain,
+ recordsByName,
+ nextVisiting,
+ sourceFile,
+ directNames
+ ));
+ }
+
+ return createDependency({
+ name: record.name,
+ version: record.version,
+ ecosystem: "nuget",
+ isDirect: parentChain.length === 0 || directNames.has(record.name.toLowerCase()) || record.isDirect,
+ parent: parentChain[parentChain.length - 1] || null,
+ parentChain,
+ transitives: deduplicateDeps(transitives),
+ sourceFile,
+ isDevelopmentDependency: false,
+ });
+}
+
+function deduplicateStringValues(values) {
+ return [...new Set(values.filter(Boolean))];
+}
+
+module.exports = nugetParser;
diff --git a/util/lockfileParsers/pythonParser.js b/util/lockfileParsers/pythonParser.js
new file mode 100644
index 0000000..80decf9
--- /dev/null
+++ b/util/lockfileParsers/pythonParser.js
@@ -0,0 +1,383 @@
+// Copyright 2026 Cloudsmith Ltd. All rights reserved.
+const path = require("path");
+const {
+ buildTree,
+ createDependency,
+ deduplicateDeps,
+ flattenDependencies,
+ getSourceFileName,
+ getWorkspacePath,
+ normalizeVersion,
+ parseInlineTomlValue,
+ parseKeyValueLine,
+ readJson,
+ parseQuotedArray,
+ pathExists,
+ readUtf8,
+ stripTomlComment,
+} = require("./shared");
+const {
+ parsePyprojectManifest,
+ parseRequirementSpec,
+} = require("./manifestHelpers");
+const { normalizePackageName } = require("../packageNameNormalizer");
+
+const SOURCE_PRIORITY = ["uv.lock", "poetry.lock", "Pipfile.lock", "requirements.txt"];
+
+const pythonParser = {
+ name: "pythonParser",
+ ecosystem: "python",
+
+ async canResolve(workspaceFolder) {
+ const matches = await this.detect(workspaceFolder);
+ return matches.length > 0;
+ },
+
+ async detect(workspaceFolder) {
+ const rootPath = getWorkspacePath(workspaceFolder);
+ for (const fileName of SOURCE_PRIORITY) {
+ const lockfilePath = path.join(rootPath, fileName);
+ if (await pathExists(lockfilePath, workspaceFolder)) {
+ const pyprojectPath = path.join(rootPath, "pyproject.toml");
+ return [{
+ resolverName: this.name,
+ ecosystem: this.ecosystem,
+ lockfilePath,
+ manifestPath: await pathExists(pyprojectPath, workspaceFolder) ? pyprojectPath : null,
+ sourceFile: fileName,
+ }];
+ }
+ }
+ return [];
+ },
+
+ async resolve({ lockfilePath, manifestPath, workspaceFolder }) {
+ const sourceFile = getSourceFileName(lockfilePath);
+ const pyproject = manifestPath && await pathExists(manifestPath, workspaceFolder)
+ ? parsePyprojectManifest(await readUtf8(manifestPath, workspaceFolder))
+ : { projectName: "", dependencies: [], directNames: new Set(), devNames: new Set() };
+
+ if (sourceFile === "requirements.txt") {
+ return parseRequirements(lockfilePath, workspaceFolder);
+ }
+ if (sourceFile === "Pipfile.lock") {
+ return parsePipfile(lockfilePath, workspaceFolder);
+ }
+ if (sourceFile === "poetry.lock" || sourceFile === "uv.lock") {
+ return parseTomlLock(lockfilePath, pyproject, workspaceFolder, sourceFile === "uv.lock");
+ }
+
+ throw new Error(`Unsupported Python dependency source: ${sourceFile}`);
+ },
+};
+
+async function parseRequirements(lockfilePath, workspaceFolder) {
+ const dependencies = [];
+
+ for (const rawLine of String(await readUtf8(lockfilePath, workspaceFolder)).split(/\r?\n/)) {
+ const line = rawLine.trim();
+ if (!line || line.startsWith("#") || line.startsWith("-")) {
+ continue;
+ }
+ const parsed = parseRequirementSpec(line);
+ if (!parsed) {
+ throw new Error(`Malformed requirements.txt entry: ${line}`);
+ }
+ dependencies.push(createDependency({
+ name: parsed.name,
+ version: parsed.version,
+ ecosystem: "python",
+ isDirect: true,
+ parent: null,
+ parentChain: [],
+ transitives: [],
+ sourceFile: getSourceFileName(lockfilePath),
+ isDevelopmentDependency: false,
+ }));
+ }
+
+ return buildTree("python", getSourceFileName(lockfilePath), dependencies, [
+ "requirements.txt does not encode transitive dependencies. Showing direct requirements only.",
+ ]);
+}
+
+async function parsePipfile(lockfilePath, workspaceFolder) {
+ const root = await readJson(lockfilePath, workspaceFolder);
+ const dependencies = [];
+
+ for (const [name, details] of Object.entries(root.default || {})) {
+ dependencies.push(createDependency({
+ name,
+ version: normalizeVersion(details && details.version),
+ ecosystem: "python",
+ isDirect: true,
+ parent: null,
+ parentChain: [],
+ transitives: [],
+ sourceFile: getSourceFileName(lockfilePath),
+ isDevelopmentDependency: false,
+ }));
+ }
+
+ for (const [name, details] of Object.entries(root.develop || {})) {
+ dependencies.push(createDependency({
+ name,
+ version: normalizeVersion(details && details.version),
+ ecosystem: "python",
+ isDirect: true,
+ parent: null,
+ parentChain: [],
+ transitives: [],
+ sourceFile: getSourceFileName(lockfilePath),
+ isDevelopmentDependency: true,
+ }));
+ }
+
+ return buildTree("python", getSourceFileName(lockfilePath), deduplicateDeps(dependencies));
+}
+
+async function parseTomlLock(lockfilePath, pyproject, workspaceFolder, skipEditableRoot) {
+ const records = parsePythonPackageRecords(
+ await readUtf8(lockfilePath, workspaceFolder),
+ skipEditableRoot
+ );
+ if (records.length === 0) {
+ throw new Error(`Malformed ${getSourceFileName(lockfilePath)}: no package entries found`);
+ }
+
+ const sourceFile = getSourceFileName(lockfilePath);
+ const normalizedDirectNames = pyproject.directNames.size > 0 || pyproject.devNames.size > 0
+ ? new Set(
+ [...pyproject.directNames, ...pyproject.devNames]
+ .map((name) => normalizePackageName(name, "python"))
+ )
+ : new Set(records.filter((record) => record.isRootDependency).map((record) => record.normalizedName));
+
+ const recordsByName = new Map();
+ const incomingCounts = new Map();
+ for (const record of records) {
+ if (!recordsByName.has(record.normalizedName)) {
+ recordsByName.set(record.normalizedName, []);
+ }
+ recordsByName.get(record.normalizedName).push(record);
+ for (const dependencyName of record.dependencies) {
+ const normalizedDependencyName = normalizePackageName(dependencyName, "python");
+ incomingCounts.set(
+ normalizedDependencyName,
+ (incomingCounts.get(normalizedDependencyName) || 0) + 1
+ );
+ }
+ }
+
+ const rootRecords = normalizedDirectNames.size > 0
+ ? [...normalizedDirectNames].map((name) => (recordsByName.get(name) || [])[0]).filter(Boolean)
+ : records.filter((record) => !incomingCounts.get(record.normalizedName));
+
+ const directRoots = deduplicateDeps(rootRecords.map((record) => buildPythonDependency(
+ record,
+ [],
+ recordsByName,
+ new Set(),
+ sourceFile,
+ new Set([...pyproject.devNames].map((name) => normalizePackageName(name, "python")))
+ )));
+
+ let dependencies = deduplicateDeps(flattenDependencies(directRoots));
+ for (const record of records) {
+ const key = `${record.normalizedName}@${record.version.toLowerCase()}`;
+ if (dependencies.some((dependency) => (
+ `${normalizePackageName(dependency.name, "python")}@${dependency.version.toLowerCase()}` === key
+ ))) {
+ continue;
+ }
+
+ dependencies.push(createDependency({
+ name: record.name,
+ version: record.version,
+ ecosystem: "python",
+ isDirect: normalizedDirectNames.has(record.normalizedName),
+ parent: null,
+ parentChain: [],
+ transitives: [],
+ sourceFile,
+ isDevelopmentDependency: false,
+ }));
+ }
+
+ return buildTree("python", sourceFile, dependencies);
+}
+
+function buildPythonDependency(record, parentChain, recordsByName, visiting, sourceFile, normalizedDevNames) {
+ const key = `${record.normalizedName}@${record.version.toLowerCase()}`;
+ if (visiting.has(key)) {
+ return createDependency({
+ name: record.name,
+ version: record.version,
+ ecosystem: "python",
+ isDirect: parentChain.length === 0,
+ parent: parentChain[parentChain.length - 1] || null,
+ parentChain,
+ transitives: [],
+ sourceFile,
+ isDevelopmentDependency: normalizedDevNames.has(record.normalizedName),
+ });
+ }
+
+ const nextVisiting = new Set(visiting);
+ nextVisiting.add(key);
+ const nextParentChain = parentChain.concat(record.name);
+ const transitives = [];
+
+ for (const dependencyName of record.dependencies) {
+ const normalizedDependencyName = normalizePackageName(dependencyName, "python");
+ const childRecord = (recordsByName.get(normalizedDependencyName) || [])[0];
+ if (!childRecord) {
+ continue;
+ }
+ transitives.push(buildPythonDependency(
+ childRecord,
+ nextParentChain,
+ recordsByName,
+ nextVisiting,
+ sourceFile,
+ normalizedDevNames
+ ));
+ }
+
+ return createDependency({
+ name: record.name,
+ version: record.version,
+ ecosystem: "python",
+ isDirect: parentChain.length === 0,
+ parent: parentChain[parentChain.length - 1] || null,
+ parentChain,
+ transitives: deduplicateDeps(transitives),
+ sourceFile,
+ isDevelopmentDependency: normalizedDevNames.has(record.normalizedName),
+ });
+}
+
+function parsePythonPackageRecords(content, skipEditableRoot) {
+ const records = [];
+ let current = null;
+ let section = "";
+ let metadataDirectNames = [];
+
+ const flushCurrent = () => {
+ if (!current || !current.name || !current.version) {
+ current = null;
+ return;
+ }
+ const isEditableRoot = skipEditableRoot && current.sourceEditable === ".";
+ if (!isEditableRoot) {
+ records.push({
+ ...current,
+ normalizedName: normalizePackageName(current.name, "python"),
+ isRootDependency: metadataDirectNames.includes(normalizePackageName(current.name, "python")),
+ });
+ }
+ current = null;
+ };
+
+ for (const rawLine of String(content || "").split(/\r?\n/)) {
+ const line = stripTomlComment(rawLine).trim();
+ if (!line || line.startsWith("#")) {
+ continue;
+ }
+
+ if (line === "[[package]]") {
+ flushCurrent();
+ current = {
+ name: "",
+ version: "",
+ dependencies: [],
+ sourceEditable: "",
+ };
+ section = "package";
+ continue;
+ }
+
+ if (line === "[package.dependencies]") {
+ section = "package.dependencies";
+ continue;
+ }
+
+ if (line === "[metadata]") {
+ flushCurrent();
+ section = "metadata";
+ continue;
+ }
+
+ if (line.startsWith("[") && line.endsWith("]")) {
+ section = "";
+ continue;
+ }
+
+ if (section === "package" && current) {
+ if (line.startsWith("name =")) {
+ current.name = parseKeyValueLine(line).value.replace(/^["']|["']$/g, "");
+ continue;
+ }
+ if (line.startsWith("version =")) {
+ current.version = parseKeyValueLine(line).value.replace(/^["']|["']$/g, "");
+ continue;
+ }
+ if (line.startsWith("source =")) {
+ current.sourceEditable = parseInlineTomlValue(parseKeyValueLine(line).value, "editable");
+ continue;
+ }
+ if (line.startsWith("dependencies =")) {
+ const value = parseKeyValueLine(line).value;
+ if (value.startsWith("[")) {
+ current.dependencies.push(...parsePythonDependencyArray(value));
+ } else if (value.startsWith("{")) {
+ current.dependencies.push(...parsePythonDependencyInlineObjects(value));
+ }
+ }
+ continue;
+ }
+
+ if (section === "package.dependencies" && current) {
+ const parts = parseKeyValueLine(line);
+ if (parts && parts.key) {
+ current.dependencies.push(parts.key.replace(/^["']|["']$/g, ""));
+ }
+ continue;
+ }
+
+ if (section === "metadata") {
+ if (line.startsWith("direct-dependencies =") || line.startsWith("root-dependencies =")) {
+ metadataDirectNames = parseQuotedArray(parseKeyValueLine(line).value)
+ .map((name) => normalizePackageName(name, "python"));
+ }
+ }
+ }
+
+ flushCurrent();
+ return records;
+}
+
+function parsePythonDependencyArray(value) {
+ const names = [];
+ for (const item of parseQuotedArray(value)) {
+ const parsed = parseRequirementSpec(item);
+ if (parsed) {
+ names.push(parsed.name);
+ }
+ }
+ if (names.length > 0) {
+ return names;
+ }
+ return parsePythonDependencyInlineObjects(value);
+}
+
+function parsePythonDependencyInlineObjects(value) {
+ const names = [];
+ const pattern = /name\s*=\s*"([^"]+)"/g;
+ for (const match of String(value || "").matchAll(pattern)) {
+ names.push(match[1]);
+ }
+ return names;
+}
+
+module.exports = pythonParser;
diff --git a/util/lockfileParsers/rubyParser.js b/util/lockfileParsers/rubyParser.js
new file mode 100644
index 0000000..c673653
--- /dev/null
+++ b/util/lockfileParsers/rubyParser.js
@@ -0,0 +1,217 @@
+// Copyright 2026 Cloudsmith Ltd. All rights reserved.
+const path = require("path");
+const {
+ buildTree,
+ countIndent,
+ createDependency,
+ deduplicateDeps,
+ flattenDependencies,
+ getSourceFileName,
+ getWorkspacePath,
+ pathExists,
+ readUtf8,
+} = require("./shared");
+const { parseGemfileManifest } = require("./manifestHelpers");
+
+const rubyParser = {
+ name: "rubyParser",
+ ecosystem: "ruby",
+
+ async canResolve(workspaceFolder) {
+ const rootPath = getWorkspacePath(workspaceFolder);
+ return (await pathExists(path.join(rootPath, "Gemfile.lock"), workspaceFolder))
+ || (await pathExists(path.join(rootPath, "Gemfile"), workspaceFolder));
+ },
+
+ async detect(workspaceFolder) {
+ const rootPath = getWorkspacePath(workspaceFolder);
+ const lockfilePath = await pathExists(path.join(rootPath, "Gemfile.lock"), workspaceFolder)
+ ? path.join(rootPath, "Gemfile.lock")
+ : null;
+ const manifestPath = await pathExists(path.join(rootPath, "Gemfile"), workspaceFolder)
+ ? path.join(rootPath, "Gemfile")
+ : null;
+ if (!lockfilePath && !manifestPath) {
+ return [];
+ }
+ return [{
+ resolverName: this.name,
+ ecosystem: this.ecosystem,
+ lockfilePath,
+ manifestPath,
+ sourceFile: getSourceFileName(lockfilePath || manifestPath),
+ }];
+ },
+
+ async resolve({ lockfilePath, manifestPath, workspaceFolder }) {
+ const sourceFile = getSourceFileName(lockfilePath || manifestPath);
+ if (!lockfilePath) {
+ const dependencies = parseGemfileManifest(await readUtf8(manifestPath, workspaceFolder)).map((dependency) => createDependency({
+ name: dependency.name,
+ version: dependency.version,
+ ecosystem: "ruby",
+ isDirect: true,
+ parent: null,
+ parentChain: [],
+ transitives: [],
+ sourceFile,
+ isDevelopmentDependency: dependency.isDevelopmentDependency,
+ }));
+ return buildTree("ruby", sourceFile, dependencies);
+ }
+
+ const directNames = manifestPath && await pathExists(manifestPath, workspaceFolder)
+ ? new Set(parseGemfileManifest(await readUtf8(manifestPath, workspaceFolder)).map((dependency) => dependency.name.toLowerCase()))
+ : null;
+ const records = parseGemfileLock(await readUtf8(lockfilePath, workspaceFolder));
+ const recordsByName = new Map();
+ const incomingCounts = new Map();
+
+ for (const record of records) {
+ recordsByName.set(record.name.toLowerCase(), record);
+ for (const dependencyName of record.dependencies) {
+ incomingCounts.set(dependencyName.toLowerCase(), (incomingCounts.get(dependencyName.toLowerCase()) || 0) + 1);
+ }
+ }
+
+ const rootRecords = directNames && directNames.size > 0
+ ? [...directNames].map((name) => recordsByName.get(name)).filter(Boolean)
+ : records.filter((record) => !incomingCounts.get(record.name.toLowerCase()));
+
+ const directRoots = deduplicateDeps(rootRecords.map((record) => buildRubyDependency(
+ record,
+ [],
+ recordsByName,
+ new Set(),
+ sourceFile,
+ directNames || new Set()
+ )));
+
+ let dependencies = deduplicateDeps(flattenDependencies(directRoots));
+ for (const record of records) {
+ const key = `${record.name.toLowerCase()}@${record.version.toLowerCase()}`;
+ if (dependencies.some((dependency) => `${dependency.name.toLowerCase()}@${dependency.version.toLowerCase()}` === key)) {
+ continue;
+ }
+ dependencies.push(createDependency({
+ name: record.name,
+ version: record.version,
+ ecosystem: "ruby",
+ isDirect: directNames ? directNames.has(record.name.toLowerCase()) : false,
+ parent: null,
+ parentChain: [],
+ transitives: [],
+ sourceFile,
+ isDevelopmentDependency: false,
+ }));
+ }
+
+ return buildTree("ruby", sourceFile, dependencies);
+ },
+};
+
+function parseGemfileLock(content) {
+ const records = [];
+ let section = "";
+ let inSpecs = false;
+ let current = null;
+
+ const flushCurrent = () => {
+ if (current && current.name && current.version) {
+ records.push(current);
+ }
+ current = null;
+ };
+
+ for (const rawLine of String(content || "").split(/\r?\n/)) {
+ const trimmed = rawLine.trimEnd();
+ if (!trimmed) {
+ continue;
+ }
+ const indent = countIndent(rawLine);
+ const line = trimmed.trim();
+
+ if (indent === 0 && /^[A-Z][A-Z0-9_ ]+$/.test(line)) {
+ flushCurrent();
+ section = line;
+ inSpecs = false;
+ continue;
+ }
+ if (section === "GEM" && indent === 2 && line === "specs:") {
+ inSpecs = true;
+ continue;
+ }
+ if (!inSpecs) {
+ continue;
+ }
+ if (indent === 4) {
+ flushCurrent();
+ const match = line.match(/^([^\s(]+) \(([^)]+)\)/);
+ if (!match) {
+ continue;
+ }
+ current = { name: match[1], version: match[2], dependencies: [] };
+ continue;
+ }
+ if (indent >= 6 && current) {
+ const dependencyName = line.split(" ", 1)[0].split("(", 1)[0].replace(/!$/, "").trim();
+ if (dependencyName) {
+ current.dependencies.push(dependencyName);
+ }
+ }
+ }
+
+ flushCurrent();
+ return records;
+}
+
+function buildRubyDependency(record, parentChain, recordsByName, visiting, sourceFile, directNames) {
+ const key = `${record.name.toLowerCase()}@${record.version.toLowerCase()}`;
+ if (visiting.has(key)) {
+ return createDependency({
+ name: record.name,
+ version: record.version,
+ ecosystem: "ruby",
+ isDirect: parentChain.length === 0 || directNames.has(record.name.toLowerCase()),
+ parent: parentChain[parentChain.length - 1] || null,
+ parentChain,
+ transitives: [],
+ sourceFile,
+ isDevelopmentDependency: false,
+ });
+ }
+
+ const nextVisiting = new Set(visiting);
+ nextVisiting.add(key);
+ const nextParentChain = parentChain.concat(record.name);
+ const transitives = [];
+
+ for (const dependencyName of record.dependencies) {
+ const childRecord = recordsByName.get(dependencyName.toLowerCase());
+ if (!childRecord) {
+ continue;
+ }
+ transitives.push(buildRubyDependency(
+ childRecord,
+ nextParentChain,
+ recordsByName,
+ nextVisiting,
+ sourceFile,
+ directNames
+ ));
+ }
+
+ return createDependency({
+ name: record.name,
+ version: record.version,
+ ecosystem: "ruby",
+ isDirect: parentChain.length === 0 || directNames.has(record.name.toLowerCase()),
+ parent: parentChain[parentChain.length - 1] || null,
+ parentChain,
+ transitives: deduplicateDeps(transitives),
+ sourceFile,
+ isDevelopmentDependency: false,
+ });
+}
+
+module.exports = rubyParser;
diff --git a/util/lockfileParsers/shared.js b/util/lockfileParsers/shared.js
new file mode 100644
index 0000000..f4be6a1
--- /dev/null
+++ b/util/lockfileParsers/shared.js
@@ -0,0 +1,412 @@
+// Copyright 2026 Cloudsmith Ltd. All rights reserved.
+const fs = require("fs");
+const path = require("path");
+
+const LARGE_FILE_THRESHOLD_BYTES = 50 * 1024 * 1024;
+const WORKSPACE_PATH_ERROR = "Refusing to read files outside the workspace folder.";
+
+function getWorkspacePath(workspaceFolder) {
+ if (!workspaceFolder) {
+ return "";
+ }
+
+ if (typeof workspaceFolder === "string") {
+ return workspaceFolder;
+ }
+
+ if (workspaceFolder.uri && workspaceFolder.uri.fsPath) {
+ return workspaceFolder.uri.fsPath;
+ }
+
+ return String(workspaceFolder);
+}
+
+async function pathExists(targetPath, workspaceFolder) {
+ const safePath = await resolveWorkspaceFilePath(targetPath, workspaceFolder);
+ if (!safePath) {
+ return false;
+ }
+
+ try {
+ await fs.promises.access(safePath, fs.constants.R_OK);
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+function getCandidateWorkspaceRoot(targetPath, workspaceFolder) {
+ const workspacePath = getWorkspacePath(workspaceFolder);
+ if (workspacePath) {
+ return workspacePath;
+ }
+
+ const rawTargetPath = String(targetPath || "").trim();
+ if (!rawTargetPath) {
+ return "";
+ }
+
+ return path.dirname(path.resolve(rawTargetPath));
+}
+
+async function resolveWorkspaceRoot(targetPath, workspaceFolder) {
+ const candidateRoot = getCandidateWorkspaceRoot(targetPath, workspaceFolder);
+ if (!candidateRoot) {
+ return "";
+ }
+
+ try {
+ return await fs.promises.realpath(candidateRoot);
+ } catch {
+ return path.resolve(candidateRoot);
+ }
+}
+
+function isWithinWorkspace(workspaceRoot, targetPath) {
+ if (!workspaceRoot || !targetPath) {
+ return false;
+ }
+
+ const relativePath = path.relative(workspaceRoot, targetPath);
+ return relativePath === ""
+ || (!relativePath.startsWith("..") && !path.isAbsolute(relativePath));
+}
+
+async function resolveWorkspaceFilePath(targetPath, workspaceFolder) {
+ const rawTargetPath = String(targetPath || "").trim();
+ if (!rawTargetPath) {
+ return null;
+ }
+
+ const resolvedTargetPath = path.resolve(rawTargetPath);
+ const workspaceRoot = await resolveWorkspaceRoot(resolvedTargetPath, workspaceFolder);
+ if (!workspaceRoot) {
+ return null;
+ }
+
+ let realTargetPath;
+ try {
+ realTargetPath = await fs.promises.realpath(resolvedTargetPath);
+ } catch {
+ return null;
+ }
+
+ return isWithinWorkspace(workspaceRoot, realTargetPath)
+ ? realTargetPath
+ : null;
+}
+
+async function readUtf8(targetPath, workspaceFolder) {
+ const safePath = await resolveWorkspaceFilePath(targetPath, workspaceFolder);
+ if (!safePath) {
+ throw new Error(WORKSPACE_PATH_ERROR);
+ }
+
+ return fs.promises.readFile(safePath, "utf8");
+}
+
+async function readJson(targetPath, workspaceFolder) {
+ return JSON.parse(await readUtf8(targetPath, workspaceFolder));
+}
+
+async function statSafe(targetPath, workspaceFolder) {
+ const safePath = await resolveWorkspaceFilePath(targetPath, workspaceFolder);
+ if (!safePath) {
+ return null;
+ }
+
+ try {
+ return await fs.promises.stat(safePath);
+ } catch {
+ return null;
+ }
+}
+
+function getSourceFileName(targetPath) {
+ return path.basename(targetPath || "");
+}
+
+function normalizeVersion(version) {
+ if (version == null) {
+ return "";
+ }
+
+ return String(version)
+ .trim()
+ .replace(/^["']|["']$/g, "")
+ .replace(/^[~^<>=! ]+/, "")
+ .trim();
+}
+
+function createDependency({
+ name,
+ version,
+ ecosystem,
+ isDirect,
+ parent,
+ parentChain,
+ transitives,
+ sourceFile,
+ isDevelopmentDependency,
+}) {
+ return {
+ name: String(name || "").trim(),
+ version: String(version || "").trim(),
+ ecosystem: String(ecosystem || "").trim(),
+ isDirect: Boolean(isDirect),
+ parent: parent || null,
+ parentChain: Array.isArray(parentChain) ? parentChain.slice() : [],
+ transitives: Array.isArray(transitives) ? transitives.slice() : [],
+ cloudsmithStatus: null,
+ cloudsmithPackage: null,
+ sourceFile: sourceFile || null,
+ isDevelopmentDependency: Boolean(isDevelopmentDependency),
+ };
+}
+
+function flattenDependencies(dependencies) {
+ const flattened = [];
+
+ for (const dependency of Array.isArray(dependencies) ? dependencies : []) {
+ flattened.push(dependency);
+ if (Array.isArray(dependency.transitives) && dependency.transitives.length > 0) {
+ flattened.push(...flattenDependencies(dependency.transitives));
+ }
+ }
+
+ return flattened;
+}
+
+function dependencyKey(dependency) {
+ return [
+ String(dependency.ecosystem || "").trim().toLowerCase(),
+ String(dependency.name || "").trim().toLowerCase(),
+ String(dependency.version || "").trim().toLowerCase(),
+ ].join(":");
+}
+
+function deduplicateDeps(dependencies) {
+ const unique = [];
+ const seen = new Map();
+
+ for (const dependency of Array.isArray(dependencies) ? dependencies : []) {
+ const key = dependencyKey(dependency);
+ const existing = seen.get(key);
+ if (!existing) {
+ seen.set(key, dependency);
+ unique.push(dependency);
+ continue;
+ }
+
+ if (!existing.isDirect && dependency.isDirect) {
+ const index = unique.indexOf(existing);
+ if (index !== -1) {
+ unique[index] = dependency;
+ }
+ seen.set(key, dependency);
+ continue;
+ }
+
+ if (
+ Array.isArray(existing.parentChain) &&
+ existing.parentChain.length === 0 &&
+ Array.isArray(dependency.parentChain) &&
+ dependency.parentChain.length > 0
+ ) {
+ const merged = {
+ ...existing,
+ parent: dependency.parent,
+ parentChain: dependency.parentChain.slice(),
+ };
+ const index = unique.indexOf(existing);
+ if (index !== -1) {
+ unique[index] = merged;
+ }
+ seen.set(key, merged);
+ }
+ }
+
+ return unique;
+}
+
+function buildTree(ecosystem, sourceFile, dependencies, warnings) {
+ return {
+ ecosystem,
+ sourceFile,
+ dependencies: deduplicateDeps(dependencies),
+ warnings: Array.isArray(warnings) ? warnings.slice() : [],
+ };
+}
+
+function stripTomlComment(line) {
+ if (typeof line !== "string" || !line.includes("#")) {
+ return line || "";
+ }
+
+ let inSingleQuote = false;
+ let inDoubleQuote = false;
+
+ for (let index = 0; index < line.length; index += 1) {
+ const char = line[index];
+ const previous = index > 0 ? line[index - 1] : "";
+ if (char === "'" && !inDoubleQuote && previous !== "\\") {
+ inSingleQuote = !inSingleQuote;
+ continue;
+ }
+ if (char === "\"" && !inSingleQuote && previous !== "\\") {
+ inDoubleQuote = !inDoubleQuote;
+ continue;
+ }
+ if (char === "#" && !inSingleQuote && !inDoubleQuote) {
+ return line.slice(0, index);
+ }
+ }
+
+ return line;
+}
+
+function stripYamlComment(line) {
+ if (typeof line !== "string" || !line.includes("#")) {
+ return line || "";
+ }
+
+ let inSingleQuote = false;
+ let inDoubleQuote = false;
+
+ for (let index = 0; index < line.length; index += 1) {
+ const char = line[index];
+ const previous = index > 0 ? line[index - 1] : "";
+ if (char === "'" && !inDoubleQuote && previous !== "\\") {
+ inSingleQuote = !inSingleQuote;
+ continue;
+ }
+ if (char === "\"" && !inSingleQuote && previous !== "\\") {
+ inDoubleQuote = !inDoubleQuote;
+ continue;
+ }
+ if (char === "#" && !inSingleQuote && !inDoubleQuote) {
+ return line.slice(0, index);
+ }
+ }
+
+ return line;
+}
+
+function countIndent(line) {
+ if (typeof line !== "string") {
+ return 0;
+ }
+
+ const firstNonWhitespace = line.search(/\S/);
+ return firstNonWhitespace === -1 ? line.length : firstNonWhitespace;
+}
+
+function parseQuotedArray(rawValue) {
+ const value = String(rawValue || "").trim();
+ if (!value.startsWith("[") || !value.endsWith("]")) {
+ return [];
+ }
+
+ const results = [];
+ let current = "";
+ let inSingleQuote = false;
+ let inDoubleQuote = false;
+
+ for (let index = 1; index < value.length - 1; index += 1) {
+ const char = value[index];
+ const previous = index > 0 ? value[index - 1] : "";
+
+ if (char === "'" && !inDoubleQuote && previous !== "\\") {
+ inSingleQuote = !inSingleQuote;
+ current += char;
+ continue;
+ }
+ if (char === "\"" && !inSingleQuote && previous !== "\\") {
+ inDoubleQuote = !inDoubleQuote;
+ current += char;
+ continue;
+ }
+
+ if (char === "," && !inSingleQuote && !inDoubleQuote) {
+ const cleaned = current.trim().replace(/^["']|["']$/g, "");
+ if (cleaned) {
+ results.push(cleaned);
+ }
+ current = "";
+ continue;
+ }
+
+ current += char;
+ }
+
+ const cleaned = current.trim().replace(/^["']|["']$/g, "");
+ if (cleaned) {
+ results.push(cleaned);
+ }
+
+ return results;
+}
+
+function parseInlineTomlValue(block, key) {
+ if (typeof block !== "string" || !block.includes("{")) {
+ return "";
+ }
+
+ const expression = new RegExp(`${escapeRegExp(key)}\\s*=\\s*(\"([^\"]*)\"|'([^']*)'|([^,}]+))`);
+ const match = block.match(expression);
+ if (!match) {
+ return "";
+ }
+
+ return (match[2] || match[3] || match[4] || "").trim();
+}
+
+function escapeRegExp(value) {
+ return String(value || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+}
+
+function firstDefined(...values) {
+ for (const value of values) {
+ if (value != null && value !== "") {
+ return value;
+ }
+ }
+ return "";
+}
+
+function parseKeyValueLine(line) {
+ if (typeof line !== "string" || !line.includes("=")) {
+ return null;
+ }
+
+ const separatorIndex = line.indexOf("=");
+ return {
+ key: line.slice(0, separatorIndex).trim(),
+ value: line.slice(separatorIndex + 1).trim(),
+ };
+}
+
+module.exports = {
+ LARGE_FILE_THRESHOLD_BYTES,
+ buildTree,
+ countIndent,
+ createDependency,
+ deduplicateDeps,
+ dependencyKey,
+ escapeRegExp,
+ firstDefined,
+ flattenDependencies,
+ getSourceFileName,
+ getWorkspacePath,
+ normalizeVersion,
+ parseInlineTomlValue,
+ readJson,
+ parseKeyValueLine,
+ parseQuotedArray,
+ pathExists,
+ readUtf8,
+ resolveWorkspaceFilePath,
+ statSafe,
+ stripTomlComment,
+ stripYamlComment,
+};
diff --git a/util/lockfileParsers/swiftParser.js b/util/lockfileParsers/swiftParser.js
new file mode 100644
index 0000000..0e21f88
--- /dev/null
+++ b/util/lockfileParsers/swiftParser.js
@@ -0,0 +1,89 @@
+// Copyright 2026 Cloudsmith Ltd. All rights reserved.
+const path = require("path");
+const {
+ buildTree,
+ createDependency,
+ getSourceFileName,
+ getWorkspacePath,
+ readJson,
+ pathExists,
+ readUtf8,
+} = require("./shared");
+const { normalizeSwiftIdentity, parsePackageSwiftManifest } = require("./manifestHelpers");
+
+const swiftParser = {
+ name: "swiftParser",
+ ecosystem: "swift",
+
+ async canResolve(workspaceFolder) {
+ const rootPath = getWorkspacePath(workspaceFolder);
+ return (await pathExists(path.join(rootPath, "Package.resolved"), workspaceFolder))
+ || (await pathExists(path.join(rootPath, "Package.swift"), workspaceFolder));
+ },
+
+ async detect(workspaceFolder) {
+ const rootPath = getWorkspacePath(workspaceFolder);
+ const lockfilePath = await pathExists(path.join(rootPath, "Package.resolved"), workspaceFolder)
+ ? path.join(rootPath, "Package.resolved")
+ : null;
+ const manifestPath = await pathExists(path.join(rootPath, "Package.swift"), workspaceFolder)
+ ? path.join(rootPath, "Package.swift")
+ : null;
+ if (!lockfilePath && !manifestPath) {
+ return [];
+ }
+ return [{
+ resolverName: this.name,
+ ecosystem: this.ecosystem,
+ lockfilePath,
+ manifestPath,
+ sourceFile: getSourceFileName(lockfilePath || manifestPath),
+ }];
+ },
+
+ async resolve({ lockfilePath, manifestPath, workspaceFolder }) {
+ const sourceFile = getSourceFileName(lockfilePath || manifestPath);
+ if (!lockfilePath) {
+ return buildTree("swift", sourceFile, parsePackageSwiftManifest(await readUtf8(manifestPath, workspaceFolder)).map((dependency) => createDependency({
+ name: dependency.name,
+ version: dependency.version,
+ ecosystem: "swift",
+ isDirect: true,
+ parent: null,
+ parentChain: [],
+ transitives: [],
+ sourceFile,
+ isDevelopmentDependency: false,
+ })));
+ }
+
+ const manifestDirectNames = manifestPath && await pathExists(manifestPath, workspaceFolder)
+ ? new Set(parsePackageSwiftManifest(await readUtf8(manifestPath, workspaceFolder)).map((dependency) => dependency.name))
+ : new Set();
+ const root = await readJson(lockfilePath);
+ const pins = Array.isArray(root.pins)
+ ? root.pins
+ : (root.object && Array.isArray(root.object.pins) ? root.object.pins : []);
+ if (pins.length === 0) {
+ throw new Error("Malformed Package.resolved: missing pins array");
+ }
+
+ return buildTree("swift", sourceFile, pins.map((pin) => {
+ const state = pin.state || {};
+ const identity = normalizeSwiftIdentity(pin.identity || pin.package || pin.location || "");
+ return createDependency({
+ name: identity,
+ version: state.version || state.revision || state.branch || "",
+ ecosystem: "swift",
+ isDirect: manifestDirectNames.size === 0 || manifestDirectNames.has(identity),
+ parent: null,
+ parentChain: [],
+ transitives: [],
+ sourceFile,
+ isDevelopmentDependency: false,
+ });
+ }));
+ },
+};
+
+module.exports = swiftParser;
diff --git a/util/lockfileResolver.js b/util/lockfileResolver.js
new file mode 100644
index 0000000..844a51e
--- /dev/null
+++ b/util/lockfileResolver.js
@@ -0,0 +1,140 @@
+// Copyright 2026 Cloudsmith Ltd. All rights reserved.
+const path = require("path");
+const npmParser = require("./lockfileParsers/npmParser");
+const pythonParser = require("./lockfileParsers/pythonParser");
+const mavenParser = require("./lockfileParsers/mavenParser");
+const gradleParser = require("./lockfileParsers/gradleParser");
+const goParser = require("./lockfileParsers/goParser");
+const cargoParser = require("./lockfileParsers/cargoParser");
+const rubyParser = require("./lockfileParsers/rubyParser");
+const dockerParser = require("./lockfileParsers/dockerParser");
+const nugetParser = require("./lockfileParsers/nugetParser");
+const dartParser = require("./lockfileParsers/dartParser");
+const composerParser = require("./lockfileParsers/composerParser");
+const helmParser = require("./lockfileParsers/helmParser");
+const swiftParser = require("./lockfileParsers/swiftParser");
+const hexParser = require("./lockfileParsers/hexParser");
+const {
+ getWorkspacePath,
+ resolveWorkspaceFilePath,
+} = require("./lockfileParsers/shared");
+
+const REGISTERED_RESOLVERS = [
+ npmParser,
+ pythonParser,
+ mavenParser,
+ gradleParser,
+ goParser,
+ cargoParser,
+ rubyParser,
+ dockerParser,
+ nugetParser,
+ dartParser,
+ composerParser,
+ helmParser,
+ swiftParser,
+ hexParser,
+];
+
+class LockfileResolver {
+ static getResolvers() {
+ return REGISTERED_RESOLVERS.slice();
+ }
+
+ static async detectResolvers(workspaceFolder) {
+ const rootPath = getWorkspacePath(workspaceFolder);
+ const matches = [];
+
+ for (const resolver of REGISTERED_RESOLVERS) {
+ if (!resolver || typeof resolver.canResolve !== "function") {
+ continue;
+ }
+
+ if (!(await resolver.canResolve(rootPath))) {
+ continue;
+ }
+
+ const detections = typeof resolver.detect === "function"
+ ? await resolver.detect(rootPath)
+ : [{
+ resolverName: resolver.name,
+ ecosystem: resolver.ecosystem,
+ workspaceFolder: rootPath,
+ lockfilePath: null,
+ manifestPath: null,
+ }];
+
+ for (const detection of detections) {
+ matches.push({
+ resolverName: resolver.name,
+ ecosystem: resolver.ecosystem,
+ workspaceFolder: rootPath,
+ lockfilePath: detection.lockfilePath || null,
+ manifestPath: detection.manifestPath || null,
+ sourceFile: detection.sourceFile
+ || path.basename(detection.lockfilePath || detection.manifestPath || ""),
+ });
+ }
+ }
+
+ return matches;
+ }
+
+ static async resolve(resolverName, lockfilePath, manifestPath, options = {}) {
+ const resolver = REGISTERED_RESOLVERS.find((candidate) => candidate.name === resolverName);
+ if (!resolver) {
+ throw new Error(`Unknown lockfile resolver: ${resolverName}`);
+ }
+
+ const workspaceFolder = getWorkspacePath(options.workspaceFolder || path.dirname(lockfilePath || manifestPath || ""));
+ const safeLockfilePath = lockfilePath
+ ? await resolveWorkspaceFilePath(lockfilePath, workspaceFolder)
+ : null;
+ const safeManifestPath = manifestPath
+ ? await resolveWorkspaceFilePath(manifestPath, workspaceFolder)
+ : null;
+
+ if (lockfilePath && !safeLockfilePath) {
+ throw new Error("Lockfile paths must stay within the workspace folder.");
+ }
+
+ if (manifestPath && !safeManifestPath) {
+ throw new Error("Manifest paths must stay within the workspace folder.");
+ }
+
+ return resolver.resolve({
+ workspaceFolder,
+ lockfilePath: safeLockfilePath,
+ manifestPath: safeManifestPath,
+ options,
+ });
+ }
+
+ static async resolveAll(workspaceFolder, options = {}) {
+ const matches = await LockfileResolver.detectResolvers(workspaceFolder);
+ const trees = [];
+
+ for (const match of matches) {
+ const tree = await LockfileResolver.resolve(
+ match.resolverName,
+ match.lockfilePath,
+ match.manifestPath,
+ {
+ ...options,
+ workspaceFolder: match.workspaceFolder,
+ detection: match,
+ }
+ );
+
+ if (tree) {
+ trees.push(tree);
+ }
+ }
+
+ return trees;
+ }
+}
+
+module.exports = {
+ LockfileResolver,
+};
diff --git a/util/manifestParser.js b/util/manifestParser.js
index 0200f3a..65b450c 100644
--- a/util/manifestParser.js
+++ b/util/manifestParser.js
@@ -1,9 +1,11 @@
-// Manifest parser - detects and parses project dependency manifests
-// Supports: npm (package.json), Python (requirements.txt, pyproject.toml),
-// Maven (pom.xml), Go (go.mod), Cargo (Cargo.toml)
-
-const fs = require("fs");
+// Copyright 2026 Cloudsmith Ltd. All rights reserved.
const path = require("path");
+const { parsePyprojectManifest } = require("./lockfileParsers/manifestHelpers");
+const {
+ getWorkspacePath,
+ pathExists,
+ readUtf8,
+} = require("./lockfileParsers/shared");
class ManifestParser {
/**
@@ -15,9 +17,7 @@ class ManifestParser {
* @returns {Array<{filePath: string, format: string, parserMethod: string}>}
*/
static async detectManifests(workspaceFolderOrPath) {
- const root = typeof workspaceFolderOrPath === "string"
- ? workspaceFolderOrPath
- : workspaceFolderOrPath.uri.fsPath;
+ const root = getWorkspacePath(workspaceFolderOrPath);
const manifests = [];
const checks = [
@@ -31,15 +31,13 @@ class ManifestParser {
for (const check of checks) {
const filePath = path.join(root, check.file);
- try {
- await fs.promises.access(filePath, fs.constants.R_OK);
+ if (await pathExists(filePath, root)) {
manifests.push({
- filePath: filePath,
+ filePath,
format: check.format,
parserMethod: check.parserMethod,
+ workspaceFolder: root,
});
- } catch (e) { // eslint-disable-line no-unused-vars
- // File doesn't exist or isn't readable, skip
}
}
@@ -54,7 +52,10 @@ class ManifestParser {
*/
static async parseManifest(manifest) {
try {
- const content = await fs.promises.readFile(manifest.filePath, "utf8");
+ const content = await readUtf8(
+ manifest.filePath,
+ manifest.workspaceFolder || path.dirname(manifest.filePath || "")
+ );
const parser = ManifestParser[manifest.parserMethod];
if (!parser) {
return [];
@@ -157,88 +158,17 @@ class ManifestParser {
}
/**
- * Parse pyproject.toml — basic line-by-line parsing for
- * [project.dependencies] and [tool.poetry.dependencies].
- * Not a full TOML parser; handles the common cases.
+ * Parse pyproject.toml via the shared lockfile manifest helper so
+ * Poetry and PEP 621 formats stay consistent with lockfile resolution.
*/
static parsePyproject(content, format) {
- const deps = [];
- const lines = content.split("\n");
- let inDepsSection = false;
- let inPoetryDeps = false;
-
- for (const rawLine of lines) {
- const line = rawLine.trim();
-
- // Detect section headers
- if (line.startsWith("[")) {
- inDepsSection = line === "[project.dependencies]" ||
- line === "[project]";
- inPoetryDeps = line === "[tool.poetry.dependencies]";
- continue;
- }
-
- // Handle [project] section with dependencies = [...] array
- if (inDepsSection && line.startsWith("dependencies")) {
- // dependencies = ["flask>=2.0", "requests"]
- const arrayMatch = line.match(/dependencies\s*=\s*\[(.*)\]/);
- if (arrayMatch) {
- const items = arrayMatch[1].split(",");
- for (const item of items) {
- const cleaned = item.trim().replace(/['"]/g, "");
- if (!cleaned) {
- continue;
- }
- const depMatch = cleaned.match(/^([a-zA-Z0-9_\-.]+)\s*([><=!~]+)?\s*(.*)?/);
- if (depMatch) {
- deps.push({
- name: depMatch[1],
- version: depMatch[3] ? depMatch[3].trim() : "",
- devDependency: false,
- format: format || "python",
- });
- }
- }
- inDepsSection = false;
- continue;
- }
- }
-
- // Handle Poetry-style: name = "^version" or name = {version = "^version"}
- if (inPoetryDeps) {
- if (!line || line.startsWith("#")) {
- continue;
- }
- // Skip python version requirement
- if (line.startsWith("python")) {
- continue;
- }
-
- // name = "^1.2.3"
- const simpleMatch = line.match(/^([a-zA-Z0-9_\-.]+)\s*=\s*"([^"]*)"/);
- if (simpleMatch) {
- deps.push({
- name: simpleMatch[1],
- version: ManifestParser._stripVersionPrefix(simpleMatch[2]),
- devDependency: false,
- format: format || "python",
- });
- continue;
- }
-
- // name = { version = "^1.2.3", ... }
- const complexMatch = line.match(/^([a-zA-Z0-9_\-.]+)\s*=\s*\{.*version\s*=\s*"([^"]*)"/);
- if (complexMatch) {
- deps.push({
- name: complexMatch[1],
- version: ManifestParser._stripVersionPrefix(complexMatch[2]),
- devDependency: false,
- format: format || "python",
- });
- }
- }
- }
- return deps;
+ const parsed = parsePyprojectManifest(content);
+ return parsed.dependencies.map((dependency) => ({
+ name: dependency.name,
+ version: dependency.version,
+ devDependency: dependency.isDevelopmentDependency,
+ format: format || "python",
+ }));
}
/**
@@ -414,7 +344,7 @@ class ManifestParser {
*/
static async findDependencyLocation(filePath, dependencyName, format) {
try {
- const content = await fs.promises.readFile(filePath, "utf8");
+ const content = await readUtf8(filePath, path.dirname(filePath));
const lines = content.split("\n");
switch (format) {
diff --git a/util/packageNameNormalizer.js b/util/packageNameNormalizer.js
new file mode 100644
index 0000000..060f343
--- /dev/null
+++ b/util/packageNameNormalizer.js
@@ -0,0 +1,143 @@
+// Copyright 2026 Cloudsmith Ltd. All rights reserved.
+const MAX_PACKAGE_NAME_LENGTH = 4096;
+
+const ECOSYSTEM_TO_FORMAT = {
+ npm: "npm",
+ maven: "maven",
+ gradle: "maven",
+ pypi: "python",
+ python: "python",
+ go: "go",
+ cargo: "cargo",
+ ruby: "ruby",
+ docker: "docker",
+ nuget: "nuget",
+ dart: "dart",
+ composer: "composer",
+ helm: "helm",
+ swift: "swift",
+ hex: "hex",
+ conda: "conda",
+};
+
+function sanitizePackageNameInput(name) {
+ const normalized = String(name == null ? "" : name)
+ .replace(/\0/g, "")
+ .trim();
+
+ if (!normalized || normalized.length > MAX_PACKAGE_NAME_LENGTH) {
+ return "";
+ }
+
+ return normalized;
+}
+
+function canonicalFormat(ecosystemOrFormat) {
+ const normalized = sanitizePackageNameInput(ecosystemOrFormat).toLowerCase();
+ return ECOSYSTEM_TO_FORMAT[normalized] || normalized;
+}
+
+function normalizePackageName(name, ecosystemOrFormat) {
+ const format = canonicalFormat(ecosystemOrFormat);
+ const rawName = sanitizePackageNameInput(name);
+ if (!rawName) {
+ return "";
+ }
+
+ if (format === "python") {
+ return rawName.toLowerCase().replace(/[-_.]+/g, "-");
+ }
+
+ return rawName.toLowerCase();
+}
+
+function getPackageLookupKeys(name, ecosystemOrFormat, identifiers) {
+ const format = canonicalFormat(ecosystemOrFormat);
+ const rawName = sanitizePackageNameInput(name);
+ if (!rawName) {
+ return [];
+ }
+
+ if (format === "maven") {
+ const artifactId = rawName.includes(":") ? rawName.split(":").slice(1).join(":") : rawName;
+ const keys = [normalizePackageName(rawName, format)];
+ if (artifactId) {
+ keys.push(normalizePackageName(artifactId, format));
+ }
+ if (identifiers && identifiers.group_id) {
+ keys.push(normalizePackageName(`${identifiers.group_id}:${rawName}`, format));
+ }
+ return [...new Set(keys.filter(Boolean))];
+ }
+
+ if (format === "docker") {
+ return buildDockerLookupKeys(rawName);
+ }
+
+ return [normalizePackageName(rawName, format)];
+}
+
+function getCloudsmithPackageLookupKeys(pkg, ecosystemOrFormat) {
+ if (!pkg || typeof pkg !== "object") {
+ return [];
+ }
+
+ const format = canonicalFormat(ecosystemOrFormat || pkg.format);
+ if (format !== "maven") {
+ return getPackageLookupKeys(pkg.name, format);
+ }
+
+ const identifiers = pkg.identifiers && typeof pkg.identifiers === "object" ? pkg.identifiers : {};
+ const keys = [normalizePackageName(pkg.name, format)];
+ if (identifiers.group_id) {
+ keys.push(normalizePackageName(`${identifiers.group_id}:${pkg.name}`, format));
+ }
+ return [...new Set(keys.filter(Boolean))];
+}
+
+function buildDockerLookupKeys(name) {
+ const raw = normalizePackageName(String(name || "").replace(/^\/+/, ""), "docker").replace(/^\/+/, "");
+ if (!raw) {
+ return [];
+ }
+
+ const segments = raw.split("/").filter(Boolean);
+ const keys = new Set([raw]);
+ const firstSegment = segments[0] || "";
+ const hasExplicitRegistry = segments.length > 1 && (firstSegment.includes(".") || firstSegment.includes(":") || firstSegment === "localhost");
+
+ if (!hasExplicitRegistry) {
+ if (segments.length === 1) {
+ keys.add(`library/${segments[0]}`);
+ keys.add(`docker.io/library/${segments[0]}`);
+ keys.add(`index.docker.io/library/${segments[0]}`);
+ } else {
+ keys.add(`docker.io/${raw}`);
+ keys.add(`index.docker.io/${raw}`);
+ if (raw.startsWith("library/")) {
+ keys.add(raw.slice("library/".length));
+ }
+ }
+ return [...keys];
+ }
+
+ const pathPart = segments.slice(1).join("/");
+ if (["docker.io", "index.docker.io", "registry-1.docker.io"].includes(firstSegment) && pathPart) {
+ keys.add(pathPart);
+ keys.add(`docker.io/${pathPart}`);
+ if (pathPart.startsWith("library/")) {
+ keys.add(pathPart.slice("library/".length));
+ }
+ }
+
+ return [...keys];
+}
+
+module.exports = {
+ ECOSYSTEM_TO_FORMAT,
+ canonicalFormat,
+ getCloudsmithPackageLookupKeys,
+ getPackageLookupKeys,
+ normalizePackageName,
+ sanitizePackageNameInput,
+};
diff --git a/util/registryEndpoints.js b/util/registryEndpoints.js
new file mode 100644
index 0000000..3475759
--- /dev/null
+++ b/util/registryEndpoints.js
@@ -0,0 +1,600 @@
+// Copyright 2026 Cloudsmith Ltd. All rights reserved.
+const {
+ canonicalFormat,
+ sanitizePackageNameInput,
+} = require("./packageNameNormalizer");
+
+const CLOUDSMITH_HOST_SUFFIX = ".cloudsmith.io";
+const MAX_REGISTRY_VALUE_LENGTH = 4096;
+
+const UNSUPPORTED_PULL_FORMATS = new Set([
+ "alpine",
+ "conda",
+ "deb",
+ "generic",
+ "huggingface",
+ "raw",
+ "rpm",
+]);
+
+const DOCKER_MANIFEST_ACCEPT =
+ "application/vnd.docker.distribution.manifest.v2+json, "
+ + "application/vnd.docker.distribution.manifest.list.v2+json, "
+ + "application/vnd.oci.image.manifest.v1+json, "
+ + "application/vnd.oci.image.index.v1+json";
+
+function formatForEcosystem(ecosystemOrFormat) {
+ const normalized = canonicalFormat(ecosystemOrFormat);
+ return normalized || null;
+}
+
+function formatForDependency(dependency) {
+ return formatForEcosystem(dependency && (dependency.format || dependency.ecosystem));
+}
+
+function isPullUnsupportedFormat(format) {
+ const normalized = formatForEcosystem(format);
+ return Boolean(normalized && UNSUPPORTED_PULL_FORMATS.has(normalized));
+}
+
+function encodePathSegment(value) {
+ const normalized = String(value == null ? "" : value)
+ .replace(/\0/g, "")
+ .trim();
+
+ if (!normalized || normalized.length > MAX_REGISTRY_VALUE_LENGTH) {
+ return "";
+ }
+
+ if (normalized === ".") {
+ return "%2E";
+ }
+
+ if (normalized === "..") {
+ return "%2E%2E";
+ }
+
+ return encodeURIComponent(normalized);
+}
+
+function encodePath(value) {
+ return String(value == null ? "" : value)
+ .replace(/\0/g, "")
+ .trim()
+ .split("/")
+ .filter(Boolean)
+ .map((segment) => encodePathSegment(segment))
+ .join("/");
+}
+
+function normalizePythonName(name) {
+ return sanitizePackageNameInput(name).toLowerCase().replace(/[-_.]+/g, "-");
+}
+
+function encodeGoModulePath(modulePath) {
+ return [...String(modulePath || "")]
+ .map((character) => {
+ if (character === "!") {
+ return "!!";
+ }
+ if (character >= "A" && character <= "Z") {
+ return `!${character.toLowerCase()}`;
+ }
+ return character;
+ })
+ .join("");
+}
+
+function cargoIndexPath(crateName) {
+ const normalized = String(crateName || "").trim().toLowerCase();
+ if (!normalized) {
+ return null;
+ }
+
+ if (normalized.length <= 2) {
+ return encodePathSegment(normalized);
+ }
+
+ if (normalized.length === 3) {
+ return `1/${encodePathSegment(normalized)}`;
+ }
+
+ return [
+ encodePathSegment(normalized.slice(0, 2)),
+ encodePathSegment(normalized.slice(2, 4)),
+ encodePathSegment(normalized),
+ ].join("/");
+}
+
+function buildNpmPackagePath(name) {
+ const rawName = sanitizePackageNameInput(name);
+ if (!rawName) {
+ return null;
+ }
+
+ if (!rawName.startsWith("@")) {
+ if (rawName.includes("/")) {
+ return null;
+ }
+
+ const encodedName = encodePathSegment(rawName);
+ return {
+ segments: [encodedName],
+ tarballBaseName: encodedName,
+ };
+ }
+
+ const separatorIndex = rawName.indexOf("/");
+ if (
+ separatorIndex <= 1
+ || separatorIndex === rawName.length - 1
+ || rawName.indexOf("/", separatorIndex + 1) !== -1
+ ) {
+ return null;
+ }
+
+ const scope = rawName.slice(0, separatorIndex);
+ const packageName = rawName.slice(separatorIndex + 1);
+
+ return {
+ segments: [encodePathSegment(scope), encodePathSegment(packageName)],
+ tarballBaseName: encodePathSegment(packageName),
+ };
+}
+
+function buildMavenCoordinates(dependency) {
+ const name = sanitizePackageNameInput(dependency && dependency.name);
+ const version = String(dependency && dependency.version || "")
+ .replace(/\0/g, "")
+ .trim();
+ const coordinates = name.split(":", 3);
+
+ if (coordinates.length < 2 || !version) {
+ return null;
+ }
+
+ const groupId = coordinates[0].trim();
+ const artifactId = coordinates[1].trim();
+ if (!groupId || !artifactId) {
+ return null;
+ }
+
+ const groupPath = groupId
+ .split(".")
+ .filter(Boolean)
+ .map((segment) => encodePathSegment(segment))
+ .join("/");
+
+ if (!groupPath) {
+ return null;
+ }
+
+ return {
+ groupPath,
+ artifactId: encodePathSegment(artifactId),
+ version: encodePathSegment(version),
+ };
+}
+
+function buildComposerCoordinates(name) {
+ const rawName = sanitizePackageNameInput(name);
+ const separatorIndex = rawName.indexOf("/");
+ if (
+ separatorIndex <= 0
+ || separatorIndex === rawName.length - 1
+ || rawName.indexOf("/", separatorIndex + 1) !== -1
+ ) {
+ return null;
+ }
+
+ const vendor = rawName.slice(0, separatorIndex);
+ const packageName = rawName.slice(separatorIndex + 1);
+
+ return {
+ vendor: encodePathSegment(vendor),
+ package: encodePathSegment(packageName),
+ packageName: `${vendor}/${packageName}`,
+ };
+}
+
+function buildSwiftCoordinates(name) {
+ const parts = sanitizePackageNameInput(name).split("/").filter(Boolean);
+ if (parts.length < 2) {
+ return null;
+ }
+
+ return {
+ scope: parts.slice(0, -1).map((part) => encodePathSegment(part)).join("/"),
+ name: encodePathSegment(parts[parts.length - 1]),
+ };
+}
+
+function buildRegistryTriggerPlan(workspace, repo, dependency) {
+ const format = formatForDependency(dependency);
+ if (!format || isPullUnsupportedFormat(format)) {
+ return null;
+ }
+
+ const safeWorkspace = encodePathSegment(workspace);
+ const safeRepo = encodePathSegment(repo);
+ const version = encodePathSegment(dependency && dependency.version);
+
+ switch (format) {
+ case "maven": {
+ const coordinates = buildMavenCoordinates(dependency);
+ if (!coordinates) {
+ return null;
+ }
+ return {
+ format,
+ strategy: "direct",
+ request: {
+ method: "GET",
+ url: `https://dl.cloudsmith.io/basic/${safeWorkspace}/${safeRepo}/maven/${coordinates.groupPath}/${coordinates.artifactId}/${coordinates.version}/${coordinates.artifactId}-${coordinates.version}.pom`,
+ headers: {},
+ },
+ };
+ }
+ case "npm": {
+ const packagePath = buildNpmPackagePath(dependency && dependency.name);
+ if (!packagePath || !version) {
+ return null;
+ }
+ return {
+ format,
+ strategy: "direct",
+ request: {
+ method: "GET",
+ url: `https://npm.cloudsmith.io/${safeWorkspace}/${safeRepo}/${packagePath.segments.join("/")}/-/${packagePath.tarballBaseName}-${version}.tgz`,
+ headers: {},
+ },
+ };
+ }
+ case "python": {
+ const normalizedName = normalizePythonName(dependency && dependency.name);
+ if (!normalizedName) {
+ return null;
+ }
+ return {
+ format,
+ strategy: "python-simple-index",
+ request: {
+ method: "GET",
+ url: `https://dl.cloudsmith.io/basic/${safeWorkspace}/${safeRepo}/python/simple/${encodePathSegment(normalizedName)}/`,
+ headers: {},
+ },
+ };
+ }
+ case "go": {
+ const modulePath = encodeGoModulePath(String(dependency && dependency.name || "").trim());
+ if (!modulePath || !version) {
+ return null;
+ }
+ return {
+ format,
+ strategy: "direct",
+ request: {
+ method: "GET",
+ url: `https://golang.cloudsmith.io/${safeWorkspace}/${safeRepo}/${modulePath}/@v/${version}.info`,
+ headers: {},
+ },
+ };
+ }
+ case "cargo": {
+ const indexPath = cargoIndexPath(dependency && dependency.name);
+ if (!indexPath) {
+ return null;
+ }
+ return {
+ format,
+ strategy: "direct",
+ request: {
+ method: "GET",
+ url: `https://cargo.cloudsmith.io/${safeWorkspace}/${safeRepo}/${indexPath}`,
+ headers: {},
+ },
+ };
+ }
+ case "ruby": {
+ const name = encodePathSegment(dependency && dependency.name);
+ if (!name || !version) {
+ return null;
+ }
+ return {
+ format,
+ strategy: "direct",
+ request: {
+ method: "GET",
+ url: `https://dl.cloudsmith.io/basic/${safeWorkspace}/${safeRepo}/ruby/gems/${name}-${version}.gem`,
+ headers: {},
+ },
+ };
+ }
+ case "nuget": {
+ const name = encodePathSegment(dependency && dependency.name);
+ if (!name || !version) {
+ return null;
+ }
+ return {
+ format,
+ strategy: "direct",
+ request: {
+ method: "GET",
+ url: `https://nuget.cloudsmith.io/${safeWorkspace}/${safeRepo}/v3/package/${name}/${version}/${name}.${version}.nupkg`,
+ headers: {},
+ },
+ };
+ }
+ case "docker": {
+ const image = encodePath(dependency && dependency.name);
+ if (!image || !version) {
+ return null;
+ }
+ return {
+ format,
+ strategy: "direct",
+ request: {
+ method: "GET",
+ url: `https://docker.cloudsmith.io/v2/${safeWorkspace}/${safeRepo}/${image}/manifests/${version}`,
+ headers: {
+ Accept: DOCKER_MANIFEST_ACCEPT,
+ },
+ },
+ };
+ }
+ case "helm": {
+ const name = encodePathSegment(dependency && dependency.name);
+ if (!name || !version) {
+ return null;
+ }
+ return {
+ format,
+ strategy: "direct",
+ request: {
+ method: "GET",
+ url: `https://dl.cloudsmith.io/basic/${safeWorkspace}/${safeRepo}/helm/charts/${name}-${version}.tgz`,
+ headers: {},
+ },
+ };
+ }
+ case "dart": {
+ const name = encodePathSegment(dependency && dependency.name);
+ if (!name) {
+ return null;
+ }
+ return {
+ format,
+ strategy: "dart-api",
+ request: {
+ method: "GET",
+ url: `https://dart.cloudsmith.io/${safeWorkspace}/${safeRepo}/api/packages/${name}`,
+ headers: {},
+ },
+ };
+ }
+ case "composer": {
+ const coordinates = buildComposerCoordinates(dependency && dependency.name);
+ if (!coordinates) {
+ return null;
+ }
+ return {
+ format,
+ strategy: "composer-p2",
+ packageName: coordinates.packageName,
+ request: {
+ method: "GET",
+ url: `https://composer.cloudsmith.io/${safeWorkspace}/${safeRepo}/p2/${coordinates.vendor}/${coordinates.package}.json`,
+ headers: {},
+ },
+ };
+ }
+ case "hex": {
+ const name = encodePathSegment(dependency && dependency.name);
+ if (!name || !version) {
+ return null;
+ }
+ return {
+ format,
+ strategy: "direct",
+ request: {
+ method: "GET",
+ url: `https://dl.cloudsmith.io/basic/${safeWorkspace}/${safeRepo}/hex/tarballs/${name}-${version}.tar`,
+ headers: {},
+ },
+ };
+ }
+ case "swift": {
+ const coordinates = buildSwiftCoordinates(dependency && dependency.name);
+ if (!coordinates || !coordinates.scope || !coordinates.name || !version) {
+ return null;
+ }
+ return {
+ format,
+ strategy: "direct",
+ request: {
+ method: "GET",
+ url: `https://dl.cloudsmith.io/basic/${safeWorkspace}/${safeRepo}/swift/${coordinates.scope}/${coordinates.name}/${version}.zip`,
+ headers: {},
+ },
+ };
+ }
+ default:
+ return null;
+ }
+}
+
+function isTrustedCloudsmithHost(host) {
+ const normalizedHost = String(host || "").trim().toLowerCase();
+ return normalizedHost === "cloudsmith.io" || normalizedHost.endsWith(CLOUDSMITH_HOST_SUFFIX);
+}
+
+function isTrustedRegistryUrl(candidateUrl) {
+ try {
+ const parsed = new URL(candidateUrl);
+ return parsed.protocol === "https:" && isTrustedCloudsmithHost(parsed.host);
+ } catch {
+ return false;
+ }
+}
+
+function resolveAndValidateRegistryUrl(candidate, baseUrl) {
+ if (!candidate) {
+ return null;
+ }
+
+ let resolved;
+ try {
+ resolved = new URL(candidate, baseUrl);
+ } catch {
+ return null;
+ }
+
+ if (!isTrustedRegistryUrl(resolved.toString())) {
+ return null;
+ }
+
+ return resolved.toString();
+}
+
+function collectHrefValues(html) {
+ const hrefs = [];
+ const pattern = /]*\bhref\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\s>]+))/gi;
+ let match = pattern.exec(String(html || ""));
+
+ while (match) {
+ hrefs.push(match[1] || match[2] || match[3] || "");
+ match = pattern.exec(String(html || ""));
+ }
+
+ return hrefs.filter(Boolean);
+}
+
+function scorePythonArtifact(url, version) {
+ const normalizedVersion = String(version || "").trim().toLowerCase();
+ const fileName = decodeURIComponent(String(url || "").split("/").pop() || "").toLowerCase();
+
+ if (!fileName) {
+ return -1;
+ }
+
+ let score = 0;
+ if (normalizedVersion) {
+ if (!fileName.includes(normalizedVersion)) {
+ return -1;
+ }
+ score += 10;
+ }
+
+ if (fileName.endsWith(".whl")) {
+ score += 2;
+ } else if (fileName.endsWith(".tar.gz") || fileName.endsWith(".zip")) {
+ score += 1;
+ }
+
+ return score;
+}
+
+function findPythonDistributionUrl(html, version, baseUrl) {
+ const candidates = collectHrefValues(html)
+ .map((href) => resolveAndValidateRegistryUrl(href, baseUrl))
+ .filter(Boolean)
+ .map((url) => ({
+ url,
+ score: scorePythonArtifact(url, version),
+ }))
+ .filter((candidate) => candidate.score >= 0)
+ .sort((left, right) => right.score - left.score || left.url.localeCompare(right.url));
+
+ return candidates.length > 0 ? candidates[0].url : null;
+}
+
+function parseDartArchiveUrl(body, version, baseUrl) {
+ let payload;
+ try {
+ payload = JSON.parse(String(body || ""));
+ } catch {
+ return null;
+ }
+
+ const wantedVersion = String(version || "").trim();
+ const candidates = [];
+
+ if (payload && payload.latest && payload.latest.version === wantedVersion && payload.latest.archive_url) {
+ candidates.push(payload.latest.archive_url);
+ }
+
+ if (Array.isArray(payload && payload.versions)) {
+ for (const entry of payload.versions) {
+ if (entry && entry.version === wantedVersion && entry.archive_url) {
+ candidates.push(entry.archive_url);
+ }
+ }
+ } else if (payload && payload.versions && typeof payload.versions === "object") {
+ const entry = payload.versions[wantedVersion];
+ if (entry && entry.archive_url) {
+ candidates.push(entry.archive_url);
+ }
+ }
+
+ if (payload && payload.version === wantedVersion && payload.archive_url) {
+ candidates.push(payload.archive_url);
+ }
+
+ for (const candidate of candidates) {
+ const resolved = resolveAndValidateRegistryUrl(candidate, baseUrl);
+ if (resolved) {
+ return resolved;
+ }
+ }
+
+ return null;
+}
+
+function parseComposerDistUrl(body, packageName, version, baseUrl) {
+ let payload;
+ try {
+ payload = JSON.parse(String(body || ""));
+ } catch {
+ return null;
+ }
+
+ const entries = [];
+ const normalizedPackageName = sanitizePackageNameInput(packageName);
+
+ if (payload && payload.packages && typeof payload.packages === "object") {
+ if (Array.isArray(payload.packages[normalizedPackageName])) {
+ entries.push(...payload.packages[normalizedPackageName]);
+ } else {
+ for (const value of Object.values(payload.packages)) {
+ if (Array.isArray(value)) {
+ entries.push(...value);
+ }
+ }
+ }
+ }
+
+ if (Array.isArray(payload)) {
+ entries.push(...payload);
+ }
+
+ const matchedEntry = entries.find((entry) => entry && entry.version === version)
+ || entries.find(Boolean);
+ const distUrl = matchedEntry
+ && matchedEntry.dist
+ && typeof matchedEntry.dist === "object"
+ ? matchedEntry.dist.url
+ : null;
+
+ return resolveAndValidateRegistryUrl(distUrl, baseUrl);
+}
+
+module.exports = {
+ buildRegistryTriggerPlan,
+ findPythonDistributionUrl,
+ formatForDependency,
+ isPullUnsupportedFormat,
+ isTrustedRegistryUrl,
+ parseComposerDistUrl,
+ parseDartArchiveUrl,
+ resolveAndValidateRegistryUrl,
+};
diff --git a/util/upstreamChecker.js b/util/upstreamChecker.js
index 354693c..943de51 100644
--- a/util/upstreamChecker.js
+++ b/util/upstreamChecker.js
@@ -1,6 +1,4 @@
-// Upstream proxy resolution checker.
-// Provides a "what if I pull this?" dry run for packages that don't exist locally.
-
+// Copyright 2026 Cloudsmith Ltd. All rights reserved.
const { CloudsmithAPI } = require("./cloudsmithAPI");
const { CredentialManager } = require("./credentialManager");
const { SearchQueryBuilder } = require("./searchQueryBuilder");
@@ -156,6 +154,19 @@ function isAbortError(error) {
return error && (error.name === "AbortError" || error.code === "ABORT_ERR");
}
+function getActiveUpstreamsFromRepositoryState(state, format) {
+ if (!state || !(state.groupedUpstreams instanceof Map)) {
+ return [];
+ }
+
+ const upstreams = state.groupedUpstreams.get(format);
+ if (!Array.isArray(upstreams)) {
+ return [];
+ }
+
+ return upstreams.filter((upstream) => upstream && upstream.is_active !== false);
+}
+
function getUpstreamRequestOptions(apiKey, signal) {
const headers = {
accept: "application/json",
@@ -450,6 +461,11 @@ class UpstreamChecker {
return state.upstreams;
}
+ async getActiveRepositoryUpstreamsForFormat(workspace, repo, format, options = {}) {
+ const state = await this.getRepositoryUpstreamState(workspace, repo, options);
+ return getActiveUpstreamsFromRepositoryState(state, format);
+ }
+
/**
* Orchestrate a full upstream resolution preview.
* Checks local existence and upstream configs for a package preview.
@@ -492,7 +508,7 @@ class UpstreamChecker {
}
_isCacheObjectRecord(value) {
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
+ return isCacheObjectRecord(value);
}
_logRepositoryUpstreamCacheError(action, workspace, repo, error) {
@@ -749,98 +765,15 @@ class UpstreamChecker {
}
_getRequestOptions(apiKey, signal) {
- const headers = {
- accept: "application/json",
- "content-type": "application/json",
- };
-
- if (apiKey) {
- headers["X-Api-Key"] = apiKey;
- }
-
- const requestOptions = {
- method: "GET",
- headers,
- };
-
- if (signal) {
- requestOptions.signal = signal;
- }
-
- return requestOptions;
+ return getUpstreamRequestOptions(apiKey, signal);
}
_isAbortError(error) {
- return error && (error.name === "AbortError" || error.code === "ABORT_ERR");
+ return isAbortError(error);
}
_isWarningWorthyFormatError(message) {
- const normalized = typeof message === "string" ? message.toLowerCase() : "";
- if (!normalized) {
- return true;
- }
-
- const benignKeywords = [
- "response status: 404",
- "not found",
- "unsupported",
- "not applicable",
- "unknown format",
- "no upstream",
- "does not exist",
- ];
- if (benignKeywords.some((keyword) => normalized.includes(keyword))) {
- return false;
- }
-
- const statusMatch = normalized.match(/response status:\s*(\d{3})/);
- if (statusMatch) {
- const statusCode = Number(statusMatch[1]);
- if (
- statusCode === 401 ||
- statusCode === 403 ||
- statusCode === 407 ||
- statusCode === 408 ||
- statusCode === 429
- ) {
- return true;
- }
- if (statusCode >= 500) {
- return true;
- }
- if (statusCode >= 400) {
- return true;
- }
- }
-
- const warningKeywords = [
- "blocked ",
- "redirect",
- "fetch failed",
- "network",
- "timed out",
- "timeout",
- "unauthorized",
- "forbidden",
- "permission",
- "access denied",
- "server error",
- "bad gateway",
- "service unavailable",
- "gateway timeout",
- "econn",
- "enotfound",
- "eai_again",
- "socket",
- "tls",
- "certificate",
- ];
-
- if (warningKeywords.some((keyword) => normalized.includes(keyword))) {
- return true;
- }
-
- return true;
+ return isWarningWorthyUpstreamFormatError(message);
}
}
@@ -857,6 +790,7 @@ async function getUpstreamDataForFormats(context, workspace, repo, formats, opti
module.exports = {
getAllUpstreamData,
getUpstreamDataForFormats,
+ getActiveUpstreamsFromRepositoryState,
isBenignUpstreamFormatError,
SUPPORTED_UPSTREAM_FORMATS,
UpstreamChecker,
diff --git a/util/upstreamGapAnalyzer.js b/util/upstreamGapAnalyzer.js
new file mode 100644
index 0000000..2dac342
--- /dev/null
+++ b/util/upstreamGapAnalyzer.js
@@ -0,0 +1,213 @@
+// Copyright 2026 Cloudsmith Ltd. All rights reserved.
+const { canonicalFormat, normalizePackageName } = require("./packageNameNormalizer");
+const { normalizeUpstreamFormat } = require("./upstreamFormats");
+const { UpstreamChecker } = require("./upstreamChecker");
+
+const UPSTREAM_REPO_CONCURRENCY = 5;
+
+function getUncoveredDependencyKey(dependency) {
+ const format = canonicalFormat(dependency && (dependency.format || dependency.ecosystem));
+ const normalizedName = normalizePackageName(dependency && dependency.name, format);
+ const version = String(dependency && dependency.version || "").trim().toLowerCase();
+
+ if (!format || !normalizedName) {
+ return null;
+ }
+
+ return `${format}:${normalizedName}:${version}`;
+}
+
+function formatLabel(format) {
+ const normalized = String(format || "").trim();
+ if (!normalized) {
+ return "package";
+ }
+ if (normalized === "npm") {
+ return "npm";
+ }
+ if (normalized === "python") {
+ return "PyPI";
+ }
+ if (normalized === "go") {
+ return "Go";
+ }
+ return normalized.charAt(0).toUpperCase() + normalized.slice(1);
+}
+
+function buildProxyLabel(upstream, format) {
+ const configuredName = String(upstream && upstream.name || "").trim();
+ if (!configuredName) {
+ return `${formatLabel(format)} proxy`;
+ }
+ return configuredName.toLowerCase().includes("proxy")
+ ? configuredName
+ : `${configuredName} proxy`;
+}
+
+function buildReachableDetail(snapshot, upstream, format) {
+ return `${buildProxyLabel(upstream, format)} on ${snapshot.repo}`;
+}
+
+function classifyDependency(dependency, snapshots) {
+ const key = getUncoveredDependencyKey(dependency);
+ const format = canonicalFormat(dependency && (dependency.format || dependency.ecosystem));
+ if (!key || !format) {
+ return {
+ upstreamStatus: "unreachable",
+ upstreamDetail: "Not available through Cloudsmith",
+ };
+ }
+
+ const upstreamFormat = normalizeUpstreamFormat(format);
+ if (!upstreamFormat) {
+ return {
+ upstreamStatus: "unreachable",
+ upstreamDetail: "Not available through Cloudsmith",
+ };
+ }
+
+ for (const snapshot of snapshots) {
+ const formatUpstreams = Array.isArray(snapshot.groupedUpstreams.get(upstreamFormat))
+ ? snapshot.groupedUpstreams.get(upstreamFormat)
+ : [];
+ const activeUpstream = formatUpstreams.find((upstream) => upstream.is_active !== false);
+ if (!activeUpstream) {
+ continue;
+ }
+
+ return {
+ upstreamStatus: "reachable",
+ upstreamDetail: buildReachableDetail(snapshot, activeUpstream, upstreamFormat),
+ };
+ }
+
+ return {
+ upstreamStatus: "no_proxy",
+ upstreamDetail: `No upstream proxy configured for ${upstreamFormat}`,
+ };
+}
+
+function buildGapPatch(uncoveredDependencies, snapshots) {
+ const patchMap = new Map();
+
+ for (const dependency of Array.isArray(uncoveredDependencies) ? uncoveredDependencies : []) {
+ if (dependency.cloudsmithStatus !== "NOT_FOUND") {
+ continue;
+ }
+
+ const key = getUncoveredDependencyKey(dependency);
+ if (!key || patchMap.has(key)) {
+ continue;
+ }
+
+ patchMap.set(key, classifyDependency(dependency, snapshots));
+ }
+
+ return patchMap;
+}
+
+function applyGapPatch(dependencies, patchMap) {
+ return (Array.isArray(dependencies) ? dependencies : []).map((dependency) => {
+ const key = getUncoveredDependencyKey(dependency);
+ if (!key || !patchMap.has(key)) {
+ return dependency;
+ }
+
+ const gap = patchMap.get(key);
+ return {
+ ...dependency,
+ upstreamStatus: gap.upstreamStatus,
+ upstreamDetail: gap.upstreamDetail,
+ };
+ });
+}
+
+async function analyzeUpstreamGaps(uncoveredDependencies, workspace, repositories, options = {}) {
+ const onProgress = typeof options.onProgress === "function" ? options.onProgress : null;
+ const cancellationToken = options.cancellationToken || null;
+ const upstreamChecker = options.upstreamChecker || new UpstreamChecker(options.context);
+ const repositoriesToInspect = Array.isArray(repositories)
+ ? repositories.map((repo) => String(repo || "").trim()).filter(Boolean)
+ : [];
+
+ if (repositoriesToInspect.length === 0) {
+ const emptyPatch = buildGapPatch(uncoveredDependencies, []);
+ if (onProgress && emptyPatch.size > 0) {
+ onProgress(new Map(emptyPatch), {
+ completed: 0,
+ total: 0,
+ workspace,
+ stage: "upstream",
+ });
+ }
+ return applyGapPatch(uncoveredDependencies, emptyPatch);
+ }
+
+ const repoUpstreamStates = new Map();
+ let completed = 0;
+
+ await runPromisePool(repositoriesToInspect, UPSTREAM_REPO_CONCURRENCY, async (repo) => {
+ if (cancellationToken && cancellationToken.isCancellationRequested) {
+ return;
+ }
+
+ const state = await upstreamChecker.getRepositoryUpstreamState(workspace, repo);
+ repoUpstreamStates.set(repo, {
+ repo,
+ groupedUpstreams: state && state.groupedUpstreams instanceof Map
+ ? state.groupedUpstreams
+ : new Map(),
+ });
+
+ completed += 1;
+ if (onProgress) {
+ onProgress(new Map(), {
+ completed,
+ total: repositoriesToInspect.length,
+ workspace,
+ stage: "upstream",
+ });
+ }
+ });
+
+ const snapshots = repositoriesToInspect
+ .filter((repo) => repoUpstreamStates.has(repo))
+ .map((repo) => repoUpstreamStates.get(repo));
+
+ const patchMap = buildGapPatch(uncoveredDependencies, snapshots);
+ if (onProgress && patchMap.size > 0) {
+ onProgress(new Map(patchMap), {
+ completed,
+ total: repositoriesToInspect.length,
+ workspace,
+ stage: "upstream",
+ });
+ }
+ return applyGapPatch(uncoveredDependencies, patchMap);
+}
+
+async function runPromisePool(items, concurrency, worker) {
+ const workers = [];
+ let index = 0;
+ const size = Math.max(1, Math.min(concurrency, items.length || 1));
+
+ for (let workerIndex = 0; workerIndex < size; 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);
+}
+
+module.exports = {
+ analyzeUpstreamGaps,
+ getUncoveredDependencyKey,
+};
diff --git a/util/upstreamPullService.js b/util/upstreamPullService.js
new file mode 100644
index 0000000..cac515f
--- /dev/null
+++ b/util/upstreamPullService.js
@@ -0,0 +1,1231 @@
+// Copyright 2026 Cloudsmith Ltd. All rights reserved.
+const vscode = require("vscode");
+const { CloudsmithAPI } = require("./cloudsmithAPI");
+const { CredentialManager } = require("./credentialManager");
+const { PaginatedFetch } = require("./paginatedFetch");
+const {
+ buildRegistryTriggerPlan,
+ findPythonDistributionUrl,
+ formatForDependency,
+ isPullUnsupportedFormat,
+ isTrustedRegistryUrl,
+ parseComposerDistUrl,
+ parseDartArchiveUrl,
+ resolveAndValidateRegistryUrl,
+} = require("./registryEndpoints");
+const { canonicalFormat } = require("./packageNameNormalizer");
+const { UpstreamChecker } = require("./upstreamChecker");
+const { normalizeUpstreamFormat } = require("./upstreamFormats");
+
+const MAX_CONCURRENT_PULLS = 5;
+const INITIAL_AUTH_PROBE_CONCURRENCY = 3;
+const MAX_REGISTRY_REDIRECTS = 5;
+const REQUEST_TIMEOUT_MS = 30 * 1000;
+const WORKSPACE_REPOSITORY_PAGE_SIZE = 500;
+
+const PULL_STATUS = Object.freeze({
+ PENDING: "pending",
+ PULLING: "pulling",
+ CACHED: "cached",
+ ALREADY_EXISTS: "exists",
+ NOT_FOUND: "not_found",
+ AUTH_FAILED: "auth_failed",
+ FORMAT_MISMATCH: "mismatch",
+ ERROR: "error",
+ SKIPPED: "skipped",
+});
+
+const PULL_SKIP_REASON = Object.freeze({
+ NO_ACTIVE_UPSTREAM: "no_active_upstream",
+ NO_PULL_SUPPORT: "no_pull_support",
+ NO_TRIGGER_URL: "no_trigger_url",
+});
+
+class UpstreamPullService {
+ constructor(context, options = {}) {
+ this.context = context;
+ this._api = options.api || new CloudsmithAPI(context);
+ this._credentialManager = options.credentialManager || new CredentialManager(context);
+ this._fetchImpl = options.fetchImpl || fetch;
+ this._fetchRepositories = options.fetchRepositories || this._fetchWorkspaceRepositories.bind(this);
+ this._showQuickPick = options.showQuickPick || vscode.window.showQuickPick.bind(vscode.window);
+ this._showErrorMessage = options.showErrorMessage || vscode.window.showErrorMessage.bind(vscode.window);
+ this._showInformationMessage = options.showInformationMessage || vscode.window.showInformationMessage.bind(vscode.window);
+ this._showWarningMessage = options.showWarningMessage || vscode.window.showWarningMessage.bind(vscode.window);
+ this._upstreamChecker = options.upstreamChecker || new UpstreamChecker(context);
+ }
+
+ async run(options) {
+ const prepared = await this.prepare(options);
+ if (!prepared) {
+ return null;
+ }
+
+ const execution = await this.execute(prepared, options);
+ if (!execution) {
+ return null;
+ }
+
+ return {
+ ...prepared,
+ ...execution,
+ };
+ }
+
+ async prepare({
+ workspace,
+ repositoryHint,
+ dependencies,
+ }) {
+ const uncoveredDependencies = dedupePullDependencies(
+ (Array.isArray(dependencies) ? dependencies : [])
+ .filter((dependency) => dependency && dependency.cloudsmithStatus !== "FOUND")
+ );
+
+ if (!workspace) {
+ await this._showErrorMessage("Run a dependency scan against a Cloudsmith workspace first.");
+ return null;
+ }
+
+ if (uncoveredDependencies.length === 0) {
+ await this._showInformationMessage("No uncovered dependencies are available to pull.");
+ return null;
+ }
+
+ const projectFormats = [...new Set(
+ uncoveredDependencies
+ .map((dependency) => normalizeUpstreamFormat(formatForDependency(dependency)))
+ .filter(Boolean)
+ )];
+
+ if (projectFormats.length === 0) {
+ await this._showInformationMessage(
+ "Pull-through caching is not available for the uncovered dependency formats in this project."
+ );
+ return null;
+ }
+
+ let repositories;
+ try {
+ repositories = await this._fetchRepositories(workspace);
+ } catch (error) {
+ const message = error && error.message ? error.message : "Could not fetch workspace repositories.";
+ await this._showErrorMessage(message);
+ return null;
+ }
+
+ const repositoryMatches = await this._findMatchingRepositories(workspace, repositories, projectFormats);
+ if (repositoryMatches.length === 0) {
+ await this._showInformationMessage(
+ `No repositories have upstream proxies configured for the dependency formats in this project (${formatListLabel(projectFormats)}). Configure an upstream proxy in Cloudsmith to enable pull-through caching.`
+ );
+ return null;
+ }
+
+ const orderedMatches = sortRepositoryMatches(repositoryMatches, repositoryHint);
+ const selected = await this._showQuickPick(
+ orderedMatches.map((match) => ({
+ label: match.repo.slug || match.repo.name,
+ description: match.repo.name && match.repo.name !== match.repo.slug ? match.repo.name : "",
+ detail: `${formatListLabel(match.activeFormats)} upstream${match.activeFormats.length === 1 ? "" : "s"} configured`,
+ _match: match,
+ })),
+ {
+ placeHolder: "Select a repository to pull dependencies through",
+ matchOnDescription: true,
+ matchOnDetail: true,
+ }
+ );
+
+ if (!selected || !selected._match) {
+ return null;
+ }
+
+ const repository = selected._match.repo;
+ const plan = buildPullExecutionPlan(
+ workspace,
+ repository.slug,
+ uncoveredDependencies,
+ selected._match.activeFormats
+ );
+
+ if (plan.pullableDependencies.length === 0) {
+ await this._showInformationMessage(buildPullPlanErrorMessage(repository.slug, plan));
+ return null;
+ }
+
+ const confirmation = await this._showWarningMessage(
+ buildPullConfirmationMessage(plan, repository.slug),
+ { modal: true },
+ "Pull dependencies"
+ );
+
+ if (confirmation !== "Pull dependencies") {
+ return null;
+ }
+
+ return {
+ workspace,
+ repository,
+ plan,
+ };
+ }
+
+ async prepareSingle({
+ workspace,
+ repositoryHint,
+ dependency,
+ }) {
+ const normalizedDependency = normalizeSingleDependency(dependency);
+ if (!workspace) {
+ await this._showErrorMessage("Run a dependency scan against a Cloudsmith workspace first.");
+ return null;
+ }
+
+ if (!normalizedDependency) {
+ await this._showWarningMessage("Could not determine the dependency details to pull.");
+ return null;
+ }
+
+ const dependencyFormat = normalizeUpstreamFormat(formatForDependency(normalizedDependency));
+ if (!dependencyFormat) {
+ await this._showInformationMessage(
+ `Pull-through caching is not available for ${formatDisplayName(normalizedDependency.format)} dependencies.`
+ );
+ return null;
+ }
+
+ let repositories;
+ try {
+ repositories = await this._fetchRepositories(workspace);
+ } catch (error) {
+ const message = error && error.message ? error.message : "Could not fetch workspace repositories.";
+ await this._showErrorMessage(message);
+ return null;
+ }
+
+ const repositoryMatches = await this._findMatchingRepositories(workspace, repositories, [dependencyFormat]);
+ if (repositoryMatches.length === 0) {
+ await this._showInformationMessage(
+ `No repositories have a ${formatDisplayName(dependencyFormat)} upstream configured. Add one in Cloudsmith to pull this dependency.`
+ );
+ return null;
+ }
+
+ const orderedMatches = sortRepositoryMatches(repositoryMatches, repositoryHint);
+ const selected = await this._showQuickPick(
+ orderedMatches.map((match) => ({
+ label: match.repo.slug || match.repo.name,
+ description: match.repo.name && match.repo.name !== match.repo.slug ? match.repo.name : "",
+ detail: buildSingleDependencyRepositoryDetail(match, dependencyFormat),
+ _match: match,
+ })),
+ {
+ placeHolder: `Select a repository to pull ${buildDependencyLabel(normalizedDependency)} through`,
+ matchOnDescription: true,
+ matchOnDetail: true,
+ }
+ );
+
+ if (!selected || !selected._match) {
+ return null;
+ }
+
+ const repository = selected._match.repo;
+ const plan = buildPullExecutionPlan(
+ workspace,
+ repository.slug,
+ [normalizedDependency],
+ selected._match.activeFormats
+ );
+
+ if (plan.pullableDependencies.length === 0) {
+ await this._showInformationMessage(buildPullPlanErrorMessage(repository.slug, plan));
+ return null;
+ }
+
+ return {
+ workspace,
+ repository,
+ plan,
+ dependency: normalizedDependency,
+ };
+ }
+
+ async execute(prepared, options = {}) {
+ const apiKey = await this._credentialManager.getApiKey();
+ if (!apiKey) {
+ await this._showErrorMessage("Authentication failed. Check your API key in Cloudsmith settings.");
+ return null;
+ }
+
+ const progress = options.progress || null;
+ const token = options.token || null;
+ const onStatus = typeof options.onStatus === "function" ? options.onStatus : null;
+ const queue = prepared.plan.pullableDependencies.slice();
+ let nextDependencyIndex = 0;
+ const details = [];
+ const counts = createResultCounts(prepared.plan.pullableDependencies.length);
+ const state = {
+ authFailureCount: 0,
+ nonAuthOutcomeCount: 0,
+ stopForAuthFailure: false,
+ canceled: false,
+ allowedConcurrency: Math.min(
+ prepared.plan.pullableDependencies.length || 1,
+ INITIAL_AUTH_PROBE_CONCURRENCY
+ ),
+ expandedConcurrency: false,
+ };
+ const pending = new Set();
+ let activeCount = 0;
+ let launchedCount = 0;
+
+ const takeNextDependency = () => {
+ if (nextDependencyIndex >= queue.length) {
+ return null;
+ }
+
+ const dependency = queue[nextDependencyIndex];
+ nextDependencyIndex += 1;
+ return dependency;
+ };
+
+ const processNext = async () => {
+ if (token && token.isCancellationRequested) {
+ state.canceled = true;
+ return;
+ }
+
+ if (state.stopForAuthFailure) {
+ return;
+ }
+
+ const dependency = takeNextDependency();
+ if (!dependency) {
+ return;
+ }
+
+ activeCount += 1;
+
+ try {
+ const pullingDetail = {
+ dependency,
+ status: PULL_STATUS.PULLING,
+ errorMessage: null,
+ requestUrl: buildPullRequestUrl(prepared.workspace, prepared.repository.slug, dependency),
+ };
+ if (onStatus) {
+ await onStatus(pullingDetail);
+ }
+
+ const result = await this._pullDependency(
+ prepared.workspace,
+ prepared.repository.slug,
+ dependency,
+ apiKey,
+ token
+ );
+
+ if (result.canceled) {
+ state.canceled = true;
+ return;
+ }
+
+ details.push(result);
+ updateResultCounts(counts, result);
+
+ if (result.status === PULL_STATUS.AUTH_FAILED) {
+ state.authFailureCount += 1;
+ } else {
+ state.nonAuthOutcomeCount += 1;
+ }
+
+ if (
+ result.status === PULL_STATUS.AUTH_FAILED
+ && state.authFailureCount >= INITIAL_AUTH_PROBE_CONCURRENCY
+ && state.nonAuthOutcomeCount === 0
+ ) {
+ state.stopForAuthFailure = true;
+ }
+
+ if (
+ !state.expandedConcurrency
+ && state.nonAuthOutcomeCount > 0
+ && state.allowedConcurrency < MAX_CONCURRENT_PULLS
+ ) {
+ state.allowedConcurrency = Math.min(MAX_CONCURRENT_PULLS, counts.total);
+ state.expandedConcurrency = true;
+ }
+
+ if (progress) {
+ progress.report({
+ message: buildProgressMessage(counts),
+ increment: counts.total > 0 ? 100 / counts.total : 100,
+ });
+ }
+
+ if (onStatus) {
+ await onStatus(result);
+ }
+ } finally {
+ activeCount -= 1;
+ fillConcurrency();
+ }
+ };
+
+ const fillConcurrency = () => {
+ while (
+ activeCount < state.allowedConcurrency
+ && (state.expandedConcurrency || launchedCount < INITIAL_AUTH_PROBE_CONCURRENCY)
+ && nextDependencyIndex < queue.length
+ && !(token && token.isCancellationRequested)
+ && !state.stopForAuthFailure
+ ) {
+ launchedCount += 1;
+ const promise = processNext();
+ pending.add(promise);
+ promise.finally(() => pending.delete(promise));
+ }
+ };
+
+ fillConcurrency();
+
+ while (pending.size > 0) {
+ await Promise.race([...pending]);
+ }
+
+ if (state.stopForAuthFailure) {
+ while (nextDependencyIndex < queue.length) {
+ const dependency = queue[nextDependencyIndex];
+ nextDependencyIndex += 1;
+ details.push({
+ dependency,
+ status: PULL_STATUS.AUTH_FAILED,
+ errorMessage: "Skipped after repeated authentication failures.",
+ requestUrl: buildPullRequestUrl(prepared.workspace, prepared.repository.slug, dependency),
+ networkError: false,
+ });
+ }
+ recomputeResultCounts(counts, details);
+ await this._showErrorMessage("Authentication failed. Check your API key in Cloudsmith settings.");
+ } else if (state.canceled) {
+ return {
+ canceled: true,
+ };
+ } else if (
+ counts.completed > 0
+ && counts.completed === counts.errors
+ && counts.networkErrors === counts.errors
+ ) {
+ await this._showErrorMessage("Cannot reach the Cloudsmith registry. Check your network connection.");
+ }
+
+ return {
+ canceled: false,
+ pullResult: {
+ total: counts.total,
+ cached: counts.cached,
+ alreadyExisted: counts.alreadyExisted,
+ notFound: counts.notFound,
+ formatMismatched: counts.formatMismatched,
+ errors: counts.errors,
+ networkErrors: counts.networkErrors,
+ authFailed: counts.authFailed,
+ skipped: counts.skipped,
+ details,
+ },
+ };
+ }
+
+ async _findMatchingRepositories(workspace, repositories, projectFormats) {
+ const matches = [];
+
+ await runPromisePool(repositories, 5, async (repo) => {
+ const repoSlug = repo && repo.slug ? repo.slug : null;
+ if (!repoSlug) {
+ return;
+ }
+
+ const state = await this._upstreamChecker.getRepositoryUpstreamState(workspace, repoSlug);
+ const activeUpstreamsByFormat = new Map();
+ const activeFormats = projectFormats.filter((format) => {
+ const upstreams = state && state.groupedUpstreams instanceof Map
+ ? state.groupedUpstreams.get(format)
+ : [];
+ const activeUpstreams = Array.isArray(upstreams)
+ ? upstreams.filter((upstream) => upstream && upstream.is_active !== false)
+ : [];
+ if (activeUpstreams.length > 0) {
+ activeUpstreamsByFormat.set(format, activeUpstreams);
+ return true;
+ }
+ return false;
+ });
+
+ if (activeFormats.length === 0) {
+ return;
+ }
+
+ matches.push({
+ repo,
+ activeFormats,
+ activeUpstreamsByFormat,
+ });
+ });
+
+ return matches.sort((left, right) => {
+ const leftSlug = String(left.repo.slug || left.repo.name || "");
+ const rightSlug = String(right.repo.slug || right.repo.name || "");
+ return leftSlug.localeCompare(rightSlug, undefined, { sensitivity: "base" });
+ });
+ }
+
+ async _fetchWorkspaceRepositories(workspace) {
+ const paginatedFetch = new PaginatedFetch(this._api);
+ const endpoint = `repos/${workspace}/?sort=name`;
+ const repositories = [];
+ let page = 1;
+
+ while (true) {
+ const result = await paginatedFetch.fetchPage(endpoint, page, WORKSPACE_REPOSITORY_PAGE_SIZE);
+ if (result.error) {
+ throw new Error(`Could not fetch workspace repositories. ${result.error}`);
+ }
+
+ repositories.push(...(Array.isArray(result.data) ? result.data : []));
+
+ const pageTotal = result.pagination && result.pagination.pageTotal
+ ? result.pagination.pageTotal
+ : 1;
+ if (page >= pageTotal) {
+ break;
+ }
+ page += 1;
+ }
+
+ return repositories;
+ }
+
+ async _pullDependency(workspace, repo, dependency, apiKey, token) {
+ const plan = buildRegistryTriggerPlan(workspace, repo, dependency);
+ const format = formatForDependency(dependency);
+
+ if (!plan) {
+ const errorMessage = isPullUnsupportedFormat(format)
+ ? `Pull-through caching is not supported for ${formatDisplayName(format)} dependencies.`
+ : `No registry trigger URL is available for ${formatDisplayName(format)} dependencies.`;
+
+ return {
+ dependency,
+ status: PULL_STATUS.FORMAT_MISMATCH,
+ errorMessage,
+ requestUrl: null,
+ networkError: false,
+ };
+ }
+
+ const metadataAttempt = await this._requestRegistry(plan.request, apiKey, token);
+ if (metadataAttempt.canceled) {
+ return metadataAttempt;
+ }
+
+ if (plan.strategy === "direct") {
+ return mapRegistryAttempt(dependency, metadataAttempt, plan.request.url, format);
+ }
+
+ if (metadataAttempt.statusCode === 401 || metadataAttempt.statusCode === 403) {
+ return mapRegistryAttempt(dependency, metadataAttempt, plan.request.url, format);
+ }
+
+ if (metadataAttempt.statusCode === 404) {
+ return mapRegistryAttempt(dependency, metadataAttempt, plan.request.url, format);
+ }
+
+ if (metadataAttempt.statusCode < 200 || metadataAttempt.statusCode >= 300) {
+ return mapRegistryAttempt(dependency, metadataAttempt, plan.request.url, format);
+ }
+
+ let artifactUrl = null;
+ if (plan.strategy === "python-simple-index") {
+ artifactUrl = findPythonDistributionUrl(metadataAttempt.body, dependency.version, plan.request.url);
+ } else if (plan.strategy === "dart-api") {
+ artifactUrl = parseDartArchiveUrl(metadataAttempt.body, dependency.version, plan.request.url);
+ } else if (plan.strategy === "composer-p2") {
+ artifactUrl = parseComposerDistUrl(
+ metadataAttempt.body,
+ plan.packageName || dependency.name,
+ dependency.version,
+ plan.request.url
+ );
+ }
+
+ if (!artifactUrl) {
+ return {
+ dependency,
+ status: PULL_STATUS.NOT_FOUND,
+ errorMessage: missingArtifactMessage(plan.strategy, dependency.version),
+ requestUrl: plan.request.url,
+ networkError: false,
+ };
+ }
+
+ const artifactAttempt = await this._requestRegistry(
+ {
+ method: "GET",
+ url: artifactUrl,
+ headers: {},
+ },
+ apiKey,
+ token
+ );
+
+ if (artifactAttempt.canceled) {
+ return artifactAttempt;
+ }
+
+ return mapRegistryAttempt(dependency, artifactAttempt, artifactUrl, format);
+ }
+
+ async _requestRegistry(request, apiKey, token) {
+ const controller = new AbortController();
+ let didTimeout = false;
+ const timeoutHandle = setTimeout(() => {
+ didTimeout = true;
+ controller.abort();
+ }, REQUEST_TIMEOUT_MS);
+
+ const cancellationDisposable = token && typeof token.onCancellationRequested === "function"
+ ? token.onCancellationRequested(() => controller.abort())
+ : null;
+
+ try {
+ const response = await this._fetchRegistryResponse(
+ request,
+ apiKey,
+ controller.signal,
+ 0
+ );
+
+ return {
+ statusCode: response.status,
+ body: await response.text(),
+ };
+ } catch (error) {
+ if (token && token.isCancellationRequested) {
+ return { canceled: true };
+ }
+
+ return {
+ statusCode: 0,
+ body: "",
+ errorMessage: didTimeout
+ ? "Registry request timed out."
+ : buildRegistryErrorMessage(request.url, error),
+ networkError: isNetworkError(error) || didTimeout,
+ };
+ } finally {
+ clearTimeout(timeoutHandle);
+ if (cancellationDisposable && typeof cancellationDisposable.dispose === "function") {
+ cancellationDisposable.dispose();
+ }
+ }
+ }
+
+ async _fetchRegistryResponse(request, apiKey, signal, redirectCount) {
+ if (!request || !isTrustedRegistryUrl(request.url)) {
+ throw new Error("Refused to send Cloudsmith credentials to an untrusted registry host.");
+ }
+
+ const response = await this._fetchImpl(request.url, {
+ method: request.method || "GET",
+ headers: {
+ Authorization: buildBasicAuthHeader(apiKey),
+ ...(request.headers || {}),
+ },
+ redirect: "manual",
+ signal,
+ });
+
+ if (!isRedirectStatus(response.status)) {
+ return response;
+ }
+
+ if (redirectCount >= MAX_REGISTRY_REDIRECTS) {
+ throw new Error("Registry request exceeded the redirect limit.");
+ }
+
+ const location = response.headers && typeof response.headers.get === "function"
+ ? response.headers.get("location")
+ : "";
+ const redirectUrl = resolveAndValidateRegistryUrl(location, request.url);
+ if (!redirectUrl || !isTrustedRegistryUrl(redirectUrl)) {
+ throw new Error("Registry redirect target was rejected.");
+ }
+
+ return this._fetchRegistryResponse(
+ {
+ ...request,
+ url: redirectUrl,
+ },
+ apiKey,
+ signal,
+ redirectCount + 1
+ );
+ }
+}
+
+function buildPullExecutionPlan(workspace, repo, dependencies, activeUpstreamFormats) {
+ const normalizedActiveFormats = [...new Set(
+ (Array.isArray(activeUpstreamFormats) ? activeUpstreamFormats : [])
+ .map((format) => normalizeUpstreamFormat(format))
+ .filter(Boolean)
+ )];
+
+ const uniqueDependencies = dedupePullDependencies(dependencies);
+ const skippedDependencies = [];
+ const pullableDependencies = [];
+
+ for (const dependency of uniqueDependencies) {
+ const format = canonicalFormat(formatForDependency(dependency) || dependency.ecosystem || "");
+ const triggerPlan = buildRegistryTriggerPlan(workspace, repo, dependency);
+
+ if (isPullUnsupportedFormat(format)) {
+ skippedDependencies.push({
+ dependency,
+ format,
+ reason: PULL_SKIP_REASON.NO_PULL_SUPPORT,
+ message: `Pull-through caching is not supported for ${formatDisplayName(format)} dependencies.`,
+ });
+ continue;
+ }
+
+ if (!triggerPlan) {
+ skippedDependencies.push({
+ dependency,
+ format,
+ reason: PULL_SKIP_REASON.NO_TRIGGER_URL,
+ message: `No registry trigger URL is available for ${formatDisplayName(format)} dependencies.`,
+ });
+ continue;
+ }
+
+ if (!normalizedActiveFormats.includes(normalizeUpstreamFormat(format))) {
+ skippedDependencies.push({
+ dependency,
+ format,
+ reason: PULL_SKIP_REASON.NO_ACTIVE_UPSTREAM,
+ message: `No ${formatDisplayName(format)} upstream is configured on this repository.`,
+ });
+ continue;
+ }
+
+ pullableDependencies.push(dependency);
+ }
+
+ return {
+ dependencies: uniqueDependencies,
+ pullableDependencies,
+ skippedDependencies,
+ activeUpstreamFormats: normalizedActiveFormats,
+ };
+}
+
+function buildPullConfirmationMessage(plan, repositoryLabel) {
+ const totalCount = plan.dependencies.length;
+ const pullableCount = plan.pullableDependencies.length;
+ const header = plan.skippedDependencies.length > 0
+ ? `Pull ${pullableCount} of ${totalCount} dependencies through ${repositoryLabel}?`
+ : singleFormatPullHeader(plan.pullableDependencies, repositoryLabel);
+ const pulledLine = buildPullableSummary(plan.pullableDependencies, plan.skippedDependencies.length > 0);
+ const skippedLine = buildSkippedSummary(plan.skippedDependencies);
+
+ return [
+ header,
+ pulledLine,
+ skippedLine,
+ "Packages not already cached will be fetched from the upstream source.",
+ ].filter(Boolean).join("\n");
+}
+
+function buildPullPlanErrorMessage(repositoryLabel, plan) {
+ const noUpstreamFormats = [...new Set(
+ plan.skippedDependencies
+ .filter((entry) => entry.reason === PULL_SKIP_REASON.NO_ACTIVE_UPSTREAM)
+ .map((entry) => entry.format)
+ .filter(Boolean)
+ )];
+
+ if (plan.pullableDependencies.length === 0 && noUpstreamFormats.length > 0) {
+ return `No ${formatListLabel(noUpstreamFormats)} upstream${noUpstreamFormats.length === 1 ? "" : "s"} are configured on ${repositoryLabel}.`;
+ }
+
+ return "Pull-through caching is not available for the uncovered dependencies in this project.";
+}
+
+function buildPullSummaryMessage(result, skippedCount) {
+ const pulledCount = result.cached + result.alreadyExisted;
+ const parts = [
+ `Pulled ${pulledCount} of ${result.total} dependencies.`,
+ `${result.cached} cached`,
+ `${result.alreadyExisted} already existed`,
+ `${result.notFound} not found upstream`,
+ ];
+
+ if (skippedCount > 0) {
+ parts.push(`${skippedCount} skipped`);
+ }
+
+ if (result.errors > 0) {
+ parts.push(`${result.errors} errors`);
+ }
+
+ return `${parts.shift()} ${parts.join(", ")}.`;
+}
+
+function buildProgressMessage(counts) {
+ const parts = [`Pulling dependencies... ${counts.completed}/${counts.total}`];
+ const detail = [];
+
+ if (counts.cached > 0) {
+ detail.push(`${counts.cached} cached`);
+ }
+ if (counts.notFound > 0) {
+ detail.push(`${counts.notFound} not found`);
+ }
+ if (counts.errors > 0) {
+ detail.push(`${counts.errors} errors`);
+ }
+
+ if (detail.length > 0) {
+ parts.push(`(${detail.join(", ")})`);
+ }
+
+ return parts.join(" ");
+}
+
+function createResultCounts(total) {
+ return {
+ total,
+ completed: 0,
+ cached: 0,
+ alreadyExisted: 0,
+ notFound: 0,
+ formatMismatched: 0,
+ errors: 0,
+ networkErrors: 0,
+ authFailed: 0,
+ skipped: 0,
+ };
+}
+
+function updateResultCounts(counts, result) {
+ counts.completed += 1;
+ switch (result.status) {
+ case PULL_STATUS.CACHED:
+ counts.cached += 1;
+ break;
+ case PULL_STATUS.ALREADY_EXISTS:
+ counts.alreadyExisted += 1;
+ break;
+ case PULL_STATUS.NOT_FOUND:
+ counts.notFound += 1;
+ break;
+ case PULL_STATUS.FORMAT_MISMATCH:
+ counts.formatMismatched += 1;
+ break;
+ case PULL_STATUS.AUTH_FAILED:
+ counts.authFailed += 1;
+ counts.errors += 1;
+ break;
+ case PULL_STATUS.SKIPPED:
+ counts.skipped += 1;
+ break;
+ case PULL_STATUS.ERROR:
+ counts.errors += 1;
+ if (result.networkError) {
+ counts.networkErrors += 1;
+ }
+ break;
+ default:
+ break;
+ }
+}
+
+function recomputeResultCounts(counts, results) {
+ const next = createResultCounts(results.length);
+ for (const result of results) {
+ updateResultCounts(next, result);
+ }
+
+ Object.assign(counts, next);
+}
+
+function mapRegistryAttempt(dependency, attempt, requestUrl, format) {
+ if (attempt.statusCode >= 200 && attempt.statusCode < 300) {
+ return {
+ dependency,
+ status: PULL_STATUS.CACHED,
+ errorMessage: null,
+ requestUrl,
+ networkError: false,
+ };
+ }
+
+ if (attempt.statusCode === 304 || attempt.statusCode === 409) {
+ return {
+ dependency,
+ status: PULL_STATUS.ALREADY_EXISTS,
+ errorMessage: null,
+ requestUrl,
+ networkError: false,
+ };
+ }
+
+ if (attempt.statusCode === 401 || attempt.statusCode === 403) {
+ return {
+ dependency,
+ status: PULL_STATUS.AUTH_FAILED,
+ errorMessage: "Authentication failed.",
+ requestUrl,
+ networkError: false,
+ };
+ }
+
+ if (attempt.statusCode === 404) {
+ return {
+ dependency,
+ status: PULL_STATUS.NOT_FOUND,
+ errorMessage: defaultNotFoundMessage(format),
+ requestUrl,
+ networkError: false,
+ };
+ }
+
+ if (attempt.statusCode === 0) {
+ return {
+ dependency,
+ status: PULL_STATUS.ERROR,
+ errorMessage: attempt.errorMessage || "Registry request failed.",
+ requestUrl,
+ networkError: Boolean(attempt.networkError),
+ };
+ }
+
+ return {
+ dependency,
+ status: PULL_STATUS.ERROR,
+ errorMessage: `Registry request returned HTTP ${attempt.statusCode}.`,
+ requestUrl,
+ networkError: false,
+ };
+}
+
+function defaultNotFoundMessage(format) {
+ switch (format) {
+ case "docker":
+ return "Image manifest not found upstream.";
+ case "go":
+ return "Go module metadata not found upstream.";
+ case "cargo":
+ return "Cargo index entry not found upstream.";
+ case "helm":
+ return "Chart archive not found upstream.";
+ default:
+ return "Package not found upstream.";
+ }
+}
+
+function missingArtifactMessage(strategy, version) {
+ switch (strategy) {
+ case "python-simple-index":
+ return `No distribution file was found for version ${version}.`;
+ case "dart-api":
+ return `No Dart archive URL was found for version ${version}.`;
+ case "composer-p2":
+ return `No Composer dist URL was found for version ${version}.`;
+ default:
+ return "No downloadable artifact was found.";
+ }
+}
+
+function buildRegistryErrorMessage(url, error) {
+ if (isNetworkError(error)) {
+ return "Cannot reach the Cloudsmith registry. Check your network connection.";
+ }
+
+ const host = safeHost(url);
+ const message = error && error.message ? error.message : "Registry request failed.";
+ return host ? `${message} (${host})` : message;
+}
+
+function isNetworkError(error) {
+ const code = error && (
+ error.code
+ || (error.cause && error.cause.code)
+ || (error.errno)
+ );
+
+ if (["ECONNREFUSED", "ECONNRESET", "ENOTFOUND", "EHOSTUNREACH", "ETIMEDOUT"].includes(code)) {
+ return true;
+ }
+
+ const message = String(error && error.message || "").toLowerCase();
+ return message.includes("fetch failed")
+ || message.includes("network")
+ || message.includes("timed out")
+ || message.includes("econnrefused")
+ || message.includes("enotfound");
+}
+
+function buildBasicAuthHeader(apiKey) {
+ return `Basic ${Buffer.from(`token:${apiKey}`).toString("base64")}`;
+}
+
+function isRedirectStatus(statusCode) {
+ return Number.isInteger(statusCode) && statusCode >= 300 && statusCode < 400;
+}
+
+function safeHost(url) {
+ try {
+ return new URL(url).host;
+ } catch {
+ return "";
+ }
+}
+
+function buildPullRequestUrl(workspace, repo, dependency) {
+ const plan = buildRegistryTriggerPlan(workspace, repo, dependency);
+ return plan && plan.request ? plan.request.url : null;
+}
+
+function dedupePullDependencies(dependencies) {
+ const unique = new Map();
+
+ for (const dependency of Array.isArray(dependencies) ? dependencies : []) {
+ const key = pullDependencyKey(dependency);
+ if (!unique.has(key)) {
+ unique.set(key, dependency);
+ }
+ }
+
+ return [...unique.values()];
+}
+
+function pullDependencyKey(dependency) {
+ return [
+ String(canonicalFormat(dependency && (dependency.format || dependency.ecosystem)) || "").toLowerCase(),
+ String(dependency && dependency.name || "").toLowerCase(),
+ String(dependency && dependency.version || "").toLowerCase(),
+ ].join(":");
+}
+
+function singleFormatPullHeader(dependencies, repositoryLabel) {
+ const formats = [...new Set(
+ dependencies.map((dependency) => canonicalFormat(formatForDependency(dependency))).filter(Boolean)
+ )];
+
+ if (formats.length === 1) {
+ return `Pull ${dependencies.length} ${formatDisplayName(formats[0])} dependenc${dependencies.length === 1 ? "y" : "ies"} through ${repositoryLabel}?`;
+ }
+
+ return `Pull ${dependencies.length} dependencies through ${repositoryLabel}?`;
+}
+
+function buildPullableSummary(dependencies, forceSummary) {
+ const groups = groupCountsByFormat(dependencies);
+ if (groups.length === 0) {
+ return "";
+ }
+
+ if (!forceSummary && groups.length === 1) {
+ return "";
+ }
+
+ return `${groups.map(({ count, format }) => `${count} ${formatDisplayName(format)}`).join(" + ")} will be pulled.`;
+}
+
+function buildSkippedSummary(skippedDependencies) {
+ const groups = groupCountsByFormat(skippedDependencies.map((entry) => ({
+ format: entry.format,
+ })));
+
+ if (groups.length === 0) {
+ return "";
+ }
+
+ const reason = skippedDependencies.every((entry) => entry.reason === PULL_SKIP_REASON.NO_ACTIVE_UPSTREAM)
+ ? "no matching upstream is configured on this repository"
+ : "pull-through is not available for these formats";
+
+ return `${groups.map(({ count, format }) => `${count} ${formatDisplayName(format)}`).join(" + ")} will be skipped (${reason}).`;
+}
+
+function groupCountsByFormat(dependencies) {
+ const counts = new Map();
+
+ for (const dependency of Array.isArray(dependencies) ? dependencies : []) {
+ const format = canonicalFormat(
+ dependency && (dependency.format || dependency.ecosystem || formatForDependency(dependency))
+ );
+ if (!format) {
+ continue;
+ }
+ counts.set(format, (counts.get(format) || 0) + 1);
+ }
+
+ return [...counts.entries()]
+ .map(([format, count]) => ({ format, count }))
+ .sort((left, right) => {
+ if (right.count !== left.count) {
+ return right.count - left.count;
+ }
+ return formatDisplayName(left.format).localeCompare(formatDisplayName(right.format), undefined, {
+ sensitivity: "base",
+ });
+ });
+}
+
+function formatDisplayName(format) {
+ const normalized = String(canonicalFormat(format) || format || "").trim().toLowerCase();
+ switch (normalized) {
+ case "npm":
+ return "npm";
+ case "python":
+ return "Python";
+ case "go":
+ return "Go";
+ case "nuget":
+ return "NuGet";
+ default:
+ return normalized ? normalized.charAt(0).toUpperCase() + normalized.slice(1) : "Unknown";
+ }
+}
+
+function formatListLabel(formats) {
+ return [...new Set(
+ (Array.isArray(formats) ? formats : [])
+ .map((format) => formatDisplayName(format))
+ .filter(Boolean)
+ )].join(", ");
+}
+
+function normalizeSingleDependency(dependency) {
+ if (!dependency || typeof dependency !== "object") {
+ return null;
+ }
+
+ const format = canonicalFormat(formatForDependency(dependency) || dependency.format || dependency.ecosystem);
+ const name = String(dependency.name || "").trim();
+ if (!name || !format) {
+ return null;
+ }
+
+ return {
+ ...dependency,
+ name,
+ version: dependency.version || dependency.declaredVersion || "",
+ format,
+ ecosystem: dependency.ecosystem || format,
+ };
+}
+
+function buildDependencyLabel(dependency) {
+ const name = String(dependency && dependency.name || "").trim() || "dependency";
+ const version = String(dependency && dependency.version || "").trim();
+ return version ? `${name}@${version}` : name;
+}
+
+function buildSingleDependencyRepositoryDetail(match, format) {
+ const upstreams = match && match.activeUpstreamsByFormat instanceof Map
+ ? match.activeUpstreamsByFormat.get(format)
+ : [];
+ const activeUpstream = Array.isArray(upstreams) ? upstreams[0] : null;
+ const configuredName = String(activeUpstream && activeUpstream.name || "").trim();
+ const sourceLabel = configuredName || defaultUpstreamSourceLabel(format);
+ if (!sourceLabel) {
+ return `${formatDisplayName(format)} upstream configured`;
+ }
+ return `${formatDisplayName(format)} upstream (${sourceLabel})`;
+}
+
+function defaultUpstreamSourceLabel(format) {
+ switch (canonicalFormat(format)) {
+ case "cargo":
+ return "crates.io";
+ case "composer":
+ return "Packagist";
+ case "conda":
+ return "Conda";
+ case "dart":
+ return "pub.dev";
+ case "docker":
+ return "Docker";
+ case "go":
+ return "Go";
+ case "helm":
+ return "Helm";
+ case "hex":
+ return "Hex";
+ case "maven":
+ return "Maven";
+ case "npm":
+ return "npm";
+ case "nuget":
+ return "NuGet";
+ case "python":
+ return "PyPI";
+ case "ruby":
+ return "RubyGems";
+ case "swift":
+ return "Swift";
+ default:
+ return null;
+ }
+}
+
+function sortRepositoryMatches(matches, repositoryHint) {
+ const hint = String(repositoryHint || "").trim().toLowerCase();
+ if (!hint) {
+ return matches;
+ }
+
+ return matches.slice().sort((left, right) => {
+ const leftSlug = String(left.repo.slug || "").toLowerCase();
+ const rightSlug = String(right.repo.slug || "").toLowerCase();
+ const leftPriority = leftSlug === hint ? 0 : 1;
+ const rightPriority = rightSlug === hint ? 0 : 1;
+ if (leftPriority !== rightPriority) {
+ return leftPriority - rightPriority;
+ }
+ return leftSlug.localeCompare(rightSlug, undefined, { sensitivity: "base" });
+ });
+}
+
+async function runPromisePool(items, concurrency, worker) {
+ const workers = [];
+ let index = 0;
+ const size = Math.max(1, Math.min(concurrency, items.length || 1));
+
+ for (let workerIndex = 0; workerIndex < size; 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);
+}
+
+module.exports = {
+ PULL_SKIP_REASON,
+ PULL_STATUS,
+ UpstreamPullService,
+ buildPullSummaryMessage,
+};
diff --git a/views/dependencyHealthProvider.js b/views/dependencyHealthProvider.js
index 5bfd331..7ad2753 100644
--- a/views/dependencyHealthProvider.js
+++ b/views/dependencyHealthProvider.js
@@ -1,21 +1,81 @@
-// Dependency Health tree data provider.
-// Reads project manifests, cross-references dependencies against Cloudsmith,
-// and surfaces a health dashboard in the sidebar.
-
-const vscode = require("vscode");
+// Copyright 2026 Cloudsmith Ltd. All rights reserved.
const path = require("path");
+const vscode = require("vscode");
const { CloudsmithAPI } = require("../util/cloudsmithAPI");
+const { LockfileResolver } = require("../util/lockfileResolver");
const { ManifestParser } = require("../util/manifestParser");
-const { TransitiveResolver } = require("../util/transitiveResolver");
+const { PaginatedFetch } = require("../util/paginatedFetch");
+const { SearchQueryBuilder } = require("../util/searchQueryBuilder");
+const { LicenseClassifier } = require("../util/licenseClassifier");
+const {
+ canonicalFormat,
+ getCloudsmithPackageLookupKeys,
+ getPackageLookupKeys,
+ normalizePackageName,
+} = require("../util/packageNameNormalizer");
+const {
+ enrichVulnerabilities,
+} = require("../util/dependencyVulnEnricher");
+const {
+ enrichLicenses,
+ getFoundDependencyKey,
+} = require("../util/dependencyLicenseEnricher");
+const { enrichPolicies } = require("../util/dependencyPolicyEnricher");
+const {
+ analyzeUpstreamGaps,
+ getUncoveredDependencyKey,
+} = require("../util/upstreamGapAnalyzer");
+const {
+ PULL_STATUS,
+ UpstreamPullService,
+ buildPullSummaryMessage,
+} = require("../util/upstreamPullService");
const DependencyHealthNode = require("../models/dependencyHealthNode");
+const DependencySourceGroupNode = require("../models/dependencySourceGroupNode");
+const DependencySummaryNode = require("../models/dependencySummaryNode");
const InfoNode = require("../models/infoNode");
-const BATCH_SIZE = 5;
-const DEFAULT_MAX_DEPENDENCIES_TO_SCAN = 200;
+const DEFAULT_MAX_DEPENDENCIES_TO_SCAN = 10000;
+const PACKAGE_INDEX_TTL_MS = 10 * 60 * 1000;
+const PACKAGE_INDEX_CACHE_MAX_SIZE = 5000;
+const PACKAGE_INDEX_PAGE_SIZE = 500;
+const PACKAGE_INDEX_FALLBACK_THRESHOLD = 10000;
+const FALLBACK_QUERY_PAGE_SIZE = 25;
+const FALLBACK_QUERY_CONCURRENCY = 8;
+const WORKSPACE_REPOSITORY_PAGE_SIZE = 500;
+const COVERAGE_MATCH_BATCH_SIZE = 50;
+const ENRICHMENT_PROGRESS_DEBOUNCE_MS = 500;
+
+const FILTER_MODES = Object.freeze({
+ VULNERABLE: "vulnerable",
+ UNCOVERED: "uncovered",
+ RESTRICTIVE_LICENSE: "restrictive_license",
+ POLICY_VIOLATION: "policy_violation",
+});
+
+const SORT_MODES = Object.freeze({
+ ALPHABETICAL: "alphabetical",
+ SEVERITY: "severity",
+ COVERAGE: "coverage",
+});
+
+const VIEW_MODES = ["direct", "flat", "tree"];
class DependencyHealthProvider {
- constructor(context, diagnosticsPublisher) {
+ constructor(context, diagnosticsPublisher, options = {}) {
this.context = context;
+ this._diagnosticsPublisher = diagnosticsPublisher || null;
+ this._services = {
+ enrichVulnerabilities: options.enrichVulnerabilities || enrichVulnerabilities,
+ enrichLicenses: options.enrichLicenses || enrichLicenses,
+ enrichPolicies: options.enrichPolicies || enrichPolicies,
+ analyzeUpstreamGaps: options.analyzeUpstreamGaps || analyzeUpstreamGaps,
+ fetchRepositories: options.fetchRepositories || this._fetchWorkspaceRepositories.bind(this),
+ upstreamPullService: options.upstreamPullService || new UpstreamPullService(context),
+ };
+ this._reportDateFactory = typeof options.reportDateFactory === "function"
+ ? options.reportDateFactory
+ : () => new Date();
this._onDidChangeTreeData = new vscode.EventEmitter();
this.onDidChangeTreeData = this._onDidChangeTreeData.event;
this.dependencies = [];
@@ -23,50 +83,143 @@ class DependencyHealthProvider {
this.lastRepo = null;
this._scanning = false;
this._statusMessage = null;
- this._diagnosticsPublisher = diagnosticsPublisher || null;
+ this._failureMessage = null;
+ this._warnings = [];
this._lastManifests = [];
- this._projectFolderPath = null; // manually selected folder, persists across scans
+ this._projectFolderPath = null;
this._hasScannedOnce = false;
- // Auto-refresh when connection status changes in secrets store.
- // This ensures the welcome/connected state updates without external refresh calls.
- this.context.secrets.onDidChange(e => {
- if (e.key === "cloudsmith-vsc.isConnected") {
- this.refresh();
- }
- });
+ this._noManifestsFolder = null;
+ this._fullTrees = [];
+ this._displayTrees = [];
+ this._summary = emptySummary();
+ this._viewMode = this._getInitialViewMode();
+ this._sortMode = SORT_MODES.ALPHABETICAL;
+ this._filterMode = null;
+ this._reportData = null;
+ this._lastScanTimestamp = null;
+
+ if (this.context && this.context.secrets && typeof this.context.secrets.onDidChange === "function") {
+ this.context.secrets.onDidChange((event) => {
+ if (event.key === "cloudsmith-vsc.isConnected") {
+ this.refresh();
+ }
+ });
+ }
+
+ this._updateContexts();
+ }
+
+ _getInitialViewMode() {
+ const config = vscode.workspace.getConfiguration("cloudsmith-vsc");
+ const configuredDefault = String(config.get("dependencyTreeDefaultView") || "flat");
+ const storedView = this.context && this.context.workspaceState
+ ? this.context.workspaceState.get("cloudsmith-vsc.dependencyTreeView")
+ : null;
+ const candidate = String(storedView || configuredDefault || "flat");
+ return ["direct", "flat", "tree"].includes(candidate) ? candidate : "flat";
+ }
+
+ async _updateContexts() {
+ await vscode.commands.executeCommand("setContext", "cloudsmith.depView", this._viewMode);
+ await vscode.commands.executeCommand("setContext", "cloudsmith.depViewMode", this._viewMode);
+ await vscode.commands.executeCommand("setContext", "cloudsmith.depFilterActive", Boolean(this._filterMode));
+ await vscode.commands.executeCommand("setContext", "cloudsmith.depScanComplete", Boolean(this._reportData));
+ await vscode.commands.executeCommand("setContext", "cloudsmith.depRepoSelected", Boolean(this.lastRepo));
+ }
+
+ async setViewMode(mode) {
+ if (!VIEW_MODES.includes(mode)) {
+ return;
+ }
+
+ this._viewMode = mode;
+ if (this.context && this.context.workspaceState && typeof this.context.workspaceState.update === "function") {
+ await this.context.workspaceState.update("cloudsmith-vsc.dependencyTreeView", mode);
+ }
+ await this._updateContexts();
+ this._rebuildSummary();
+ this.refresh();
+ }
+
+ getViewMode() {
+ return this._viewMode;
+ }
+
+ async cycleViewMode() {
+ const currentIndex = VIEW_MODES.indexOf(this._viewMode);
+ const nextMode = VIEW_MODES[(currentIndex + 1) % VIEW_MODES.length];
+ await this.setViewMode(nextMode);
+ return nextMode;
+ }
+
+ async setFilterMode(mode) {
+ this._filterMode = mode || null;
+ await this._updateContexts();
+ this._rebuildSummary();
+ this.refresh();
+ }
+
+ getFilterMode() {
+ return this._filterMode;
+ }
+
+ async clearFilter() {
+ await this.setFilterMode(null);
+ }
+
+ setSortMode(mode) {
+ if (!Object.values(SORT_MODES).includes(mode)) {
+ return;
+ }
+
+ this._sortMode = mode;
+ this._rebuildSummary();
+ this.refresh();
+ }
+
+ getSortMode() {
+ return this._sortMode;
+ }
+
+ getReportData() {
+ return this._reportData;
+ }
+
+ async _storeReportData(scanDate) {
+ this._lastScanTimestamp = normalizeReportTimestamp(scanDate);
+ this._reportData = buildComplianceReportData(
+ path.basename(this.getProjectFolder() || "workspace"),
+ this._fullTrees.flatMap((tree) => tree.dependencies),
+ { scanDate: this._lastScanTimestamp }
+ );
+ await this._updateContexts();
}
- /**
- * Get the project folder to scan. Returns a path string.
- * Priority: manually selected folder > first VS Code workspace folder > null.
- */
getProjectFolder() {
if (this._projectFolderPath) {
return this._projectFolderPath;
}
const folders = vscode.workspace.workspaceFolders;
- if (folders && folders.length > 0) {
- return folders[0].uri.fsPath;
- }
- return null;
+ return folders && folders[0] ? folders[0].uri.fsPath : null;
}
- /**
- * Set a manually picked project folder.
- */
setProjectFolder(folderPath) {
this._projectFolderPath = folderPath;
}
- /**
- * Prompt the user to pick a folder when no workspace is open.
- * Returns the selected path or null if cancelled.
- */
async promptForFolder() {
const choice = await vscode.window.showQuickPick(
[
- { label: "$(folder-opened) Select a folder to scan", description: "Browse for a project folder", _action: "pick" },
- { label: "$(folder) Open a project folder", description: "Open a folder in VS Code", _action: "open" },
+ {
+ label: "$(folder-opened) Select a folder to scan",
+ description: "Browse for a project folder",
+ _action: "pick",
+ },
+ {
+ label: "$(folder) Open a project folder",
+ description: "Open a folder in VS Code",
+ _action: "open",
+ },
],
{ placeHolder: "No workspace folder is open. Select a project folder to scan." }
);
@@ -77,45 +230,35 @@ class DependencyHealthProvider {
if (choice._action === "open") {
await vscode.commands.executeCommand("vscode.openFolder");
- return null; // VS Code will reload, scan can happen after
+ return null;
}
- // Folder picker dialog
const selected = await vscode.window.showOpenDialog({
canSelectFolders: true,
canSelectFiles: false,
canSelectMany: false,
- openLabel: "Scan for dependencies",
+ openLabel: "Scan dependencies",
});
if (!selected || selected.length === 0) {
return null;
}
- const folderPath = selected[0].fsPath;
- this._projectFolderPath = folderPath;
- return folderPath;
+ this._projectFolderPath = selected[0].fsPath;
+ return this._projectFolderPath;
}
- /**
- * Scan project manifests and cross-reference against Cloudsmith.
- *
- * @param {string} cloudsmithWorkspace Workspace/owner slug.
- * @param {string|null} cloudsmithRepo Optional repo slug for scoped scan.
- * @param {string|null} projectFolder Optional project folder path override.
- */
async scan(cloudsmithWorkspace, cloudsmithRepo, projectFolder) {
if (this._scanning) {
vscode.window.showWarningMessage("A dependency scan is already in progress.");
return;
}
- // Resolve project folder
let folderPath = projectFolder || this.getProjectFolder();
if (!folderPath) {
folderPath = await this.promptForFolder();
if (!folderPath) {
- return; // User cancelled
+ return;
}
}
@@ -123,24 +266,28 @@ class DependencyHealthProvider {
this._hasScannedOnce = true;
this.lastWorkspace = cloudsmithWorkspace;
this.lastRepo = cloudsmithRepo;
- this.dependencies = [];
- this._statusMessage = "Scanning project manifests...";
+ this._reportData = null;
+ this._failureMessage = null;
+ this._warnings = [];
+ this._noManifestsFolder = null;
+ this._statusMessage = "Parsing lockfiles...";
+ this._displayTrees = [];
+ this._fullTrees = [];
+ this._summary = emptySummary();
+ await this._updateContexts();
this.refresh();
const cancellationSource = new vscode.CancellationTokenSource();
try {
- const scanResult = await vscode.window.withProgress(
+ const result = await vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
- title: "Scanning dependency health",
+ title: "Scanning dependencies",
cancellable: true,
},
async (progress, token) => {
- const tokenSubscription = token.onCancellationRequested(() => {
- cancellationSource.cancel();
- });
-
+ const subscription = token.onCancellationRequested(() => cancellationSource.cancel());
try {
return await this._performScan(
cloudsmithWorkspace,
@@ -150,23 +297,23 @@ class DependencyHealthProvider {
cancellationSource.token
);
} finally {
- tokenSubscription.dispose();
+ subscription.dispose();
}
}
);
- if (scanResult && scanResult.canceled) {
+ if (result && result.canceled) {
this._statusMessage = null;
if (this._diagnosticsPublisher) {
this._diagnosticsPublisher.clear();
}
vscode.window.showInformationMessage("Dependency scan canceled.");
}
- } catch (e) {
- const reason = e && e.message
- ? e.message
- : "Check the Cloudsmith connection.";
- this.dependencies = [];
+ } catch (error) {
+ const reason = error && error.message ? error.message : "Check the Cloudsmith connection.";
+ this._displayTrees = [];
+ this._fullTrees = [];
+ this._summary = emptySummary();
if (this._diagnosticsPublisher) {
this._diagnosticsPublisher.clear();
}
@@ -176,374 +323,2509 @@ class DependencyHealthProvider {
} finally {
cancellationSource.dispose();
this._scanning = false;
+ await this._updateContexts();
this.refresh();
}
}
_getMaxDependenciesToScan() {
- const config = vscode.workspace.getConfiguration("cloudsmith-vsc");
- const configuredValue = Number(config.get("maxDependenciesToScan"));
+ const configuredValue = Number(vscode.workspace.getConfiguration("cloudsmith-vsc").get("maxDependenciesToScan"));
if (!Number.isFinite(configuredValue) || configuredValue < 1) {
return DEFAULT_MAX_DEPENDENCIES_TO_SCAN;
}
return Math.floor(configuredValue);
}
- async _waitForCancellationOrTimeout(token, ms) {
- if (token && token.isCancellationRequested) {
- return true;
- }
+ async _performScan(cloudsmithWorkspace, cloudsmithRepo, folderPath, progress, token) {
+ progress.report({ message: "Parsing lockfiles..." });
+ this._lastManifests = await ManifestParser.detectManifests(folderPath);
- return new Promise(resolve => {
- let subscription = null;
- const timer = setTimeout(() => {
- if (subscription) {
- subscription.dispose();
- }
- resolve(false);
- }, ms);
+ const resolveTransitives = vscode.workspace.getConfiguration("cloudsmith-vsc").get("resolveTransitiveDependencies") !== false;
+ const trees = [];
+ const warnings = [];
- if (token && typeof token.onCancellationRequested === "function") {
- subscription = token.onCancellationRequested(() => {
- clearTimeout(timer);
- if (subscription) {
- subscription.dispose();
+ if (resolveTransitives) {
+ const detections = await LockfileResolver.detectResolvers(folderPath);
+ for (const detection of detections) {
+ if (token.isCancellationRequested) {
+ return { canceled: true };
+ }
+ try {
+ const tree = await LockfileResolver.resolve(
+ detection.resolverName,
+ detection.lockfilePath,
+ detection.manifestPath,
+ {
+ workspaceFolder: folderPath,
+ maxDependenciesToScan: this._getMaxDependenciesToScan(),
+ }
+ );
+ if (tree) {
+ trees.push(tree);
+ if (Array.isArray(tree.warnings) && tree.warnings.length > 0) {
+ warnings.push(...tree.warnings);
+ }
}
- resolve(true);
- });
+ } catch (error) {
+ warnings.push(error && error.message ? error.message : "A lockfile parser failed.");
+ }
}
- });
- }
-
- _isExactDependencyMatch(pkg, expected) {
- if (!pkg || typeof pkg !== "object" || !expected) {
- return false;
}
- if (String(pkg.name || "") !== String(expected.name || "")) {
- return false;
- }
- if (String(pkg.format || "") !== String(expected.format || "")) {
- return false;
+ if (trees.length === 0) {
+ const fallbackTrees = await this._buildManifestFallbackTrees(this._lastManifests);
+ trees.push(...fallbackTrees);
}
- // When the declared version is empty/null/undefined, treat as a name-only
- // match so we select the newest package from the query results.
- if (expected.version) {
- if (String(pkg.version || "") !== String(expected.version)) {
- return false;
+
+ if (trees.length === 0) {
+ this._displayTrees = [];
+ this._fullTrees = [];
+ this._summary = emptySummary();
+ this._statusMessage = null;
+ if (this._lastManifests.length === 0) {
+ this._noManifestsFolder = path.basename(folderPath);
}
+ await this._storeReportData(this._reportDateFactory());
+ return { canceled: false };
}
- return true;
- }
- _exactDependencyMatch(pkg, deps) {
- if (!pkg || !Array.isArray(deps)) {
- return null;
+ const normalizedTrees = trees
+ .map(normalizeTree)
+ .filter((tree) => Array.isArray(tree.dependencies) && tree.dependencies.length > 0);
+
+ if (normalizedTrees.length === 0) {
+ this._displayTrees = [];
+ this._fullTrees = [];
+ this._summary = emptySummary();
+ this._statusMessage = null;
+ await this._storeReportData(this._reportDateFactory());
+ return { canceled: false };
}
- return deps.find(dep => this._isExactDependencyMatch(pkg, dep)) || null;
- }
+ this._noManifestsFolder = null;
+ this._fullTrees = markTreesAsChecking(normalizedTrees);
+
+ const limited = limitDisplayTrees(this._fullTrees, this._getMaxDependenciesToScan());
+ this._displayTrees = limited.trees;
+ this._warnings = warnings.slice();
+ if (limited.truncated) {
+ const warning = `Dependency display is capped at ${this._getMaxDependenciesToScan()} items `
+ + `out of ${limited.totalDependencies} resolved dependencies.`;
+ this._warnings.push(warning);
+ vscode.window.showWarningMessage(warning);
+ }
+ this._statusMessage = null;
+ this._rebuildSummary();
+ this.refresh();
- async _performScan(cloudsmithWorkspace, cloudsmithRepo, folderPath, progress, token) {
- progress.report({ message: "Detecting manifests", increment: 10 });
+ const totalCoverageDependencies = countCoverageDependencies(this._fullTrees);
+ progress.report({
+ message: `Found ${limited.totalDependencies} dependencies. Fetching package index...`,
+ });
- let allDeps = [];
- this._lastManifests = [];
+ await this._runCoverageChecks(
+ cloudsmithWorkspace,
+ cloudsmithRepo,
+ totalCoverageDependencies,
+ progress,
+ token
+ );
- // Scan the single resolved folder path (not workspace folders)
if (token.isCancellationRequested) {
return { canceled: true };
}
- const manifests = await ManifestParser.detectManifests(folderPath);
+ progress.report({
+ message: "Enriching vulnerabilities, licenses, policy, and upstream availability...",
+ });
+
+ await this._runEnrichmentPasses(cloudsmithWorkspace, cloudsmithRepo, progress, token);
+
if (token.isCancellationRequested) {
return { canceled: true };
}
- this._lastManifests = manifests;
+ await this._publishDiagnostics();
+ this._rebuildSummary();
+ await this._storeReportData(this._reportDateFactory());
+ return { canceled: false };
+ }
- if (manifests.length === 0) {
- // No manifests found — set a descriptive message
- const folderName = path.basename(folderPath);
- this.dependencies = [];
- this._statusMessage = null;
- this._noManifestsFolder = folderName;
- return { canceled: false };
+ async _buildManifestFallbackTrees(manifests) {
+ const trees = [];
+ for (const manifest of manifests) {
+ const parsed = await ManifestParser.parseManifest(manifest);
+ if (!Array.isArray(parsed) || parsed.length === 0) {
+ continue;
+ }
+ trees.push({
+ ecosystem: manifest.format,
+ sourceFile: path.basename(manifest.filePath),
+ dependencies: parsed.map((dependency) => ({
+ name: dependency.name,
+ version: dependency.version,
+ ecosystem: manifest.format,
+ format: canonicalFormat(manifest.format),
+ isDirect: true,
+ parent: null,
+ parentChain: [],
+ transitives: [],
+ cloudsmithStatus: "CHECKING",
+ cloudsmithPackage: null,
+ sourceFile: path.basename(manifest.filePath),
+ devDependency: Boolean(dependency.devDependency),
+ isDevelopmentDependency: Boolean(dependency.devDependency),
+ })),
+ });
}
+ return trees;
+ }
- for (const manifest of manifests) {
- if (token.isCancellationRequested) {
- return { canceled: true };
+ async _runCoverageChecks(cloudsmithWorkspace, cloudsmithRepo, totalDependencies, progress, token) {
+ const dependenciesByFormat = groupDependenciesByFormat(this._fullTrees);
+ await this._runCoverageResolution(
+ cloudsmithWorkspace,
+ cloudsmithRepo,
+ dependenciesByFormat,
+ totalDependencies,
+ progress,
+ token,
+ {
+ packageIndexFailureVerb: "fetch",
+ progressLabel: "Matching coverage",
}
+ );
+ }
- const parsed = await ManifestParser.parseManifest(manifest);
- allDeps = allDeps.concat(parsed);
+ async _runCoverageResolution(
+ cloudsmithWorkspace,
+ cloudsmithRepo,
+ dependenciesByFormat,
+ totalDependencies,
+ progress,
+ token,
+ options = {}
+ ) {
+ const formats = Object.keys(dependenciesByFormat);
+ const progressLabel = options.progressLabel || "Matching coverage";
+ const packageIndexFailureVerb = options.packageIndexFailureVerb || "fetch";
+
+ if (formats.length === 0 || totalDependencies === 0) {
+ return 0;
}
- if (allDeps.length === 0) {
- this._statusMessage = null;
- this._noManifestsFolder = path.basename(folderPath);
- return { canceled: false };
- }
+ const indexEntries = await Promise.all(
+ formats.map(async (format) => ({
+ format,
+ dependencies: uniqueDependenciesForCoverage(dependenciesByFormat[format]),
+ packageIndex: await this._fetchPackageIndex(cloudsmithWorkspace, cloudsmithRepo, format),
+ }))
+ );
- // Clear the no-manifests flag since we found deps
- this._noManifestsFolder = null;
+ let completed = 0;
+ for (const { format, dependencies, packageIndex } of indexEntries) {
+ if (token.isCancellationRequested) {
+ return completed;
+ }
- progress.report({ message: "Resolving manifests", increment: 15 });
+ if (packageIndex.error) {
+ this._warnings.push(`Could not ${packageIndexFailureVerb} the ${format} package index. ${packageIndex.error}`);
+ }
- const resolveConfig = vscode.workspace.getConfiguration("cloudsmith-vsc");
- if (resolveConfig.get("resolveTransitiveDependencies")) {
- this._statusMessage = "Resolving transitive dependencies via CLI...";
- this.refresh();
+ if (packageIndex.tooLarge || packageIndex.error) {
+ completed = await this._resolveCoverageWithFallbackQueries(
+ cloudsmithWorkspace,
+ cloudsmithRepo,
+ format,
+ dependencies,
+ completed,
+ totalDependencies,
+ progress,
+ token,
+ progressLabel
+ );
+ continue;
+ }
- const directNames = new Set(allDeps.map(d => d.name));
- const formatsResolved = new Set();
- const formats = [...new Set(this._lastManifests.map(m => m.format))];
+ completed = await this._matchCoverageBatch(
+ dependencies,
+ packageIndex.index,
+ completed,
+ totalDependencies,
+ progress,
+ token,
+ progressLabel
+ );
+ }
- for (const format of formats) {
- if (token.isCancellationRequested) {
- return { canceled: true };
- }
- if (formatsResolved.has(format)) {
- continue;
- }
- try {
- const transitiveDeps = await TransitiveResolver.resolve(folderPath, format);
- if (token.isCancellationRequested) {
- return { canceled: true };
- }
- if (transitiveDeps && transitiveDeps.length > 0) {
- for (const dep of transitiveDeps) {
- dep.isDirect = directNames.has(dep.name);
- }
- allDeps = allDeps.filter(d => d.format !== format);
- allDeps = allDeps.concat(transitiveDeps);
- formatsResolved.add(format);
- }
- } catch (e) {
- vscode.window.showWarningMessage(
- `Could not resolve transitive dependencies for ${format}. Using direct dependencies only. ${e.message}`
- );
- }
+ return completed;
+ }
+
+ async _matchCoverageBatch(dependencies, packageIndex, completed, totalDependencies, progress, token, progressLabel) {
+ const pendingMatches = [];
+
+ for (let index = 0; index < dependencies.length; index += 1) {
+ if (token.isCancellationRequested) {
+ return completed;
}
- }
- const seen = new Set();
- const uniqueDeps = [];
- for (const dep of allDeps) {
- const key = `${dep.format}:${dep.name}`;
- if (!seen.has(key)) {
- seen.add(key);
- uniqueDeps.push(dep);
+ const dependency = dependencies[index];
+ pendingMatches.push({
+ dependency,
+ match: findCoverageMatch(packageIndex, dependency),
+ });
+
+ if (pendingMatches.length < COVERAGE_MATCH_BATCH_SIZE && index < dependencies.length - 1) {
+ continue;
}
- }
- const maxDependenciesToScan = this._getMaxDependenciesToScan();
- const depsToScan = uniqueDeps.slice(0, maxDependenciesToScan);
- if (uniqueDeps.length > depsToScan.length) {
- vscode.window.showWarningMessage(
- `Dependency scan truncated to ${depsToScan.length} dependencies out of ${uniqueDeps.length}. Increase cloudsmith-vsc.maxDependenciesToScan to scan more.`
+ completed = await this._flushCoverageMatchBatch(
+ pendingMatches,
+ completed,
+ totalDependencies,
+ progress,
+ progressLabel
);
}
- this._statusMessage = `Found ${uniqueDeps.length} dependencies. Checking ${depsToScan.length} against Cloudsmith...`;
+ return completed;
+ }
+
+ async _flushCoverageMatchBatch(pendingMatches, completed, totalDependencies, progress, progressLabel) {
+ if (pendingMatches.length === 0) {
+ return completed;
+ }
+
+ this._applyCoverageMatchBatch(pendingMatches);
+
+ const batchSize = pendingMatches.length;
+ pendingMatches.length = 0;
+ completed += batchSize;
+
+ this._rebuildSummary();
+ progress.report({
+ message: `${progressLabel}... ${completed}/${totalDependencies}`,
+ increment: totalDependencies > 0 ? (batchSize * 100) / totalDependencies : 100,
+ });
this.refresh();
+ await yieldToEventLoop();
+
+ return completed;
+ }
- progress.report({ message: "Checking Cloudsmith", increment: 20 });
+ _applyCoverageMatchBatch(matches) {
+ if (!Array.isArray(matches) || matches.length === 0) {
+ return;
+ }
- const byFormat = {};
- for (const dep of depsToScan) {
- if (!byFormat[dep.format]) {
- byFormat[dep.format] = [];
- }
- byFormat[dep.format].push(dep);
+ const matchMap = new Map();
+ for (const { dependency, match } of matches) {
+ matchMap.set(coverageLookupKey(dependency), {
+ cloudsmithStatus: match ? "FOUND" : "NOT_FOUND",
+ cloudsmithPackage: match || null,
+ ...(match ? { upstreamStatus: null, upstreamDetail: null } : {}),
+ });
}
- const cloudsmithAPI = new CloudsmithAPI(this.context);
- const allResults = new Map();
+ this._fullTrees = applyCoverageMatchBatchToTrees(this._fullTrees, matchMap);
+ this._displayTrees = applyCoverageMatchBatchToTrees(this._displayTrees, matchMap);
+ }
- for (const [format, deps] of Object.entries(byFormat)) {
- for (let i = 0; i < deps.length; i += BATCH_SIZE) {
- if (token.isCancellationRequested) {
- return { canceled: true };
+ _createDebouncedEnrichmentHandler(patchApplier) {
+ let pendingPatchMaps = [];
+ let flushTimeout = null;
+
+ const flush = () => {
+ if (flushTimeout) {
+ clearTimeout(flushTimeout);
+ flushTimeout = null;
+ }
+
+ if (pendingPatchMaps.length === 0) {
+ return;
+ }
+
+ const mergedPatchMap = mergePatchMaps(pendingPatchMaps);
+ pendingPatchMaps = [];
+
+ patchApplier(mergedPatchMap);
+ this._rebuildSummary();
+ this.refresh();
+ };
+
+ return {
+ onProgress: (patchMap) => {
+ if (!(patchMap instanceof Map) || patchMap.size === 0) {
+ return;
+ }
+
+ pendingPatchMaps.push(patchMap);
+ if (!flushTimeout) {
+ flushTimeout = setTimeout(() => {
+ flushTimeout = null;
+ flush();
+ }, ENRICHMENT_PROGRESS_DEBOUNCE_MS);
}
+ },
+ flush,
+ };
+ }
- const batch = deps.slice(i, i + BATCH_SIZE);
- const nameTerms = batch.map(d => `name:^${d.name}$`).join(" OR ");
- const query = `(${nameTerms}) AND format:${format}`;
- const baseEndpoint = cloudsmithRepo
- ? `packages/${cloudsmithWorkspace}/${cloudsmithRepo}/`
- : `packages/${cloudsmithWorkspace}/`;
- const endpoint = `${baseEndpoint}?query=${encodeURIComponent(query)}&sort=-version&page_size=${BATCH_SIZE * 3}`;
+ async _resolveCoverageWithFallbackQueries(
+ cloudsmithWorkspace,
+ cloudsmithRepo,
+ format,
+ dependencies,
+ completed,
+ totalDependencies,
+ progress,
+ token,
+ progressLabel = "Matching coverage"
+ ) {
+ const api = new CloudsmithAPI(this.context);
+ const endpoint = cloudsmithRepo
+ ? `packages/${cloudsmithWorkspace}/${cloudsmithRepo}/`
+ : `packages/${cloudsmithWorkspace}/`;
+ const uniqueDependencies = dependencies.slice();
+
+ for (let index = 0; index < uniqueDependencies.length; index += COVERAGE_MATCH_BATCH_SIZE) {
+ if (token.isCancellationRequested) {
+ return completed;
+ }
- const result = await cloudsmithAPI.get(endpoint);
+ const dependencyBatch = uniqueDependencies.slice(index, index + COVERAGE_MATCH_BATCH_SIZE);
+ const pendingMatches = [];
+ await runPromisePool(dependencyBatch, FALLBACK_QUERY_CONCURRENCY, async (dependency) => {
if (token.isCancellationRequested) {
- return { canceled: true };
+ return;
}
- if (typeof result === "string" && result.includes("429")) {
- this._statusMessage = "Rate limited. Pausing scan for 30 seconds...";
- this.refresh();
- const cancelledDuringBackoff = await this._waitForCancellationOrTimeout(token, 30000);
- if (cancelledDuringBackoff) {
- return { canceled: true };
- }
-
- const retry = await cloudsmithAPI.get(endpoint);
- if (token.isCancellationRequested) {
- return { canceled: true };
+ let match = null;
+ for (const lookupName of getPackageLookupKeys(dependency.name, dependency.format)) {
+ const query = new SearchQueryBuilder()
+ .format(dependency.format)
+ .name(lookupName)
+ .build();
+ const result = await api.get(`${endpoint}?query=${encodeURIComponent(query)}&page_size=${FALLBACK_QUERY_PAGE_SIZE}`);
+ if (typeof result === "string") {
+ this._warnings.push(`Coverage lookup failed for ${dependency.name}. ${result}`);
+ continue;
}
- if (typeof retry === "string") {
- throw new Error(
- `Dependency lookup failed after retry for ${format}: ${retry}`
- );
- }
- if (Array.isArray(retry)) {
- for (const pkg of retry) {
- const matchingDep = this._exactDependencyMatch(pkg, batch);
- if (matchingDep) {
- const mapKey = `${matchingDep.format}:${matchingDep.name}`;
- if (!allResults.has(mapKey) || pkg.version > allResults.get(mapKey).version) {
- allResults.set(mapKey, pkg);
- }
- }
- }
- }
- } else if (typeof result === "string") {
- throw new Error(`Dependency lookup failed for ${format}: ${result}`);
- } else if (Array.isArray(result)) {
- for (const pkg of result) {
- const matchingDep = this._exactDependencyMatch(pkg, batch);
- if (matchingDep) {
- const mapKey = `${matchingDep.format}:${matchingDep.name}`;
- if (!allResults.has(mapKey) || pkg.version > allResults.get(mapKey).version) {
- allResults.set(mapKey, pkg);
- }
+ if (Array.isArray(result) && result.length > 0) {
+ match = matchCoverageCandidates(result, dependency);
+ if (match) {
+ break;
}
}
}
- for (const dep of batch) {
- const match = allResults.get(`${dep.format}:${dep.name}`) || null;
- this.dependencies.push(
- new DependencyHealthNode(dep, match, this.context)
- );
- }
+ pendingMatches.push({ dependency, match });
+ });
- this.dependencies.sort((a, b) => a.sortOrder - b.sortOrder);
+ completed = await this._flushCoverageMatchBatch(
+ pendingMatches,
+ completed,
+ totalDependencies,
+ progress,
+ progressLabel
+ );
- this._statusMessage = `Checked ${Math.min(i + BATCH_SIZE, deps.length)} of ${deps.length} ${format} dependencies...`;
- progress.report({ message: `Checked ${Math.min(i + BATCH_SIZE, deps.length)} of ${deps.length} ${format} dependencies...` });
- this.refresh();
+ if (token.isCancellationRequested) {
+ return completed;
}
}
- this.dependencies.sort((a, b) => a.sortOrder - b.sortOrder);
+ return completed;
+ }
- if (this._diagnosticsPublisher) {
- await this._diagnosticsPublisher.publish(this._lastManifests, this.dependencies);
+ async _fetchPackageIndex(cloudsmithWorkspace, cloudsmithRepo, format) {
+ const cacheKey = `${String(cloudsmithWorkspace || "").toLowerCase()}:${String(cloudsmithRepo || "").toLowerCase()}:${format}`;
+ const cachedValue = getCachedPackageIndexValue(cacheKey);
+ if (cachedValue) {
+ return cachedValue;
}
- this._statusMessage = null;
- return { canceled: false };
+ const firstPage = await this._fetchSinglePage(
+ cloudsmithWorkspace,
+ cloudsmithRepo,
+ format,
+ 1,
+ PACKAGE_INDEX_PAGE_SIZE
+ );
+ if (firstPage.error) {
+ const value = { error: firstPage.error, tooLarge: false, index: new Map() };
+ setCachedPackageIndexValue(cacheKey, value);
+ return value;
+ }
+
+ const totalCount = firstPage.pagination.count || firstPage.data.length;
+ if (totalCount > PACKAGE_INDEX_FALLBACK_THRESHOLD) {
+ const value = { error: null, tooLarge: true, index: new Map(), totalCount };
+ setCachedPackageIndexValue(cacheKey, value);
+ return value;
+ }
+
+ const packages = [...firstPage.data];
+ const pageTotal = firstPage.pagination && firstPage.pagination.pageTotal
+ ? firstPage.pagination.pageTotal
+ : Math.ceil(totalCount / PACKAGE_INDEX_PAGE_SIZE) || 1;
+
+ if (pageTotal > 1) {
+ const remainingPages = await Promise.all(
+ Array.from({ length: pageTotal - 1 }, (_, index) => this._fetchSinglePage(
+ cloudsmithWorkspace,
+ cloudsmithRepo,
+ format,
+ index + 2,
+ PACKAGE_INDEX_PAGE_SIZE
+ ))
+ );
+
+ for (const nextPage of remainingPages) {
+ if (nextPage.error) {
+ const value = { error: nextPage.error, tooLarge: false, index: new Map() };
+ setCachedPackageIndexValue(cacheKey, value);
+ return value;
+ }
+ packages.push(...nextPage.data);
+ }
+ }
+
+ const value = {
+ error: null,
+ tooLarge: false,
+ index: buildPackageIndex(packages, format),
+ totalCount,
+ };
+ setCachedPackageIndexValue(cacheKey, value);
+ return value;
}
- /** Re-run the last scan with same settings. */
- async rescan() {
- if (this.lastWorkspace) {
- await this.scan(this.lastWorkspace, this.lastRepo);
- } else {
- vscode.window.showInformationMessage('No previous scan. Run "Scan dependencies" first.');
+ async _fetchSinglePage(cloudsmithWorkspace, cloudsmithRepo, format, page, pageSize) {
+ const api = new CloudsmithAPI(this.context);
+ const paginatedFetch = new PaginatedFetch(api);
+ const endpoint = cloudsmithRepo
+ ? `packages/${cloudsmithWorkspace}/${cloudsmithRepo}/`
+ : `packages/${cloudsmithWorkspace}/`;
+
+ return paginatedFetch.fetchPage(endpoint, page, pageSize, `format:${format}`);
+ }
+
+ async _runEnrichmentPasses(cloudsmithWorkspace, cloudsmithRepo, progress, token) {
+ const dependencies = this._fullTrees.flatMap((tree) => tree.dependencies);
+ const tasks = [
+ this._runVulnerabilityEnrichment(dependencies, cloudsmithWorkspace, progress, token),
+ this._runLicenseEnrichment(dependencies, token),
+ this._runPolicyEnrichment(dependencies, token),
+ ];
+
+ const uncoveredDependencies = dependencies.filter((dependency) => dependency.cloudsmithStatus === "NOT_FOUND");
+ if (uncoveredDependencies.length > 0) {
+ tasks.push(this._runUpstreamGapAnalysis(uncoveredDependencies, cloudsmithWorkspace, cloudsmithRepo, progress, token));
+ }
+
+ const results = await Promise.allSettled(tasks);
+ for (const result of results) {
+ if (result.status !== "rejected") {
+ continue;
+ }
+
+ const message = result.reason && result.reason.message
+ ? result.reason.message
+ : String(result.reason || "An enrichment step failed.");
+ this._warnings.push(message);
}
}
- getTreeItem(element) {
- return element.getTreeItem();
+ async _runVulnerabilityEnrichment(dependencies, workspace, progress, token) {
+ const handler = this._createDebouncedEnrichmentHandler((patchMap) => {
+ this._fullTrees = applyFoundOverlayPatch(this._fullTrees, patchMap, (dependency, vulnerabilities) => ({
+ ...dependency,
+ vulnerabilities,
+ }));
+ this._displayTrees = applyFoundOverlayPatch(this._displayTrees, patchMap, (dependency, vulnerabilities) => ({
+ ...dependency,
+ vulnerabilities,
+ }));
+ });
+
+ try {
+ await this._services.enrichVulnerabilities(dependencies, workspace, {
+ context: this.context,
+ cancellationToken: token,
+ onProgress: (patchMap, meta = {}) => {
+ if (meta.total > 0) {
+ progress.report({
+ message: `Loading vulnerability details... ${meta.completed}/${meta.total}`,
+ });
+ }
+ handler.onProgress(patchMap);
+ },
+ });
+ } finally {
+ handler.flush();
+ }
}
- // IMPORTANT: Connection status is checked live from context.secrets every render.
- // Do NOT cache this value or rely on external refresh calls to set a connection flag.
- // This pattern was adopted after three regressions caused by refresh wiring changes.
- async getChildren(element) {
- if (element) {
- return element.getChildren();
+ async _runLicenseEnrichment(dependencies, token) {
+ const handler = this._createDebouncedEnrichmentHandler((patchMap) => {
+ this._fullTrees = applyFoundOverlayPatch(this._fullTrees, patchMap, (dependency, license) => ({
+ ...dependency,
+ license,
+ }));
+ this._displayTrees = applyFoundOverlayPatch(this._displayTrees, patchMap, (dependency, license) => ({
+ ...dependency,
+ license,
+ }));
+ });
+
+ try {
+ await this._services.enrichLicenses(dependencies, {
+ cancellationToken: token,
+ onProgress: (patchMap) => {
+ handler.onProgress(patchMap);
+ },
+ });
+ } finally {
+ handler.flush();
}
+ }
- // Show progress message while scanning
- if (this._statusMessage) {
- return [new InfoNode(
- this._statusMessage,
- "",
- this._statusMessage,
- "sync~spin",
- "statusMessage"
- )];
+ async _runPolicyEnrichment(dependencies, token) {
+ const handler = this._createDebouncedEnrichmentHandler((patchMap) => {
+ this._fullTrees = applyFoundOverlayPatch(this._fullTrees, patchMap, (dependency, policy) => ({
+ ...dependency,
+ policy,
+ }));
+ this._displayTrees = applyFoundOverlayPatch(this._displayTrees, patchMap, (dependency, policy) => ({
+ ...dependency,
+ policy,
+ }));
+ });
+
+ try {
+ await this._services.enrichPolicies(dependencies, {
+ cancellationToken: token,
+ onProgress: (patchMap) => {
+ handler.onProgress(patchMap);
+ },
+ });
+ } finally {
+ handler.flush();
}
+ }
- // Show failure message if last scan failed
- if (this._failureMessage) {
- return [new InfoNode(
- this._failureMessage,
- "",
- this._failureMessage,
- "error",
- "statusMessage"
- )];
+ async _runUpstreamGapAnalysis(uncoveredDependencies, workspace, repo, progress, token) {
+ const repositories = repo
+ ? [repo]
+ : await this._services.fetchRepositories(workspace, token);
+
+ const handler = this._createDebouncedEnrichmentHandler((patchMap) => {
+ this._fullTrees = applyUncoveredOverlayPatch(this._fullTrees, patchMap, (dependency, gap) => ({
+ ...dependency,
+ upstreamStatus: gap.upstreamStatus,
+ upstreamDetail: gap.upstreamDetail,
+ }));
+ this._displayTrees = applyUncoveredOverlayPatch(this._displayTrees, patchMap, (dependency, gap) => ({
+ ...dependency,
+ upstreamStatus: gap.upstreamStatus,
+ upstreamDetail: gap.upstreamDetail,
+ }));
+ });
+
+ try {
+ await this._services.analyzeUpstreamGaps(uncoveredDependencies, workspace, repositories, {
+ context: this.context,
+ cancellationToken: token,
+ onProgress: (patchMap, meta = {}) => {
+ if (meta.total > 0) {
+ progress.report({
+ message: `Checking upstream coverage... ${meta.completed}/${meta.total}`,
+ });
+ }
+ handler.onProgress(patchMap);
+ },
+ });
+ } finally {
+ handler.flush();
}
+ }
- // Show "no manifests found" state
- if (this._noManifestsFolder) {
- return [new InfoNode(
- "No dependency manifests found",
- this._noManifestsFolder,
- "Supported formats: package.json, requirements.txt, pyproject.toml, pom.xml, go.mod, Cargo.toml",
- "warning",
- "infoNode"
- )];
+ async _fetchWorkspaceRepositories(workspace, token) {
+ const api = new CloudsmithAPI(this.context);
+ const paginatedFetch = new PaginatedFetch(api);
+ const endpoint = `repos/${workspace}/?sort=name`;
+ const repositories = [];
+ let page = 1;
+
+ while (!token || !token.isCancellationRequested) {
+ const result = await paginatedFetch.fetchPage(endpoint, page, WORKSPACE_REPOSITORY_PAGE_SIZE);
+ if (result.error) {
+ this._warnings.push(`Could not load repositories for upstream analysis. ${result.error}`);
+ break;
+ }
+
+ for (const repository of Array.isArray(result.data) ? result.data : []) {
+ if (repository && repository.slug) {
+ repositories.push(repository.slug);
+ }
+ }
+
+ const pageTotal = result.pagination && result.pagination.pageTotal
+ ? result.pagination.pageTotal
+ : 1;
+ if (page >= pageTotal) {
+ break;
+ }
+ page += 1;
}
- // Show results if we have them
- if (this.dependencies.length > 0) {
- return this.dependencies;
+ return [...new Set(repositories)];
+ }
+
+ async _publishDiagnostics() {
+ if (!this._diagnosticsPublisher) {
+ return;
}
- // Welcome state — no scan has been run yet
- if (!this._hasScannedOnce) {
- const isConnected = await this.context.secrets.get("cloudsmith-vsc.isConnected");
- if (isConnected !== "true") {
- return [new InfoNode(
- "Connect to Cloudsmith",
- "Use the key icon above to set up a personal or service account API key, CLI import, or SSO.",
- "Set up Cloudsmith authentication to get started.",
- "plug",
- undefined,
- { command: "cloudsmith-vsc.configureCredentials", title: "Set up authentication" }
- )];
- }
- // Connected but no scan run yet
- return [new InfoNode(
- "Scan project dependencies",
- "Select the play button above to start.",
- "Reads local manifest files (package.json, requirements.txt, pyproject.toml, pom.xml, go.mod, Cargo.toml) and checks each dependency against the selected Cloudsmith workspace.",
- "folder",
- "dependencyHealthWelcome"
- )];
- }
-
- // Scan completed but no dependencies were found in the manifests
- return [new InfoNode(
- "No dependencies found",
- "",
- "The manifest files were parsed but contained no dependency entries.",
- "info",
- "infoNode"
- )];
+ const diagnosticNodes = this._fullTrees
+ .flatMap((tree) => tree.dependencies)
+ .filter((dependency) => dependency.isDirect)
+ .map((dependency) => new DependencyHealthNode(dependency, null, this.context));
+
+ await this._diagnosticsPublisher.publish(this._lastManifests, diagnosticNodes);
+ }
+
+ buildDependencyNodesForTree(tree) {
+ if (this._viewMode === "tree") {
+ return this._buildTreeModeNodes(tree);
+ }
+
+ return this._buildListModeNodes(tree);
+ }
+
+ _buildListModeNodes(tree) {
+ const visibleDependencies = this._viewMode === "direct"
+ ? tree.dependencies.filter((dependency) => dependency.isDirect)
+ : tree.dependencies.slice();
+
+ return visibleDependencies
+ .filter((dependency) => matchesFilter(dependency, this._filterMode))
+ .sort((left, right) => compareDependencies(left, right, this._sortMode, true))
+ .map((dependency) => new DependencyHealthNode(
+ dependency,
+ null,
+ this.context,
+ { childMode: "details" }
+ ));
+ }
+
+ _buildTreeModeNodes(tree) {
+ const roots = getTreeRootDependencies(tree)
+ .sort((left, right) => compareDependencies(left, right, this._sortMode, false));
+
+ const filteredRoots = roots
+ .map((dependency) => buildFilteredTreeWrapper(dependency, this._filterMode, this._sortMode))
+ .filter(Boolean);
+
+ const duplicateAwareRoots = annotateDuplicateWrappers(filteredRoots, new Map(), []);
+ return duplicateAwareRoots.map((wrapper) => this._createTreeDependencyNode(wrapper));
+ }
+
+ _createTreeDependencyNode(wrapper) {
+ return new DependencyHealthNode(
+ wrapper.dependency,
+ null,
+ this.context,
+ {
+ childMode: "tree",
+ treeChildren: wrapper.children,
+ duplicateReference: wrapper.duplicate,
+ firstOccurrencePath: wrapper.firstOccurrencePath,
+ dimmedForFilter: wrapper.dimmedForFilter,
+ treeChildFactory: (children) => children.map((child) => this._createTreeDependencyNode(child)),
+ }
+ );
+ }
+
+ async buildReport() {
+ if (this._fullTrees.length === 0) {
+ return null;
+ }
+
+ const dependencies = this._fullTrees.flatMap((tree) => tree.dependencies);
+ const projectName = path.basename(this.getProjectFolder() || "workspace");
+ return buildDependencyHealthReport(
+ projectName,
+ dependencies,
+ this._summary,
+ formatReportDate(this._reportDateFactory())
+ );
+ }
+
+ async pullDependencies() {
+ if (this._scanning) {
+ vscode.window.showWarningMessage("Wait for the current dependency operation to finish.");
+ return;
+ }
+
+ if (!this.lastWorkspace) {
+ vscode.window.showInformationMessage("Run a dependency scan before pulling dependencies.");
+ return;
+ }
+
+ const dependencies = this._fullTrees.flatMap((tree) => tree.dependencies);
+ if (dependencies.length === 0) {
+ vscode.window.showInformationMessage("Run a dependency scan before pulling dependencies.");
+ return;
+ }
+
+ const cancellationSource = new vscode.CancellationTokenSource();
+ this._scanning = true;
+ this._failureMessage = null;
+ await this._updateContexts();
+
+ try {
+ const result = await vscode.window.withProgress(
+ {
+ location: vscode.ProgressLocation.Notification,
+ title: "Pulling dependencies",
+ cancellable: true,
+ },
+ async (progress, token) => {
+ const subscription = token.onCancellationRequested(() => cancellationSource.cancel());
+ try {
+ progress.report({ message: "Preparing pull-through request..." });
+ const execution = await this._services.upstreamPullService.run({
+ workspace: this.lastWorkspace,
+ repositoryHint: this.lastRepo,
+ dependencies,
+ progress,
+ token: cancellationSource.token,
+ });
+
+ if (!execution || execution.canceled) {
+ return execution || { canceled: true };
+ }
+
+ this.lastRepo = execution.repository.slug;
+ await this._updateContexts();
+
+ progress.report({ message: "Refreshing Cloudsmith coverage..." });
+ await this._refreshCoverageAfterPull(
+ execution.workspace,
+ execution.repository.slug,
+ progress,
+ cancellationSource.token
+ );
+
+ if (cancellationSource.token.isCancellationRequested) {
+ return { canceled: true };
+ }
+
+ return execution;
+ } finally {
+ subscription.dispose();
+ }
+ }
+ );
+
+ if (!result) {
+ return;
+ }
+
+ if (result.canceled) {
+ vscode.window.showInformationMessage("Dependency pull canceled.");
+ return;
+ }
+
+ if (result.pullResult) {
+ vscode.window.showInformationMessage(
+ buildPullSummaryMessage(result.pullResult, result.plan.skippedDependencies.length)
+ );
+ }
+ } finally {
+ cancellationSource.dispose();
+ this._scanning = false;
+ await this._updateContexts();
+ this.refresh();
+ }
+ }
+
+ async pullSingleDependency(item) {
+ if (this._scanning) {
+ vscode.window.showWarningMessage("Wait for the current dependency operation to finish.");
+ return;
+ }
+
+ if (!this.lastWorkspace) {
+ vscode.window.showInformationMessage("Run a dependency scan before pulling dependencies.");
+ return;
+ }
+
+ const dependency = createSingleDependencyPullTarget(item);
+ if (!dependency) {
+ vscode.window.showWarningMessage("Could not determine the dependency details.");
+ return;
+ }
+
+ const prepared = await this._services.upstreamPullService.prepareSingle({
+ workspace: this.lastWorkspace,
+ repositoryHint: this.lastRepo,
+ dependency,
+ });
+ if (!prepared) {
+ return;
+ }
+
+ const cancellationSource = new vscode.CancellationTokenSource();
+ this._scanning = true;
+ this._failureMessage = null;
+ await this._updateContexts();
+
+ try {
+ const result = await vscode.window.withProgress(
+ {
+ location: vscode.ProgressLocation.Notification,
+ title: `Pulling ${formatSingleDependencyLabel(prepared.dependency)} through ${prepared.repository.slug}...`,
+ cancellable: true,
+ },
+ async (progress, token) => {
+ const subscription = token.onCancellationRequested(() => cancellationSource.cancel());
+ try {
+ progress.report({ message: "Triggering upstream pull..." });
+ const execution = await this._services.upstreamPullService.execute(prepared, {
+ progress,
+ token: cancellationSource.token,
+ });
+
+ if (!execution || execution.canceled) {
+ return execution || { canceled: true };
+ }
+
+ this.lastRepo = prepared.repository.slug;
+ await this._updateContexts();
+
+ const pullDetail = getSingleDependencyPullDetail(execution.pullResult);
+ if (isSuccessfulSingleDependencyPull(pullDetail)) {
+ progress.report({ message: "Refreshing Cloudsmith coverage..." });
+ await this._refreshSingleDependencyAfterPull(
+ prepared.workspace,
+ prepared.repository.slug,
+ prepared.dependency,
+ progress,
+ cancellationSource.token
+ );
+ }
+
+ return {
+ ...prepared,
+ ...execution,
+ };
+ } finally {
+ subscription.dispose();
+ }
+ }
+ );
+
+ if (!result) {
+ return;
+ }
+
+ if (result.canceled) {
+ vscode.window.showInformationMessage("Dependency pull canceled.");
+ return;
+ }
+
+ const notification = buildSingleDependencyPullNotification(
+ prepared.dependency,
+ prepared.repository.slug,
+ getSingleDependencyPullDetail(result.pullResult)
+ );
+ if (notification.level === "error") {
+ vscode.window.showErrorMessage(notification.message);
+ } else {
+ vscode.window.showInformationMessage(notification.message);
+ }
+ } finally {
+ cancellationSource.dispose();
+ this._scanning = false;
+ await this._updateContexts();
+ this.refresh();
+ }
+ }
+
+ async _refreshCoverageAfterPull(cloudsmithWorkspace, cloudsmithRepo, progress, token) {
+ clearPackageIndexCache(cloudsmithWorkspace, cloudsmithRepo);
+ await this._refreshCoverageForDependencies(
+ cloudsmithWorkspace,
+ cloudsmithRepo,
+ null,
+ progress,
+ token,
+ { refreshRemainingUpstream: true }
+ );
+ }
+
+ async _refreshSingleDependencyAfterPull(cloudsmithWorkspace, cloudsmithRepo, dependency, progress, token) {
+ clearPackageIndexCache(cloudsmithWorkspace, cloudsmithRepo, dependency.format || dependency.ecosystem);
+ await this._refreshCoverageForDependencies(
+ cloudsmithWorkspace,
+ cloudsmithRepo,
+ [dependency],
+ progress,
+ token
+ );
+ }
+
+ async _refreshCoverageForDependencies(
+ cloudsmithWorkspace,
+ cloudsmithRepo,
+ targetDependencies,
+ progress,
+ token,
+ options = {}
+ ) {
+ const targetKeys = new Set(
+ (Array.isArray(targetDependencies) ? targetDependencies : [])
+ .map((dependency) => coverageLookupKey(dependency))
+ .filter(Boolean)
+ );
+ const unresolvedDependencies = uniqueDependenciesForCoverage(
+ this._fullTrees
+ .flatMap((tree) => tree.dependencies)
+ .filter((dependency) => (
+ dependency.cloudsmithStatus !== "FOUND"
+ && (targetKeys.size === 0 || targetKeys.has(coverageLookupKey(dependency)))
+ ))
+ );
+ const totalDependencies = unresolvedDependencies.length;
+
+ if (totalDependencies === 0) {
+ await this._publishDiagnostics();
+ this._rebuildSummary();
+ await this._storeReportData(this._reportDateFactory());
+ return [];
+ }
+
+ const previousFoundKeys = new Set(
+ this._fullTrees
+ .flatMap((tree) => tree.dependencies)
+ .filter((dependency) => (
+ dependency.cloudsmithStatus === "FOUND"
+ && (targetKeys.size === 0 || targetKeys.has(coverageLookupKey(dependency)))
+ ))
+ .map((dependency) => coverageLookupKey(dependency))
+ .filter(Boolean)
+ );
+
+ const dependenciesByFormat = groupDependenciesByFormat([{ dependencies: unresolvedDependencies }]);
+ await this._runCoverageResolution(
+ cloudsmithWorkspace,
+ cloudsmithRepo,
+ dependenciesByFormat,
+ totalDependencies,
+ progress,
+ token,
+ {
+ packageIndexFailureVerb: "refresh",
+ progressLabel: "Refreshing Cloudsmith coverage",
+ }
+ );
+
+ const newlyFoundDependencies = uniqueDependenciesForCoverage(
+ this._fullTrees
+ .flatMap((tree) => tree.dependencies)
+ .filter((dependency) => {
+ const key = coverageLookupKey(dependency);
+ return dependency.cloudsmithStatus === "FOUND"
+ && Boolean(key)
+ && !previousFoundKeys.has(key)
+ && (targetKeys.size === 0 || targetKeys.has(key));
+ })
+ );
+
+ if (newlyFoundDependencies.length > 0) {
+ progress.report({
+ message: targetKeys.size > 0
+ ? "Enriching pulled dependency..."
+ : "Enriching newly covered dependencies...",
+ });
+ await Promise.all([
+ this._runVulnerabilityEnrichment(newlyFoundDependencies, cloudsmithWorkspace, progress, token),
+ this._runLicenseEnrichment(newlyFoundDependencies, token),
+ this._runPolicyEnrichment(newlyFoundDependencies, token),
+ ]);
+ }
+
+ if (options.refreshRemainingUpstream) {
+ const remainingUncovered = this._fullTrees
+ .flatMap((tree) => tree.dependencies)
+ .filter((dependency) => dependency.cloudsmithStatus === "NOT_FOUND");
+ if (remainingUncovered.length > 0) {
+ progress.report({ message: "Refreshing upstream availability..." });
+ await this._runUpstreamGapAnalysis(
+ remainingUncovered,
+ cloudsmithWorkspace,
+ cloudsmithRepo,
+ progress,
+ token
+ );
+ }
+ }
+
+ await this._publishDiagnostics();
+ this._rebuildSummary();
+ await this._storeReportData(this._reportDateFactory());
+ this.refresh();
+
+ return newlyFoundDependencies;
+ }
+
+ async rescan() {
+ if (!this.lastWorkspace) {
+ vscode.window.showInformationMessage('No previous scan. Run "Scan dependencies" first.');
+ return;
+ }
+ await this.scan(this.lastWorkspace, this.lastRepo);
+ }
+
+ getTreeItem(element) {
+ return element.getTreeItem();
+ }
+
+ async getChildren(element) {
+ if (element) {
+ return element.getChildren();
+ }
+
+ if (this._statusMessage) {
+ return [
+ new InfoNode(
+ this._statusMessage,
+ "",
+ this._statusMessage,
+ "loading~spin",
+ "statusMessage"
+ ),
+ ];
+ }
+
+ if (this._failureMessage) {
+ return [
+ new InfoNode(
+ this._failureMessage,
+ "",
+ this._failureMessage,
+ "error",
+ "statusMessage"
+ ),
+ ];
+ }
+
+ if (this._noManifestsFolder) {
+ return [
+ new InfoNode(
+ "No dependency manifests or lockfiles found",
+ this._noManifestsFolder,
+ "Supported formats include npm, Python, Maven, Gradle, Go, Cargo, Ruby, Docker, NuGet, Dart, Composer, Helm, Swift, and Hex.",
+ "warning",
+ "infoNode"
+ ),
+ ];
+ }
+
+ if (this._displayTrees.length > 0) {
+ const nodes = [new DependencySummaryNode(this._summary)];
+ if (this._warnings.length > 0) {
+ nodes.push(new InfoNode(
+ this._warnings[0],
+ "",
+ this._warnings.join("\n"),
+ "warning",
+ "statusMessage"
+ ));
+ }
+ nodes.push(...this._displayTrees.map((tree) => new DependencySourceGroupNode(tree, this)));
+ return nodes;
+ }
+
+ if (!this._hasScannedOnce) {
+ const isConnected = this.context && this.context.secrets
+ ? await this.context.secrets.get("cloudsmith-vsc.isConnected")
+ : "false";
+ if (isConnected !== "true") {
+ return [
+ new InfoNode(
+ "Connect to Cloudsmith",
+ "Use the key icon above to set up authentication.",
+ "Set up Cloudsmith authentication to get started.",
+ "plug",
+ undefined,
+ { command: "cloudsmith-vsc.configureCredentials", title: "Set up authentication" }
+ ),
+ ];
+ }
+
+ return [
+ new InfoNode(
+ "Scan dependencies",
+ "Select the play button above to start.",
+ "Scans lockfiles and manifests, resolves direct and transitive dependencies, and checks each one against Cloudsmith.",
+ "folder",
+ "dependencyHealthWelcome"
+ ),
+ ];
+ }
+
+ return [
+ new InfoNode(
+ "No dependencies found",
+ "",
+ "The detected dependency files did not contain any dependencies to scan.",
+ "info",
+ "infoNode"
+ ),
+ ];
}
refresh() {
this._onDidChangeTreeData.fire();
}
+
+ _rebuildSummary() {
+ this._summary = buildDependencySummary(this._fullTrees, this._displayTrees, {
+ filterMode: this._filterMode,
+ });
+ this.dependencies = this._displayTrees.flatMap((tree) => tree.dependencies);
+ }
+}
+
+DependencyHealthProvider.packageIndexCache = new Map();
+
+function pruneExpiredPackageIndexCache(now = Date.now()) {
+ for (const [cacheKey, cacheEntry] of DependencyHealthProvider.packageIndexCache.entries()) {
+ if (!cacheEntry || cacheEntry.expiresAt <= now) {
+ DependencyHealthProvider.packageIndexCache.delete(cacheKey);
+ }
+ }
+}
+
+function getCachedPackageIndexValue(cacheKey) {
+ const cached = DependencyHealthProvider.packageIndexCache.get(cacheKey);
+ if (!cached) {
+ return null;
+ }
+
+ if (cached.expiresAt > Date.now()) {
+ return cached.value;
+ }
+
+ DependencyHealthProvider.packageIndexCache.delete(cacheKey);
+ return null;
+}
+
+function setCachedPackageIndexValue(cacheKey, value) {
+ if (DependencyHealthProvider.packageIndexCache.size >= PACKAGE_INDEX_CACHE_MAX_SIZE) {
+ pruneExpiredPackageIndexCache();
+ }
+
+ DependencyHealthProvider.packageIndexCache.set(cacheKey, {
+ expiresAt: Date.now() + PACKAGE_INDEX_TTL_MS,
+ value,
+ });
+}
+
+function normalizeTree(tree) {
+ return {
+ ecosystem: tree.ecosystem,
+ sourceFile: tree.sourceFile,
+ dependencies: deduplicateDependenciesWithStatus(
+ (tree.dependencies || []).map((dependency) => normalizeDependency(dependency, tree))
+ ),
+ };
+}
+
+function normalizeDependency(dependency, tree) {
+ const ecosystem = dependency.ecosystem || tree.ecosystem;
+ const format = dependency.format || canonicalFormat(ecosystem);
+ return {
+ ...dependency,
+ ecosystem,
+ format,
+ sourceFile: dependency.sourceFile || tree.sourceFile,
+ parent: dependency.parent || null,
+ parentChain: Array.isArray(dependency.parentChain) ? dependency.parentChain.slice() : [],
+ transitives: Array.isArray(dependency.transitives)
+ ? dependency.transitives.map((child) => normalizeDependency(child, tree))
+ : [],
+ cloudsmithStatus: dependency.cloudsmithStatus || null,
+ cloudsmithPackage: dependency.cloudsmithPackage || null,
+ devDependency: Boolean(dependency.devDependency || dependency.isDevelopmentDependency),
+ isDevelopmentDependency: Boolean(dependency.isDevelopmentDependency || dependency.devDependency),
+ vulnerabilities: dependency.vulnerabilities || null,
+ license: dependency.license || null,
+ policy: dependency.policy || null,
+ upstreamStatus: dependency.upstreamStatus || null,
+ upstreamDetail: dependency.upstreamDetail || null,
+ };
+}
+
+function deduplicateDependenciesWithStatus(dependencies) {
+ const seen = new Map();
+ const results = [];
+
+ for (const dependency of dependencies) {
+ const key = displayDependencyKey(dependency);
+ const existing = seen.get(key);
+ if (!existing) {
+ seen.set(key, dependency);
+ results.push(dependency);
+ continue;
+ }
+
+ if (!existing.isDirect && dependency.isDirect) {
+ const index = results.indexOf(existing);
+ if (index !== -1) {
+ results[index] = dependency;
+ }
+ seen.set(key, dependency);
+ }
+ }
+
+ return results;
+}
+
+function displayDependencyKey(dependency) {
+ return [
+ dependency.sourceFile || "",
+ dependency.format || "",
+ dependency.name || "",
+ dependency.version || "",
+ dependency.isDirect ? "direct" : "transitive",
+ (dependency.parentChain || []).join(">"),
+ ].join(":").toLowerCase();
+}
+
+function coverageLookupKey(dependency) {
+ return [
+ canonicalFormat(dependency.format || dependency.ecosystem),
+ normalizePackageName(dependency.name, dependency.format || dependency.ecosystem),
+ String(dependency.version || "").toLowerCase(),
+ ].join(":");
+}
+
+function groupDependenciesByFormat(trees) {
+ const byFormat = {};
+ for (const tree of trees) {
+ for (const dependency of tree.dependencies) {
+ if (!byFormat[dependency.format]) {
+ byFormat[dependency.format] = [];
+ }
+ byFormat[dependency.format].push(dependency);
+ }
+ }
+ return byFormat;
+}
+
+function uniqueDependenciesForCoverage(dependencies) {
+ const seen = new Set();
+ const unique = [];
+ for (const dependency of dependencies) {
+ const key = coverageLookupKey(dependency);
+ if (seen.has(key)) {
+ continue;
+ }
+ seen.add(key);
+ unique.push(dependency);
+ }
+ return unique;
+}
+
+function countCoverageDependencies(trees) {
+ return Object.values(groupDependenciesByFormat(trees))
+ .reduce((count, dependencies) => count + uniqueDependenciesForCoverage(dependencies).length, 0);
+}
+
+function clearPackageIndexCache(workspace, repo, format) {
+ const workspaceKey = String(workspace || "").toLowerCase();
+ const repoKey = String(repo || "").toLowerCase();
+ const formatKey = format ? String(canonicalFormat(format) || format).toLowerCase() : null;
+
+ for (const cacheKey of DependencyHealthProvider.packageIndexCache.keys()) {
+ if (!cacheKey.startsWith(`${workspaceKey}:${repoKey}:`)) {
+ continue;
+ }
+
+ if (formatKey && !cacheKey.endsWith(`:${formatKey}`)) {
+ continue;
+ }
+
+ DependencyHealthProvider.packageIndexCache.delete(cacheKey);
+ }
+}
+
+function buildPackageIndex(packages, format) {
+ const index = new Map();
+ for (const pkg of packages) {
+ const versionKey = String(pkg.version || "").toLowerCase();
+ for (const nameKey of getCloudsmithPackageLookupKeys(pkg, format)) {
+ if (!index.has(nameKey)) {
+ index.set(nameKey, new Map());
+ }
+ const versionMap = index.get(nameKey);
+ if (!versionMap.has(versionKey)) {
+ versionMap.set(versionKey, []);
+ }
+ versionMap.get(versionKey).push(pkg);
+ }
+ }
+ return index;
+}
+
+function findCoverageMatch(packageIndex, dependency) {
+ for (const lookupKey of getPackageLookupKeys(dependency.name, dependency.format)) {
+ const versions = packageIndex.get(lookupKey);
+ if (!versions) {
+ continue;
+ }
+ const versionKey = String(dependency.version || "").toLowerCase();
+ if (versionKey && versions.has(versionKey)) {
+ return versions.get(versionKey)[0] || null;
+ }
+ const firstMatch = [...versions.values()][0];
+ if (firstMatch && firstMatch[0]) {
+ return firstMatch[0];
+ }
+ }
+ return null;
+}
+
+function matchCoverageCandidates(candidates, dependency) {
+ const dependencyKeys = getPackageLookupKeys(dependency.name, dependency.format);
+ let nameMatch = null;
+
+ for (const candidate of candidates) {
+ const candidateKeys = new Set(getCloudsmithPackageLookupKeys(candidate, dependency.format));
+ const nameMatches = dependencyKeys.some((key) => candidateKeys.has(key));
+ if (!nameMatches) {
+ continue;
+ }
+ if (!dependency.version || candidate.version === dependency.version) {
+ return candidate;
+ }
+ if (!nameMatch) {
+ nameMatch = candidate;
+ }
+ }
+ return nameMatch;
+}
+
+function applyCoverageMatchBatchToTrees(trees, matchMap) {
+ return applyPatchMapToTrees(trees, coverageLookupKey, matchMap, (dependency, patch) => ({
+ ...dependency,
+ cloudsmithStatus: patch.cloudsmithStatus,
+ cloudsmithPackage: patch.cloudsmithPackage,
+ upstreamStatus: Object.prototype.hasOwnProperty.call(patch, "upstreamStatus")
+ ? patch.upstreamStatus
+ : dependency.upstreamStatus,
+ upstreamDetail: Object.prototype.hasOwnProperty.call(patch, "upstreamDetail")
+ ? patch.upstreamDetail
+ : dependency.upstreamDetail,
+ }));
+}
+
+function applyFoundOverlayPatch(trees, patchMap, mergeFn) {
+ return applyPatchMapToTrees(trees, getFoundDependencyKey, patchMap, mergeFn);
+}
+
+function applyUncoveredOverlayPatch(trees, patchMap, mergeFn) {
+ return applyPatchMapToTrees(trees, getUncoveredDependencyKey, patchMap, mergeFn);
+}
+
+function applyPatchMapToTrees(trees, getKey, patchMap, mergeFn) {
+ if (!(patchMap instanceof Map) || patchMap.size === 0) {
+ return trees;
+ }
+
+ return trees.map((tree) => ({
+ ...tree,
+ dependencies: tree.dependencies.map((dependency) => applyRecursiveDependencyPatch(
+ dependency,
+ getKey,
+ patchMap,
+ mergeFn
+ )),
+ }));
+}
+
+function applyRecursiveDependencyPatch(dependency, getKey, patchMap, mergeFn) {
+ const key = getKey(dependency);
+ const hasPatch = Boolean(key) && patchMap.has(key);
+ const mergedDependency = hasPatch ? mergeFn(dependency, patchMap.get(key), key) : dependency;
+ const originalChildren = Array.isArray(mergedDependency.transitives) ? mergedDependency.transitives : [];
+ const nextChildren = originalChildren.map((child) => applyRecursiveDependencyPatch(child, getKey, patchMap, mergeFn));
+ if (originalChildren === nextChildren || arraysEqualByReference(originalChildren, nextChildren)) {
+ return mergedDependency;
+ }
+ return {
+ ...mergedDependency,
+ transitives: nextChildren,
+ };
+}
+
+function applyRecursiveDependencyUpdate(dependency, predicate, mergeFn) {
+ const mergedDependency = predicate(dependency) ? mergeFn(dependency) : dependency;
+ const originalChildren = Array.isArray(mergedDependency.transitives) ? mergedDependency.transitives : [];
+ const nextChildren = originalChildren.map((child) => applyRecursiveDependencyUpdate(child, predicate, mergeFn));
+ if (originalChildren === nextChildren || arraysEqualByReference(originalChildren, nextChildren)) {
+ return mergedDependency;
+ }
+ return {
+ ...mergedDependency,
+ transitives: nextChildren,
+ };
+}
+
+function arraysEqualByReference(left, right) {
+ if (left.length !== right.length) {
+ return false;
+ }
+ for (let index = 0; index < left.length; index += 1) {
+ if (left[index] !== right[index]) {
+ return false;
+ }
+ }
+ return true;
+}
+
+function markTreesAsChecking(trees) {
+ return trees.map((tree) => ({
+ ...tree,
+ dependencies: tree.dependencies.map((dependency) => applyRecursiveDependencyUpdate(
+ dependency,
+ () => true,
+ (candidate) => ({
+ ...candidate,
+ cloudsmithStatus: "CHECKING",
+ cloudsmithPackage: null,
+ vulnerabilities: null,
+ license: null,
+ policy: null,
+ upstreamStatus: null,
+ upstreamDetail: null,
+ })
+ )),
+ }));
+}
+
+function limitDisplayTrees(trees, maxDependencies) {
+ const allDependencies = [];
+ for (const tree of trees) {
+ for (const dependency of tree.dependencies) {
+ allDependencies.push(dependency);
+ }
+ }
+
+ if (allDependencies.length <= maxDependencies) {
+ return {
+ trees: trees.map((tree) => ({
+ ...tree,
+ dependencies: tree.dependencies.slice().sort((left, right) => compareDependencies(left, right, SORT_MODES.ALPHABETICAL, true)),
+ })),
+ truncated: false,
+ totalDependencies: allDependencies.length,
+ };
+ }
+
+ const allowedKeys = new Set(
+ allDependencies
+ .slice()
+ .sort(compareDependenciesForLimit)
+ .slice(0, maxDependencies)
+ .map(displayDependencyKey)
+ );
+
+ const limitedTrees = trees
+ .map((tree) => ({
+ ...tree,
+ dependencies: tree.dependencies
+ .filter((dependency) => allowedKeys.has(displayDependencyKey(dependency)))
+ .map((dependency) => pruneDependencyTree(dependency, allowedKeys))
+ .sort((left, right) => compareDependencies(left, right, SORT_MODES.ALPHABETICAL, true)),
+ }))
+ .filter((tree) => tree.dependencies.length > 0);
+
+ return {
+ trees: limitedTrees,
+ truncated: true,
+ totalDependencies: allDependencies.length,
+ };
+}
+
+function pruneDependencyTree(dependency, allowedKeys) {
+ const transitives = Array.isArray(dependency.transitives)
+ ? dependency.transitives
+ .filter((child) => allowedKeys.has(displayDependencyKey(child)))
+ .map((child) => pruneDependencyTree(child, allowedKeys))
+ : [];
+
+ return {
+ ...dependency,
+ transitives,
+ };
+}
+
+function compareDependenciesForLimit(left, right) {
+ if (left.isDirect !== right.isDirect) {
+ return left.isDirect ? -1 : 1;
+ }
+ return compareDependencies(left, right, SORT_MODES.ALPHABETICAL, false);
+}
+
+function compareDependencies(left, right, sortMode, preferDirect) {
+ if (preferDirect && left.isDirect !== right.isDirect) {
+ return left.isDirect ? -1 : 1;
+ }
+
+ if (sortMode === SORT_MODES.SEVERITY) {
+ const severityDelta = dependencySeveritySortGroup(left) - dependencySeveritySortGroup(right);
+ if (severityDelta !== 0) {
+ return severityDelta;
+ }
+ }
+
+ if (sortMode === SORT_MODES.COVERAGE) {
+ const coverageDelta = dependencyCoverageSortGroup(left) - dependencyCoverageSortGroup(right);
+ if (coverageDelta !== 0) {
+ return coverageDelta;
+ }
+ }
+
+ const leftName = String(left.name || "").toLowerCase();
+ const rightName = String(right.name || "").toLowerCase();
+ if (leftName !== rightName) {
+ return leftName.localeCompare(rightName);
+ }
+
+ return String(left.version || "").localeCompare(String(right.version || ""));
+}
+
+function dependencyCoverageSortGroup(dependency) {
+ if (dependency.cloudsmithStatus === "NOT_FOUND") {
+ return 0;
+ }
+ if (dependency.cloudsmithStatus === "CHECKING") {
+ return 2;
+ }
+ return 1;
+}
+
+function dependencySeveritySortGroup(dependency) {
+ if (dependency.cloudsmithStatus !== "FOUND") {
+ return dependency.cloudsmithStatus === "NOT_FOUND" ? 5 : 6;
+ }
+
+ const policy = getDependencyPolicyData(dependency);
+ const vulnerabilities = getDependencyVulnerabilityData(dependency);
+ const licenseClassification = getDependencyLicenseClassification(dependency);
+
+ if (policy && (policy.quarantined || policy.denied)) {
+ return 0;
+ }
+
+ if (vulnerabilities && vulnerabilities.count > 0) {
+ if (vulnerabilities.maxSeverity === "Critical") {
+ return 1;
+ }
+ if (vulnerabilities.maxSeverity === "High") {
+ return 2;
+ }
+ return 3;
+ }
+
+ if (licenseClassification === "restrictive") {
+ return 2;
+ }
+
+ if (licenseClassification === "weak_copyleft" || (policy && policy.violated)) {
+ return 3;
+ }
+
+ return 4;
+}
+
+function getTreeRootDependencies(tree) {
+ return (tree.dependencies || []).filter((dependency) => {
+ const hasParentChain = Array.isArray(dependency.parentChain) && dependency.parentChain.length > 0;
+ return !dependency.parent && !hasParentChain;
+ });
+}
+
+function buildFilteredTreeWrapper(dependency, filterMode, sortMode) {
+ const children = Array.isArray(dependency.transitives)
+ ? dependency.transitives
+ .slice()
+ .sort((left, right) => compareDependencies(left, right, sortMode, false))
+ .map((child) => buildFilteredTreeWrapper(child, filterMode, sortMode))
+ .filter(Boolean)
+ : [];
+ const matches = matchesFilter(dependency, filterMode);
+
+ if (filterMode && !matches && children.length === 0) {
+ return null;
+ }
+
+ return {
+ dependency,
+ children,
+ duplicate: false,
+ firstOccurrencePath: null,
+ dimmedForFilter: Boolean(filterMode) && !matches,
+ };
+}
+
+function annotateDuplicateWrappers(wrappers, seen, ancestry) {
+ return wrappers.map((wrapper) => {
+ const pathLabel = ancestry.concat(wrapper.dependency.name).join(" > ");
+ const duplicateKey = buildDuplicateKey(wrapper.dependency);
+ if (duplicateKey && seen.has(duplicateKey)) {
+ return {
+ ...wrapper,
+ duplicate: true,
+ firstOccurrencePath: seen.get(duplicateKey),
+ children: [],
+ };
+ }
+
+ if (duplicateKey) {
+ seen.set(duplicateKey, pathLabel);
+ }
+
+ return {
+ ...wrapper,
+ children: annotateDuplicateWrappers(wrapper.children, seen, ancestry.concat(wrapper.dependency.name)),
+ };
+ });
+}
+
+function buildDuplicateKey(dependency) {
+ const name = String(dependency.name || "").trim().toLowerCase();
+ const version = String(dependency.version || "").trim().toLowerCase();
+ if (!name) {
+ return null;
+ }
+ return `${name}:${version}`;
+}
+
+function matchesFilter(dependency, filterMode) {
+ const vulnerabilities = getDependencyVulnerabilityData(dependency);
+ const policy = getDependencyPolicyData(dependency);
+ const licenseClassification = getDependencyLicenseClassification(dependency);
+
+ if (!filterMode) {
+ return true;
+ }
+
+ switch (filterMode) {
+ case FILTER_MODES.VULNERABLE:
+ return Boolean(vulnerabilities && vulnerabilities.count > 0);
+ case FILTER_MODES.UNCOVERED:
+ return dependency.cloudsmithStatus === "NOT_FOUND";
+ case FILTER_MODES.RESTRICTIVE_LICENSE:
+ return licenseClassification === "restrictive";
+ case FILTER_MODES.POLICY_VIOLATION:
+ return Boolean(policy && policy.violated);
+ default:
+ return true;
+ }
+}
+
+function getFilterLabel(filterMode) {
+ switch (filterMode) {
+ case FILTER_MODES.VULNERABLE:
+ return "vulnerable only";
+ case FILTER_MODES.UNCOVERED:
+ return "not in Cloudsmith";
+ case FILTER_MODES.RESTRICTIVE_LICENSE:
+ return "restrictive licenses";
+ case FILTER_MODES.POLICY_VIOLATION:
+ return "policy violations";
+ default:
+ return null;
+ }
+}
+
+function buildDependencySummary(fullTrees, displayTrees, options = {}) {
+ const fullDependencies = fullTrees.flatMap((tree) => tree.dependencies);
+ const displayDependencies = displayTrees.flatMap((tree) => tree.dependencies);
+ const summaryDependencies = fullDependencies.length > 0 ? fullDependencies : displayDependencies;
+ const direct = fullDependencies.filter((dependency) => dependency.isDirect).length;
+ const ecosystems = {};
+
+ for (const tree of fullTrees) {
+ ecosystems[tree.ecosystem] = (ecosystems[tree.ecosystem] || 0) + tree.dependencies.length;
+ }
+
+ const found = summaryDependencies.filter((dependency) => dependency.cloudsmithStatus === "FOUND").length;
+ const notFound = summaryDependencies.filter((dependency) => dependency.cloudsmithStatus === "NOT_FOUND").length;
+ const checking = summaryDependencies.filter((dependency) => dependency.cloudsmithStatus === "CHECKING").length;
+ const reachableViaUpstream = summaryDependencies.filter((dependency) => (
+ dependency.cloudsmithStatus === "NOT_FOUND" && dependency.upstreamStatus === "reachable"
+ )).length;
+ const unreachableViaUpstream = summaryDependencies.filter((dependency) => (
+ dependency.cloudsmithStatus === "NOT_FOUND"
+ && (dependency.upstreamStatus === "no_proxy" || dependency.upstreamStatus === "unreachable")
+ )).length;
+ const vulnerable = summaryDependencies.filter((dependency) => {
+ const vulnerabilities = getDependencyVulnerabilityData(dependency);
+ return dependency.cloudsmithStatus === "FOUND" && vulnerabilities && vulnerabilities.count > 0;
+ }).length;
+ const severityCounts = {};
+ for (const dependency of summaryDependencies) {
+ const vulnerabilities = getDependencyVulnerabilityData(dependency);
+ if (dependency.cloudsmithStatus === "FOUND" && vulnerabilities && vulnerabilities.count > 0 && vulnerabilities.maxSeverity) {
+ severityCounts[vulnerabilities.maxSeverity] = (severityCounts[vulnerabilities.maxSeverity] || 0) + 1;
+ }
+ }
+
+ const permissiveLicenses = summaryDependencies.filter((dependency) => (
+ dependency.cloudsmithStatus === "FOUND"
+ && getDependencyLicenseClassification(dependency) === "permissive"
+ )).length;
+ const weakCopyleftLicenses = summaryDependencies.filter((dependency) => (
+ dependency.cloudsmithStatus === "FOUND"
+ && getDependencyLicenseClassification(dependency) === "weak_copyleft"
+ )).length;
+ const restrictiveLicenses = summaryDependencies.filter((dependency) => (
+ dependency.cloudsmithStatus === "FOUND"
+ && getDependencyLicenseClassification(dependency) === "restrictive"
+ )).length;
+ const unknownLicenses = summaryDependencies.filter((dependency) => (
+ dependency.cloudsmithStatus === "FOUND"
+ && getDependencyLicenseClassification(dependency) === "unknown"
+ )).length;
+ const policyViolations = summaryDependencies.filter((dependency) => {
+ const policy = getDependencyPolicyData(dependency);
+ return dependency.cloudsmithStatus === "FOUND" && policy && policy.violated;
+ }).length;
+ const quarantined = summaryDependencies.filter((dependency) => {
+ const policy = getDependencyPolicyData(dependency);
+ return dependency.cloudsmithStatus === "FOUND" && policy && (policy.quarantined || policy.denied);
+ }).length;
+
+ const filterMode = options.filterMode || null;
+ const filterLabel = getFilterLabel(filterMode);
+ const filteredCount = filterMode
+ ? summaryDependencies.filter((dependency) => matchesFilter(dependency, filterMode)).length
+ : 0;
+
+ return {
+ total: fullDependencies.length,
+ direct,
+ transitive: fullDependencies.length - direct,
+ found,
+ notFound,
+ reachableViaUpstream,
+ unreachableViaUpstream,
+ ecosystems,
+ coveragePercent: summaryDependencies.length === 0
+ ? 0
+ : Math.round((found / summaryDependencies.length) * 100),
+ checking,
+ vulnerable,
+ severityCounts,
+ restrictiveLicenses,
+ weakCopyleftLicenses,
+ permissiveLicenses,
+ unknownLicenses,
+ policyViolations,
+ quarantined,
+ filterMode,
+ filterLabel,
+ filteredCount,
+ };
+}
+
+function emptySummary() {
+ return {
+ total: 0,
+ direct: 0,
+ transitive: 0,
+ found: 0,
+ notFound: 0,
+ reachableViaUpstream: 0,
+ unreachableViaUpstream: 0,
+ ecosystems: {},
+ coveragePercent: 0,
+ checking: 0,
+ vulnerable: 0,
+ severityCounts: {},
+ restrictiveLicenses: 0,
+ weakCopyleftLicenses: 0,
+ permissiveLicenses: 0,
+ unknownLicenses: 0,
+ policyViolations: 0,
+ quarantined: 0,
+ filterMode: null,
+ filterLabel: null,
+ filteredCount: 0,
+ };
+}
+
+async function runPromisePool(items, concurrency, worker) {
+ const workers = [];
+ let index = 0;
+ const size = Math.max(1, Math.min(concurrency, items.length || 1));
+
+ for (let workerIndex = 0; workerIndex < size; 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 mergePatchMaps(patchMaps) {
+ const mergedPatchMap = new Map();
+
+ for (const patchMap of patchMaps) {
+ if (!(patchMap instanceof Map)) {
+ continue;
+ }
+
+ for (const [key, value] of patchMap.entries()) {
+ mergedPatchMap.set(key, value);
+ }
+ }
+
+ return mergedPatchMap;
+}
+
+function yieldToEventLoop() {
+ return new Promise((resolve) => {
+ if (typeof setImmediate === "function") {
+ setImmediate(resolve);
+ return;
+ }
+
+ setTimeout(resolve, 0);
+ });
+}
+
+function createSingleDependencyPullTarget(item) {
+ if (!item || typeof item !== "object") {
+ return null;
+ }
+
+ const name = String(item.name || "").trim();
+ const format = canonicalFormat(item.format || item.ecosystem);
+ if (!name || !format) {
+ return null;
+ }
+
+ const versionValue = typeof item.declaredVersion === "string"
+ ? item.declaredVersion
+ : (typeof item.version === "string" ? item.version : "");
+
+ return {
+ ...item,
+ name,
+ version: versionValue || "",
+ format,
+ ecosystem: item.ecosystem || format,
+ };
+}
+
+function formatSingleDependencyLabel(dependency) {
+ const name = String(dependency && dependency.name || "").trim() || "dependency";
+ const version = String(dependency && dependency.version || "").trim();
+ return version ? `${name}@${version}` : name;
+}
+
+function getSingleDependencyPullDetail(pullResult) {
+ return pullResult && Array.isArray(pullResult.details) ? (pullResult.details[0] || null) : null;
+}
+
+function isSuccessfulSingleDependencyPull(detail) {
+ return Boolean(
+ detail
+ && (detail.status === PULL_STATUS.CACHED || detail.status === PULL_STATUS.ALREADY_EXISTS)
+ );
+}
+
+function buildSingleDependencyPullNotification(dependency, repositorySlug, detail) {
+ const dependencyLabel = formatSingleDependencyLabel(dependency);
+ if (!detail) {
+ return {
+ level: "error",
+ message: `Could not pull ${dependencyLabel}.`,
+ };
+ }
+
+ switch (detail.status) {
+ case PULL_STATUS.CACHED:
+ case PULL_STATUS.ALREADY_EXISTS:
+ return {
+ level: "info",
+ message: `${dependencyLabel} cached in ${repositorySlug}`,
+ };
+ case PULL_STATUS.NOT_FOUND:
+ return {
+ level: "info",
+ message: `${dependencyLabel} not found on the upstream source.`,
+ };
+ case PULL_STATUS.AUTH_FAILED:
+ return {
+ level: "error",
+ message: "Authentication failed. Check your API key in Cloudsmith settings.",
+ };
+ default:
+ return {
+ level: "error",
+ message: detail.errorMessage
+ ? `Could not pull ${dependencyLabel}. ${detail.errorMessage}`
+ : `Could not pull ${dependencyLabel}.`,
+ };
+ }
+}
+
+function formatReportDate(date) {
+ if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
+ return new Date().toISOString().slice(0, 10);
+ }
+ return date.toISOString().slice(0, 10);
+}
+
+function normalizeReportTimestamp(value) {
+ const date = value instanceof Date ? value : new Date(value);
+ if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
+ return new Date().toISOString();
+ }
+ return date.toISOString();
+}
+
+function buildComplianceReportData(projectName, dependencies, options = {}) {
+ const uniqueDependencies = dedupeComplianceDependencies(dependencies);
+ const ecosystemBreakdown = {};
+
+ for (const dependency of uniqueDependencies) {
+ const ecosystem = String(dependency.format || dependency.ecosystem || "unknown").toLowerCase();
+ ecosystemBreakdown[ecosystem] = (ecosystemBreakdown[ecosystem] || 0) + 1;
+ }
+
+ const vulnerableDeps = uniqueDependencies
+ .filter((dependency) => dependency.cloudsmithStatus === "FOUND")
+ .map((dependency) => {
+ const vulnerabilities = getDependencyVulnerabilityData(dependency);
+ if (!vulnerabilities || vulnerabilities.count <= 0) {
+ return null;
+ }
+
+ const fixEntry = Array.isArray(vulnerabilities.entries)
+ ? vulnerabilities.entries.find((entry) => entry && entry.fixVersion)
+ : null;
+
+ return {
+ name: dependency.name,
+ version: dependency.version || "",
+ isDirect: Boolean(dependency.isDirect),
+ maxSeverity: vulnerabilities.maxSeverity || null,
+ cveCount: vulnerabilities.count || 0,
+ hasFixAvailable: Boolean(fixEntry || vulnerabilities.hasFixAvailable),
+ };
+ })
+ .filter(Boolean)
+ .sort(compareComplianceVulnerabilityRows);
+
+ const severityCounts = {};
+ for (const dependency of vulnerableDeps) {
+ const severity = dependency.maxSeverity || "Unknown";
+ severityCounts[severity] = (severityCounts[severity] || 0) + 1;
+ }
+
+ const restrictiveLicenseDeps = uniqueDependencies
+ .filter((dependency) => dependency.cloudsmithStatus === "FOUND")
+ .map((dependency) => {
+ const classification = getDependencyLicenseClassification(dependency);
+ if (!["restrictive", "weak_copyleft"].includes(classification)) {
+ return null;
+ }
+
+ const licenseData = dependency.license || null;
+ const inspection = dependency.cloudsmithPackage
+ ? LicenseClassifier.inspect(dependency.cloudsmithPackage)
+ : LicenseClassifier.inspect(null);
+ const spdx = licenseData && licenseData.spdx
+ ? licenseData.spdx
+ : dependency.spdx_license
+ ? dependency.spdx_license
+ : inspection.spdxLicense || inspection.displayValue || "";
+
+ return {
+ name: dependency.name,
+ version: dependency.version || "",
+ spdx,
+ classification: humanizeLicenseClassification(classification),
+ };
+ })
+ .filter(Boolean)
+ .sort(compareNamedRows);
+
+ const policyViolationDeps = uniqueDependencies
+ .filter((dependency) => dependency.cloudsmithStatus === "FOUND")
+ .map((dependency) => {
+ const policy = getDependencyPolicyData(dependency);
+ if (!policy || !policy.violated) {
+ return null;
+ }
+
+ return {
+ name: dependency.name,
+ version: dependency.version || "",
+ status: humanizePolicyStatus(policy),
+ detail: policy.statusReason || defaultPolicyDetail(policy),
+ };
+ })
+ .filter(Boolean)
+ .sort(compareCompliancePolicyRows);
+
+ const uncoveredDeps = uniqueDependencies
+ .filter((dependency) => dependency.cloudsmithStatus === "NOT_FOUND")
+ .map((dependency) => ({
+ name: dependency.name,
+ version: dependency.version || "",
+ ecosystem: dependency.format || dependency.ecosystem || "",
+ upstreamStatus: dependency.upstreamStatus || "unknown",
+ upstreamDetail: dependency.upstreamDetail || defaultUpstreamDetail(dependency.upstreamStatus),
+ }))
+ .sort(compareComplianceUncoveredRows);
+
+ const total = uniqueDependencies.length;
+ const direct = uniqueDependencies.filter((dependency) => dependency.isDirect).length;
+ const found = uniqueDependencies.filter((dependency) => dependency.cloudsmithStatus === "FOUND").length;
+ const notFound = uniqueDependencies.filter((dependency) => dependency.cloudsmithStatus === "NOT_FOUND").length;
+ const upstreamReachable = uncoveredDeps.filter((dependency) => dependency.upstreamStatus === "reachable").length;
+ const upstreamNoProxy = uncoveredDeps.filter((dependency) => dependency.upstreamStatus === "no_proxy").length;
+ const upstreamUnreachable = uncoveredDeps.filter((dependency) => dependency.upstreamStatus === "unreachable").length;
+
+ return {
+ projectName: projectName || "workspace",
+ scanDate: normalizeReportTimestamp(options.scanDate),
+ summary: {
+ total,
+ direct,
+ transitive: Math.max(total - direct, 0),
+ found,
+ notFound,
+ coveragePct: total === 0 ? 0 : Math.round((found / total) * 100),
+ vulnCount: vulnerableDeps.length,
+ criticalCount: severityCounts.Critical || 0,
+ highCount: severityCounts.High || 0,
+ mediumCount: severityCounts.Medium || 0,
+ lowCount: severityCounts.Low || 0,
+ restrictiveLicenseCount: restrictiveLicenseDeps.length,
+ policyViolationCount: policyViolationDeps.length,
+ upstreamReachable,
+ upstreamNoProxy,
+ upstreamUnreachable,
+ },
+ ecosystemBreakdown,
+ vulnerableDeps,
+ restrictiveLicenseDeps,
+ policyViolationDeps,
+ uncoveredDeps,
+ };
+}
+
+function dedupeComplianceDependencies(dependencies) {
+ const uniqueDependencies = new Map();
+
+ for (const dependency of Array.isArray(dependencies) ? dependencies : []) {
+ const key = complianceDependencyKey(dependency);
+ if (!uniqueDependencies.has(key)) {
+ uniqueDependencies.set(key, { ...dependency });
+ continue;
+ }
+
+ uniqueDependencies.set(key, mergeComplianceDependency(uniqueDependencies.get(key), dependency));
+ }
+
+ return [...uniqueDependencies.values()];
+}
+
+function complianceDependencyKey(dependency) {
+ return [
+ String(dependency.format || dependency.ecosystem || "").toLowerCase(),
+ String(dependency.name || "").toLowerCase(),
+ String(dependency.version || "").toLowerCase(),
+ ].join(":");
+}
+
+function mergeComplianceDependency(existing, candidate) {
+ return {
+ ...existing,
+ isDirect: Boolean(existing.isDirect || candidate.isDirect),
+ cloudsmithStatus: pickBetterCoverageStatus(existing.cloudsmithStatus, candidate.cloudsmithStatus),
+ cloudsmithPackage: existing.cloudsmithPackage || candidate.cloudsmithPackage || null,
+ vulnerabilities: pickRicherVulnerabilityData(existing.vulnerabilities, candidate.vulnerabilities),
+ license: existing.license || candidate.license || null,
+ policy: pickRicherPolicyData(existing.policy, candidate.policy),
+ upstreamStatus: existing.upstreamStatus || candidate.upstreamStatus || null,
+ upstreamDetail: existing.upstreamDetail || candidate.upstreamDetail || null,
+ };
+}
+
+function pickBetterCoverageStatus(left, right) {
+ const priorities = {
+ FOUND: 3,
+ NOT_FOUND: 2,
+ CHECKING: 1,
+ };
+ const leftPriority = priorities[left] || 0;
+ const rightPriority = priorities[right] || 0;
+ return rightPriority > leftPriority ? right : left;
+}
+
+function pickRicherVulnerabilityData(left, right) {
+ if (!left) {
+ return right || null;
+ }
+ if (!right) {
+ return left;
+ }
+ if (Boolean(right.detailsLoaded) !== Boolean(left.detailsLoaded)) {
+ return right.detailsLoaded ? right : left;
+ }
+ return (right.count || 0) > (left.count || 0) ? right : left;
+}
+
+function pickRicherPolicyData(left, right) {
+ if (!left) {
+ return right || null;
+ }
+ if (!right) {
+ return left;
+ }
+ if (Boolean(right.denied || right.quarantined) !== Boolean(left.denied || left.quarantined)) {
+ return right.denied || right.quarantined ? right : left;
+ }
+ if (Boolean(right.statusReason) !== Boolean(left.statusReason)) {
+ return right.statusReason ? right : left;
+ }
+ return right.violated ? right : left;
+}
+
+function compareComplianceVulnerabilityRows(left, right) {
+ const severityDelta = severitySortWeight(left.maxSeverity) - severitySortWeight(right.maxSeverity);
+ if (severityDelta !== 0) {
+ return severityDelta;
+ }
+
+ if (left.isDirect !== right.isDirect) {
+ return left.isDirect ? -1 : 1;
+ }
+
+ return compareNamedRows(left, right);
+}
+
+function compareCompliancePolicyRows(left, right) {
+ const statusDelta = policyStatusSortWeight(left.status) - policyStatusSortWeight(right.status);
+ if (statusDelta !== 0) {
+ return statusDelta;
+ }
+ return compareNamedRows(left, right);
+}
+
+function compareComplianceUncoveredRows(left, right) {
+ const statusDelta = upstreamStatusSortWeight(left.upstreamStatus) - upstreamStatusSortWeight(right.upstreamStatus);
+ if (statusDelta !== 0) {
+ return statusDelta;
+ }
+ return compareNamedRows(left, right);
+}
+
+function compareNamedRows(left, right) {
+ const nameDelta = String(left.name || "").localeCompare(String(right.name || ""), undefined, { sensitivity: "base" });
+ if (nameDelta !== 0) {
+ return nameDelta;
+ }
+ return String(left.version || "").localeCompare(String(right.version || ""), undefined, { sensitivity: "base" });
+}
+
+function severitySortWeight(severity) {
+ switch (severity) {
+ case "Critical":
+ return 0;
+ case "High":
+ return 1;
+ case "Medium":
+ return 2;
+ case "Low":
+ return 3;
+ default:
+ return 4;
+ }
+}
+
+function upstreamStatusSortWeight(status) {
+ switch (status) {
+ case "reachable":
+ return 0;
+ case "no_proxy":
+ return 1;
+ case "unreachable":
+ return 2;
+ default:
+ return 3;
+ }
+}
+
+function policyStatusSortWeight(status) {
+ switch (status) {
+ case "Quarantined":
+ return 0;
+ case "Denied":
+ return 1;
+ case "Policy violation":
+ return 2;
+ default:
+ return 3;
+ }
+}
+
+function humanizeLicenseClassification(classification) {
+ switch (classification) {
+ case "restrictive":
+ return "Restrictive";
+ case "weak_copyleft":
+ return "Weak copyleft";
+ default:
+ return "Unclassified";
+ }
+}
+
+function humanizePolicyStatus(policy) {
+ if (policy.quarantined) {
+ return "Quarantined";
+ }
+ if (policy.denied) {
+ return "Denied";
+ }
+ if (policy.status && policy.status !== "Completed") {
+ return policy.status;
+ }
+ return "Policy violation";
+}
+
+function defaultPolicyDetail(policy) {
+ if (policy.denied || policy.quarantined) {
+ return "Blocked by Cloudsmith policy.";
+ }
+ return "Policy requirements were not met.";
+}
+
+function defaultUpstreamDetail(status) {
+ switch (status) {
+ case "reachable":
+ return "Available via an upstream proxy.";
+ case "no_proxy":
+ return "No upstream proxy is configured for this ecosystem.";
+ case "unreachable":
+ return "Configured upstreams could not serve this package.";
+ default:
+ return "Not found in Cloudsmith.";
+ }
+}
+
+function buildDependencyHealthReport(projectName, dependencies, summary, generatedDate) {
+ const vulnerableDependencies = dependencies
+ .filter((dependency) => dependency.vulnerabilities && dependency.vulnerabilities.count > 0)
+ .sort((left, right) => compareDependencies(left, right, SORT_MODES.SEVERITY, false));
+ const uncoveredDependencies = dependencies
+ .filter((dependency) => dependency.cloudsmithStatus === "NOT_FOUND")
+ .sort((left, right) => compareDependencies(left, right, SORT_MODES.COVERAGE, false));
+ const policyViolations = dependencies
+ .filter((dependency) => dependency.policy && dependency.policy.violated)
+ .sort((left, right) => compareDependencies(left, right, SORT_MODES.SEVERITY, false));
+
+ const lines = [
+ `# Dependency Health Report — ${projectName}`,
+ `Generated: ${generatedDate}`,
+ "",
+ "## Summary",
+ `- ${summary.total} total dependencies (${summary.direct} direct, ${summary.transitive} transitive)`,
+ `- ${summary.found} served by Cloudsmith (${summary.coveragePercent}% coverage)`,
+ ];
+
+ if (summary.notFound > 0) {
+ lines.push(`- ${summary.notFound} not found in Cloudsmith`);
+ }
+
+ if (summary.vulnerable > 0) {
+ const severityParts = ["Critical", "High", "Medium", "Low"]
+ .filter((severity) => summary.severityCounts[severity] > 0)
+ .map((severity) => `${summary.severityCounts[severity]} ${severity}`);
+ lines.push(`- ${summary.vulnerable} with known vulnerabilities (${severityParts.join(", ")})`);
+ }
+
+ lines.push("");
+ lines.push("## Vulnerable Dependencies");
+ if (vulnerableDependencies.length === 0) {
+ lines.push("None");
+ } else {
+ lines.push("| Package | Version | Type | Severity | CVEs | Fix Available |");
+ lines.push("|---------|---------|------|----------|------|---------------|");
+ for (const dependency of vulnerableDependencies) {
+ const fixEntry = (dependency.vulnerabilities.entries || []).find((entry) => entry.fixVersion);
+ const fixCell = fixEntry
+ ? `Yes (${fixEntry.fixVersion})`
+ : dependency.vulnerabilities.hasFixAvailable
+ ? "Yes"
+ : "No";
+ lines.push(`| ${dependency.name} | ${dependency.version || "—"} | ${dependency.isDirect ? "Direct" : "Transitive"} | ${dependency.vulnerabilities.maxSeverity || "Unknown"} | ${(dependency.vulnerabilities.cveIds || []).join(", ") || "—"} | ${fixCell} |`);
+ }
+ }
+
+ const licenseTotals = summary.permissiveLicenses + summary.weakCopyleftLicenses + summary.restrictiveLicenses + summary.unknownLicenses;
+ if (licenseTotals > 0) {
+ lines.push("");
+ lines.push("## License Summary");
+ lines.push(`- ${summary.permissiveLicenses} permissive`);
+ lines.push(`- ${summary.weakCopyleftLicenses} weak copyleft`);
+ lines.push(`- ${summary.restrictiveLicenses} restrictive`);
+ lines.push(`- ${summary.unknownLicenses} unknown`);
+ }
+
+ if (policyViolations.length > 0) {
+ lines.push("");
+ lines.push("## Policy Compliance");
+ for (const dependency of policyViolations) {
+ const reason = dependency.policy.denied ? "deny policy violated" : "policy violated";
+ lines.push(`- ${dependency.name} ${dependency.version || ""} — ${reason}`.trim());
+ }
+ }
+
+ if (uncoveredDependencies.length > 0) {
+ lines.push("");
+ lines.push("## Uncovered Dependencies");
+ lines.push("| Package | Version | Ecosystem | Upstream Status | Detail |");
+ lines.push("|---------|---------|-----------|-----------------|--------|");
+ for (const dependency of uncoveredDependencies) {
+ lines.push(`| ${dependency.name} | ${dependency.version || "—"} | ${dependency.format || dependency.ecosystem || "—"} | ${formatUpstreamStatus(dependency.upstreamStatus)} | ${dependency.upstreamDetail || "—"} |`);
+ }
+ }
+
+ return lines.join("\n");
+}
+
+function formatUpstreamStatus(status) {
+ switch (status) {
+ case "reachable":
+ return "Reachable";
+ case "no_proxy":
+ return "No proxy";
+ case "unreachable":
+ return "Unreachable";
+ default:
+ return "Unknown";
+ }
+}
+
+function getDependencyVulnerabilityData(dependency) {
+ if (dependency.vulnerabilities) {
+ return dependency.vulnerabilities;
+ }
+
+ const cloudsmithPackage = dependency.cloudsmithPackage;
+ if (!cloudsmithPackage) {
+ return null;
+ }
+
+ const count = Number(
+ cloudsmithPackage.vulnerability_scan_results_count
+ || cloudsmithPackage.num_vulnerabilities
+ || 0
+ );
+ if (!Number.isFinite(count) || count <= 0) {
+ return null;
+ }
+
+ return {
+ count,
+ maxSeverity: cloudsmithPackage.max_severity || null,
+ };
+}
+
+function getDependencyPolicyData(dependency) {
+ if (dependency.policy) {
+ return dependency.policy;
+ }
+
+ const cloudsmithPackage = dependency.cloudsmithPackage;
+ if (!cloudsmithPackage) {
+ return null;
+ }
+
+ const status = String(cloudsmithPackage.status_str || "").trim() || null;
+ const quarantined = status === "Quarantined";
+ const denied = quarantined || Boolean(cloudsmithPackage.deny_policy_violated);
+ const violated = denied
+ || Boolean(cloudsmithPackage.policy_violated)
+ || Boolean(cloudsmithPackage.license_policy_violated)
+ || Boolean(cloudsmithPackage.vulnerability_policy_violated);
+
+ return {
+ violated,
+ denied,
+ quarantined,
+ status,
+ statusReason: String(cloudsmithPackage.status_reason || "").trim() || null,
+ };
+}
+
+function getDependencyLicenseClassification(dependency) {
+ if (dependency.license && dependency.license.classification) {
+ return dependency.license.classification;
+ }
+
+ if (!dependency.cloudsmithPackage) {
+ return "unknown";
+ }
+
+ const inspection = LicenseClassifier.inspect(dependency.cloudsmithPackage);
+ switch (inspection.tier) {
+ case "permissive":
+ return "permissive";
+ case "cautious":
+ return "weak_copyleft";
+ case "restrictive":
+ return "restrictive";
+ default:
+ return "unknown";
+ }
}
-module.exports = { DependencyHealthProvider };
+module.exports = {
+ DependencyHealthProvider,
+ FILTER_MODES,
+ SORT_MODES,
+ buildComplianceReportData,
+ buildDependencyHealthReport,
+ buildDependencySummary,
+ buildFilteredTreeWrapper,
+ buildPackageIndex,
+ findCoverageMatch,
+ getFilterLabel,
+ matchesFilter,
+ matchCoverageCandidates,
+};