diff --git a/CHANGELOG.md b/CHANGELOG.md
index f4e4df0..1c91ab7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,40 @@
+## 2.2.0 - April 2026
+### Transitive Dependency Visibility
+
+#### Transitive Dependency Resolution
+- Dependency Health view now resolves the complete dependency set (direct and transitive) by parsing lockfiles and manifests directly.
+- Ecosystems with lockfiles (npm, Yarn, pnpm, Python/Poetry/uv, Rust, Ruby, Go, NuGet, Dart, PHP, Helm, Swift, Hex) resolve the full transitive tree automatically.
+- Maven and Gradle resolve direct dependencies from pom.xml and build.gradle. The extension prompts when a tree file is not found.
+- Docker and Helm resolve direct dependencies (base images and chart dependencies).
+- Summary bar shows total dependency count with direct/transitive breakdown and per-ecosystem composition for multi-ecosystem projects.
+
+#### Vulnerability, License, and Policy Overlays
+- Resolved dependencies found in Cloudsmith are checked for known vulnerabilities with severity indicators displayed inline.
+- License classification (permissive, weak copyleft, restrictive) and policy compliance status shown for all covered dependencies.
+- Summary bar aggregates vulnerability counts by severity, restrictive license count, and policy violation count.
+
+#### Upstream Proxy Gap Analysis
+- Dependencies not found in Cloudsmith are checked against configured upstream proxies across all repositories.
+- Each uncovered dependency shows whether it's reachable via an existing upstream or requires a new proxy to be configured.
+
+#### Tree Visualization
+- New tree view mode displays the full dependency hierarchy with collapsible parent-child relationships.
+- Diamond dependencies collapsed on subsequent occurrences to prevent exponential tree growth.
+- Filters prune the tree to show only vulnerable, uncovered, or restrictive-license dependency paths.
+- Three-way view toggle: direct only, all (flat), or all (tree).
+
+#### Pull Dependencies Through Upstream
+- New pull action caches uncovered dependencies through a selected repository's upstream proxy directly from the editor.
+- Repository selector shows only repositories with upstream proxies matching the project's dependency formats.
+- Right-click any individual uncovered dependency to pull just that package through an upstream.
+
+#### Compliance Report
+- New report view opens a dependency health summary in a dedicated editor panel with coverage, vulnerability, license, policy, and upstream gap analysis.
+
+#### UX Improvements
+- Toolbar consolidated to five inline actions: scan, pull, view mode cycle, sort and filter, and compliance report.
+- Format-specific icons displayed on uncovered dependencies for at-a-glance ecosystem identification.
+
## 2.1.1 - April 2026
### Fixed
- Fixed erroneous error banner displaying in the Upstream Webview when all upstream data loaded successfully.
diff --git a/README.md b/README.md
index 8d05fbe..f008842 100644
--- a/README.md
+++ b/README.md
@@ -80,12 +80,12 @@ The right-click menu provides access to the following commands, varying dependin
- **Inspect package** — View the full raw JSON API response for the package.
- **Copy Install Command** — Copy the installation command for the package to the clipboard.
-- **Show Install Command** - Show the installation command for the package.
-- **Show vulnerabilities** - Open a webview showing the vulnerabilities report for a package.
-- **View package in Cloudsmith** - Open the package page in the Cloudsmith web UI for the configured workspace.
-- **Promote Package** - Promote the package between configured repositories.
-- **Show Promotion Status** - Show the current status of the package promotion request.
-- **Find safe version** - Show possible safe versions of the package within Cloudsmith for quick remediation.
+- **Show Install Command** - Show the installation command for the package.
+- **Show vulnerabilities** - Open a webview showing the vulnerabilities report for a package.
+- **View package in Cloudsmith** - Open the package page in the Cloudsmith web UI for the configured workspace.
+- **Promote Package** - Promote the package between configured repositories.
+- **Show Promotion Status** - Show the current status of the package promotion request.
+- **Find safe version** - Show possible safe versions of the package within Cloudsmith for quick remediation.
@@ -109,6 +109,54 @@ If you have access to multiple workspaces, the explorer lets you switch between
View vulnerability data associated with packages directly in the explorer, including security scan results when available.
+### Dependency Health
+
+The Dependency Health view scans your project's manifest and lockfiles, cross-references every declared and transitive dependency against your Cloudsmith workspace, and shows coverage, vulnerability, license, and policy status at a glance.
+
+#### Transitive Resolution
+
+The extension parses lockfiles and manifests directly. Most ecosystems resolve the full dependency tree from an existing lockfile. For ecosystems without a standard lockfile, the extension parses the manifest for direct dependencies and can optionally parse a generated dependency tree for transitives.
+
+| Ecosystem | Automatic (lockfile) | Direct only (manifest) | Notes |
+|-----------|---------------------|----------------------|-------|
+| npm / Yarn / pnpm | package-lock.json, yarn.lock, pnpm-lock.yaml | package.json | |
+| Python | poetry.lock, uv.lock, Pipfile.lock | pyproject.toml, requirements.txt | requirements.txt provides direct deps only |
+| Maven | | pom.xml | Run `mvn dependency:tree -DoutputFile=dependency-tree.txt` once to enable transitive resolution |
+| Gradle | gradle.lockfile | build.gradle, build.gradle.kts | Run `gradle dependencies` once if dependency locking is not enabled |
+| Go | go.mod | | go.mod marks direct vs indirect natively |
+| Rust | Cargo.lock | Cargo.toml | |
+| Ruby | Gemfile.lock | Gemfile | |
+| Docker | | Dockerfile, docker-compose.yml | All dependencies are direct (base images) |
+| NuGet | packages.lock.json | | |
+| Dart | pubspec.lock | | |
+| PHP | composer.lock | | |
+| Helm | Chart.lock | | Helm dependencies are all direct |
+| Swift | Package.resolved | | |
+| Elixir | mix.lock | | |
+
+#### View Modes
+
+- **Direct only** — shows only top-level manifest dependencies.
+- **All (flat)** — shows every resolved dependency in a flat list with direct/transitive labels.
+- **All (tree)** — shows the full dependency hierarchy. Diamond dependencies are collapsed to keep the tree manageable.
+
+#### Overlays
+
+Each dependency found in Cloudsmith is enriched with:
+- **Vulnerability status** — severity count and max severity inline, with click-through to CVE details.
+- **License classification** — permissive, weak copyleft, or restrictive, with configurable flagging.
+- **Policy compliance** — quarantine and policy violation indicators.
+
+Dependencies not found in Cloudsmith show upstream proxy reachability — whether a configured upstream could serve them.
+
+#### Pull Through Upstream
+
+Click "Pull dependencies" to cache uncovered dependencies through a repository's upstream proxy. The extension shows only repositories with matching upstream formats, pulls in parallel, and automatically rescans after completion. You can also right-click any individual dependency to pull just that one package.
+
+#### Compliance Report
+
+The report view opens a styled summary panel with coverage percentage, vulnerability breakdown by severity, license risk summary, policy compliance, and upstream gap analysis.
+
### Configuration & Settings
The extension exposes several settings under `cloudsmith-vsc.*`:
@@ -121,13 +169,16 @@ The extension exposes several settings under `cloudsmith-vsc.*`:
| `cloudsmith-vsc.defaultWorkspace` | Cloudsmith workspace slug to load by default. Leave empty to show all accessible workspaces. |
| `cloudsmith-vsc.showPermissibilityIndicators` | Show visual indicators for quarantined packages and policy violations. Default: `true`. |
| `cloudsmith-vsc.showLicenseIndicators` | Show license risk classification on packages. Default: `true`. |
+| `cloudsmith-vsc.flagRestrictiveLicenses` | Color-code restrictive licenses in the Dependency Health view. Default: `true`. |
+| `cloudsmith-vsc.restrictiveLicenses` | List of SPDX license identifiers flagged as restrictive. Default: `["AGPL-3.0", "GPL-2.0", "GPL-3.0", "SSPL-1.0"]`. |
| `cloudsmith-vsc.showDockerDigestCommand` | Show an additional "Pull by digest" option for Docker install commands. Default: `false`. |
| `cloudsmith-vsc.experimentalSSOBrowser` | Enable experimental browser-based SSO authentication. Default: `false`. |
-| `cloudsmith-vsc.useLegacyWebApp` | Use the legacy `cloudsmith.io` webapp for platform links. Default: `false`. |
| `cloudsmith-vsc.autoScanOnOpen` | Automatically scan project dependencies against Cloudsmith when a workspace is opened. Default: `false`. |
| `cloudsmith-vsc.dependencyScanWorkspace` | Cloudsmith workspace slug to use for dependency health scanning. |
| `cloudsmith-vsc.dependencyScanRepo` | Cloudsmith repository slug to use for dependency health scanning. |
-| `cloudsmith-vsc.resolveTransitiveDependencies` | Resolve transitive (indirect) dependencies using the package manager CLI. Default: `false`. |
+| `cloudsmith-vsc.resolveTransitiveDependencies` | Parse lockfiles to resolve transitive dependencies. When disabled, only direct manifest dependencies are shown. Default: `true`. |
+| `cloudsmith-vsc.dependencyTreeDefaultView` | Default view mode for the Dependency Health panel: `direct`, `flat`, or `tree`. Default: `flat`. |
+| `cloudsmith-vsc.maxDependenciesToScan` | Maximum number of dependencies to display. Pull operations always process all dependencies regardless of this limit. Default: `10000`. |
| `cloudsmith-vsc.searchPageSize` | Number of results per page when searching packages (10–100). Default: `50`. |
| `cloudsmith-vsc.recentSearches` | Number of recent searches to remember (0–50). Default: `10`. |
@@ -145,8 +196,14 @@ All commands are available via the Command Palette (`Cmd+Shift+P`):
| `Cloudsmith: Copy to Clipboard` | Copy a package detail value to the clipboard. |
| `Cloudsmith: Refresh Packages` | Refresh the Cloudsmith explorer tree. |
| `Cloudsmith: Search Packages` | Search for packages within a repository. |
+| `Cloudsmith: Scan Dependencies` | Scan project lockfiles and check dependency coverage against Cloudsmith. |
+| `Cloudsmith: Pull Dependencies` | Pull uncovered dependencies through a repository's upstream proxy. |
+| `Cloudsmith: Pull Dependency` | Pull a single dependency through an upstream proxy (right-click context menu). |
+| `Cloudsmith: View Compliance Report` | Open the dependency health compliance report in an editor panel. |
+| `Cloudsmith: Cycle Dependency View` | Switch between direct, flat, and tree view modes. |
+| `Cloudsmith: Sort and Filter Dependencies` | Open sort and filter options for the Dependency Health view. |
## License
-Apache 2.0
+Apache 2.0
\ No newline at end of file
diff --git a/extension.js b/extension.js
index 3d7cb23..0e08aac 100644
--- a/extension.js
+++ b/extension.js
@@ -6,9 +6,14 @@ const { CloudsmithAPI } = require("./util/cloudsmithAPI");
const { CredentialManager } = require("./util/credentialManager");
const { RecentSearches } = require("./util/recentSearches");
const { RemediationHelper } = require("./util/remediationHelper");
-const { DependencyHealthProvider } = require("./views/dependencyHealthProvider");
+const {
+ DependencyHealthProvider,
+ FILTER_MODES,
+ SORT_MODES,
+} = require("./views/dependencyHealthProvider");
const { InstallCommandBuilder } = require("./util/installCommandBuilder");
const { VulnerabilityProvider } = require("./views/vulnerabilityProvider");
+const { ComplianceReportProvider } = require("./views/complianceReportProvider");
const { QuarantineExplainProvider } = require("./views/quarantineExplainProvider");
const { DiagnosticsPublisher } = require("./util/diagnosticsPublisher");
const { SSOAuthManager } = require("./util/ssoAuthManager");
@@ -322,6 +327,223 @@ function buildPresetQuery(preset, customQuery) {
return builder.build();
}
+async function resolveDependencyScanTarget(context) {
+ const config = vscode.workspace.getConfiguration("cloudsmith-vsc");
+ let scanWorkspace = config.get("dependencyScanWorkspace");
+ let scanRepo = config.get("dependencyScanRepo") || null;
+
+ if (!scanWorkspace) {
+ scanWorkspace = getDefaultWorkspace();
+ }
+
+ if (scanWorkspace) {
+ return {
+ scanWorkspace,
+ scanRepo,
+ };
+ }
+
+ const workspaces = await getWorkspaces(context);
+ if (!workspaces) {
+ return null;
+ }
+ if (workspaces.length === 0) {
+ vscode.window.showErrorMessage("No workspaces found. Connect to Cloudsmith first.");
+ return null;
+ }
+
+ const selectedWorkspace = await vscode.window.showQuickPick(
+ workspaces.map((workspace) => ({
+ label: workspace.name,
+ description: workspace.slug,
+ })),
+ {
+ placeHolder: "Select a Cloudsmith workspace for the scan",
+ }
+ );
+
+ if (!selectedWorkspace) {
+ return null;
+ }
+
+ scanWorkspace = selectedWorkspace.description;
+
+ const selectedScope = await vscode.window.showQuickPick(
+ [
+ {
+ label: "All repositories",
+ description: "Search across the entire workspace",
+ _all: true,
+ },
+ {
+ label: "Select a specific repository",
+ description: "Search one repository",
+ _all: false,
+ },
+ ],
+ {
+ placeHolder: "Select a scan scope",
+ }
+ );
+
+ if (!selectedScope) {
+ return null;
+ }
+
+ if (!selectedScope._all) {
+ const cloudsmithAPI = new CloudsmithAPI(context);
+ const repos = await cloudsmithAPI.get(`repos/${scanWorkspace}/?sort=name`);
+ if (typeof repos !== "string" && Array.isArray(repos) && repos.length > 0) {
+ const selectedRepo = await vscode.window.showQuickPick(
+ repos.map((repository) => ({
+ label: repository.name,
+ description: repository.slug,
+ })),
+ {
+ placeHolder: "Select a repository",
+ }
+ );
+
+ if (selectedRepo) {
+ scanRepo = selectedRepo.description;
+ }
+ }
+ }
+
+ return {
+ scanWorkspace,
+ scanRepo,
+ };
+}
+
+function buildDependencySortFilterItems(provider) {
+ const currentSort = provider.getSortMode();
+ const currentFilter = provider.getFilterMode();
+ return [
+ {
+ label: "Sort",
+ kind: vscode.QuickPickItemKind.Separator,
+ },
+ createDependencyPickerItem(
+ "Alphabetical",
+ "Default ordering",
+ "sort",
+ SORT_MODES.ALPHABETICAL,
+ currentSort === SORT_MODES.ALPHABETICAL
+ ),
+ createDependencyPickerItem(
+ "Severity",
+ "Most severe first",
+ "sort",
+ SORT_MODES.SEVERITY,
+ currentSort === SORT_MODES.SEVERITY
+ ),
+ createDependencyPickerItem(
+ "Coverage",
+ "Not found first",
+ "sort",
+ SORT_MODES.COVERAGE,
+ currentSort === SORT_MODES.COVERAGE
+ ),
+ {
+ label: "Filters",
+ kind: vscode.QuickPickItemKind.Separator,
+ },
+ createDependencyPickerItem(
+ "Vulnerable only",
+ "Toggle vulnerable dependencies",
+ "filter",
+ FILTER_MODES.VULNERABLE,
+ currentFilter === FILTER_MODES.VULNERABLE
+ ),
+ createDependencyPickerItem(
+ "Not in Cloudsmith",
+ "Toggle uncovered dependencies",
+ "filter",
+ FILTER_MODES.UNCOVERED,
+ currentFilter === FILTER_MODES.UNCOVERED
+ ),
+ createDependencyPickerItem(
+ "Restrictive licenses",
+ "Toggle restrictive or weak copyleft results",
+ "filter",
+ FILTER_MODES.RESTRICTIVE_LICENSE,
+ currentFilter === FILTER_MODES.RESTRICTIVE_LICENSE
+ ),
+ createDependencyPickerItem(
+ "Policy violations",
+ "Toggle policy failures",
+ "filter",
+ FILTER_MODES.POLICY_VIOLATION,
+ currentFilter === FILTER_MODES.POLICY_VIOLATION
+ ),
+ createDependencyPickerItem(
+ "Show all dependencies",
+ "Clear active dependency filters",
+ "filter",
+ null,
+ currentFilter === null
+ ),
+ ];
+}
+
+function createDependencyPickerItem(label, description, action, value, active) {
+ return {
+ label: `${active ? "$(check)" : "$(circle-large-outline)"} ${label}`,
+ description,
+ _action: action,
+ _value: value,
+ };
+}
+
+async function showDependencySortFilterPicker(provider) {
+ await new Promise((resolve) => {
+ const quickPick = vscode.window.createQuickPick();
+ const disposables = [];
+
+ const refreshItems = () => {
+ quickPick.items = buildDependencySortFilterItems(provider);
+ };
+
+ quickPick.title = "Sort & filter dependencies";
+ quickPick.matchOnDescription = true;
+ quickPick.ignoreFocusOut = true;
+ refreshItems();
+
+ disposables.push(quickPick.onDidAccept(async () => {
+ const selected = quickPick.selectedItems[0];
+ if (!selected || !selected._action) {
+ quickPick.hide();
+ return;
+ }
+
+ quickPick.busy = true;
+ try {
+ if (selected._action === "sort") {
+ provider.setSortMode(selected._value);
+ } else if (selected._value === null || provider.getFilterMode() === selected._value) {
+ await provider.clearFilter();
+ } else {
+ await provider.setFilterMode(selected._value);
+ }
+ refreshItems();
+ } finally {
+ quickPick.busy = false;
+ }
+ }));
+
+ disposables.push(quickPick.onDidHide(() => {
+ quickPick.dispose();
+ for (const disposable of disposables) {
+ disposable.dispose();
+ }
+ resolve();
+ }));
+
+ quickPick.show();
+ });
+}
+
/**
* @param {vscode.ExtensionContext} context
@@ -386,7 +608,7 @@ async function activate(context) {
const dependencyHealthProvider = new DependencyHealthProvider(context, diagnosticsPublisher);
vscode.window.createTreeView("cloudsmithDependencyHealthView", {
treeDataProvider: dependencyHealthProvider,
- showCollapseAll: true,
+ showCollapseAll: false,
});
context.subscriptions.push(
@@ -413,6 +635,10 @@ async function activate(context) {
const vulnerabilityProvider = new VulnerabilityProvider(context);
context.subscriptions.push({ dispose: () => vulnerabilityProvider.dispose() });
+ // Create compliance report WebView provider
+ const complianceReportProvider = new ComplianceReportProvider(context);
+ context.subscriptions.push({ dispose: () => complianceReportProvider.dispose() });
+
// Create quarantine explanation WebView provider
const quarantineExplainProvider = new QuarantineExplainProvider(context);
context.subscriptions.push({ dispose: () => quarantineExplainProvider.dispose() });
@@ -447,28 +673,6 @@ async function activate(context) {
void initializeConnectionContext();
- // Auto-scan dependencies on open if configured
- const autoScanConfig = vscode.workspace.getConfiguration("cloudsmith-vsc");
- if (autoScanConfig.get("autoScanOnOpen")) {
- const scanWorkspace = autoScanConfig.get("dependencyScanWorkspace");
- if (scanWorkspace) {
- const scanRepo = autoScanConfig.get("dependencyScanRepo") || null;
- // Delay to avoid blocking VS Code startup
- setTimeout(() => {
- dependencyHealthProvider.scan(scanWorkspace, scanRepo);
- }, 2000);
- } else {
- vscode.window.showInformationMessage(
- "Auto-scan is enabled but no Cloudsmith workspace is configured.",
- "Configure"
- ).then((selection) => {
- if (selection === "Configure") {
- vscode.commands.executeCommand("workbench.action.openSettings", "cloudsmith-vsc.dependencyScanWorkspace");
- }
- });
- }
- }
-
// Shared post-authentication handler: connect, refresh all views, and prompt
// to set default workspace if only one workspace is available.
@@ -1331,6 +1535,14 @@ async function activate(context) {
await vulnerabilityProvider.show(item);
}),
+ vscode.commands.registerCommand("cloudsmith-vsc.showDepVulnerabilities", async (item) => {
+ await vscode.commands.executeCommand("cloudsmith-vsc.showVulnerabilities", item);
+ }),
+
+ vscode.commands.registerCommand("cloudsmith-vsc.findDepSafeVersion", async (item) => {
+ await vscode.commands.executeCommand("cloudsmith-vsc.findSafeVersion", item);
+ }),
+
// Register vulnerability filter command — updates a summary node in-place
vscode.commands.registerCommand("cloudsmith-vsc.filterVulnerabilities", async (vulnSummaryNode) => {
if (!vulnSummaryNode ||
@@ -1423,99 +1635,99 @@ async function activate(context) {
// Register scan dependencies command
vscode.commands.registerCommand("cloudsmith-vsc.scanDependencies", async () => {
- const config = vscode.workspace.getConfiguration("cloudsmith-vsc");
- let scanWorkspace = config.get("dependencyScanWorkspace");
- let scanRepo = config.get("dependencyScanRepo") || null;
+ if (dependencyHealthProvider.lastWorkspace) {
+ await dependencyHealthProvider.rescan();
+ return;
+ }
- // If no dedicated scan workspace, try the default workspace setting
- if (!scanWorkspace) {
- scanWorkspace = getDefaultWorkspace();
+ const scanTarget = await resolveDependencyScanTarget(context);
+ if (!scanTarget) {
+ return;
}
- // If still no workspace, prompt user
- if (!scanWorkspace) {
- const workspaces = await getWorkspaces(context);
- if (!workspaces) {
- return;
- }
- if (workspaces.length === 0) {
- vscode.window.showErrorMessage("No workspaces found. Connect to Cloudsmith first.");
- return;
- }
+ await dependencyHealthProvider.scan(scanTarget.scanWorkspace, scanTarget.scanRepo);
+ }),
- const wsItems = workspaces.map(ws => ({ label: ws.name, description: ws.slug }));
- const selectedWs = await vscode.window.showQuickPick(wsItems, {
- placeHolder: "Select a Cloudsmith workspace for the scan",
- });
- if (!selectedWs) {
- return;
- }
- scanWorkspace = selectedWs.description;
+ vscode.commands.registerCommand("cloudsmith-vsc.scanDependenciesPending", async () => {
+ await vscode.commands.executeCommand("cloudsmith-vsc.scanDependencies");
+ }),
- // Optionally select a repo
- const scopeItems = [
- { label: "All repositories", description: "Search across the entire workspace", _all: true },
- { label: "Select a specific repository", description: "Search one repository" },
- ];
- const selectedScope = await vscode.window.showQuickPick(scopeItems, {
- placeHolder: "Select a scan scope",
- });
- if (!selectedScope) {
- return;
- }
+ vscode.commands.registerCommand("cloudsmith-vsc.scanDependenciesComplete", async () => {
+ await vscode.commands.executeCommand("cloudsmith-vsc.scanDependencies");
+ }),
- if (!selectedScope._all) {
- const cloudsmithAPI = new CloudsmithAPI(context);
- const repos = await cloudsmithAPI.get(`repos/${scanWorkspace}/?sort=name`);
- if (typeof repos !== "string" && Array.isArray(repos) && repos.length > 0) {
- const repoItems = repos.map(r => ({ label: r.name, description: r.slug }));
- const selectedRepo = await vscode.window.showQuickPick(repoItems, {
- placeHolder: "Select a repository",
- });
- if (selectedRepo) {
- scanRepo = selectedRepo.description;
- }
- }
- }
- }
+ vscode.commands.registerCommand("cloudsmith-vsc.pullDependencies", async () => {
+ await dependencyHealthProvider.pullDependencies();
+ }),
- // Resolve project folder: stored path > workspace folder > prompt
- // The provider handles the prompt internally if no folder is available
- await dependencyHealthProvider.scan(scanWorkspace, scanRepo);
+ vscode.commands.registerCommand("cloudsmith-vsc.pullSingleDependency", async (item) => {
+ await dependencyHealthProvider.pullSingleDependency(item);
}),
- // Register rescan dependencies command
- vscode.commands.registerCommand("cloudsmith-vsc.rescanDependencies", async () => {
- await dependencyHealthProvider.rescan();
+ vscode.commands.registerCommand("cloudsmith-vsc.cycleDepView", async () => {
+ await dependencyHealthProvider.cycleViewMode();
}),
- // Register change dependency folder command
- vscode.commands.registerCommand("cloudsmith-vsc.changeDependencyFolder", async () => {
- const selected = await vscode.window.showOpenDialog({
- canSelectFolders: true,
- canSelectFiles: false,
- canSelectMany: false,
- openLabel: "Select project folder to scan",
- });
+ vscode.commands.registerCommand("cloudsmith-vsc.cycleDepViewDirect", async () => {
+ await vscode.commands.executeCommand("cloudsmith-vsc.cycleDepView");
+ }),
- if (!selected || selected.length === 0) {
- return;
- }
+ vscode.commands.registerCommand("cloudsmith-vsc.cycleDepViewFlat", async () => {
+ await vscode.commands.executeCommand("cloudsmith-vsc.cycleDepView");
+ }),
- dependencyHealthProvider.setProjectFolder(selected[0].fsPath);
+ vscode.commands.registerCommand("cloudsmith-vsc.cycleDepViewTree", async () => {
+ await vscode.commands.executeCommand("cloudsmith-vsc.cycleDepView");
+ }),
- // Re-run scan if we have a previous workspace context
- if (dependencyHealthProvider.lastWorkspace) {
- await dependencyHealthProvider.scan(
- dependencyHealthProvider.lastWorkspace,
- dependencyHealthProvider.lastRepo
- );
- } else {
- vscode.window.showInformationMessage(
- `Project folder set to ${selected[0].fsPath}. Run "Scan dependencies" to check against Cloudsmith.`
- );
- dependencyHealthProvider.refresh();
+ vscode.commands.registerCommand("cloudsmith-vsc.depViewDirect", async () => {
+ await dependencyHealthProvider.setViewMode("direct");
+ }),
+
+ vscode.commands.registerCommand("cloudsmith-vsc.depViewFlat", async () => {
+ await dependencyHealthProvider.setViewMode("flat");
+ }),
+
+ vscode.commands.registerCommand("cloudsmith-vsc.depViewTree", async () => {
+ await dependencyHealthProvider.setViewMode("tree");
+ }),
+
+ vscode.commands.registerCommand("cloudsmith-vsc.depFilterVulnerable", async () => {
+ await dependencyHealthProvider.setFilterMode(FILTER_MODES.VULNERABLE);
+ }),
+
+ vscode.commands.registerCommand("cloudsmith-vsc.depFilterUncovered", async () => {
+ await dependencyHealthProvider.setFilterMode(FILTER_MODES.UNCOVERED);
+ }),
+
+ vscode.commands.registerCommand("cloudsmith-vsc.depFilterRestrictiveLicense", async () => {
+ await dependencyHealthProvider.setFilterMode(FILTER_MODES.RESTRICTIVE_LICENSE);
+ }),
+
+ vscode.commands.registerCommand("cloudsmith-vsc.depFilterPolicyViolation", async () => {
+ await dependencyHealthProvider.setFilterMode(FILTER_MODES.POLICY_VIOLATION);
+ }),
+
+ vscode.commands.registerCommand("cloudsmith-vsc.depFilterClear", async () => {
+ await dependencyHealthProvider.clearFilter();
+ }),
+
+ vscode.commands.registerCommand("cloudsmith-vsc.depSortFilter", async () => {
+ await showDependencySortFilterPicker(dependencyHealthProvider);
+ }),
+
+ vscode.commands.registerCommand("cloudsmith-vsc.depSortFilterActive", async () => {
+ await vscode.commands.executeCommand("cloudsmith-vsc.depSortFilter");
+ }),
+
+ vscode.commands.registerCommand("cloudsmith-vsc.viewComplianceReport", async () => {
+ const reportData = dependencyHealthProvider.getReportData();
+ if (!reportData) {
+ vscode.window.showInformationMessage("Run a dependency scan before opening the report.");
+ return;
}
+
+ complianceReportProvider.show(reportData);
}),
// Register copy install command
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/models/packageNode.js b/models/packageNode.js
index b378815..6d44ce6 100644
--- a/models/packageNode.js
+++ b/models/packageNode.js
@@ -1,8 +1,8 @@
// Package node treeview
const vscode = require("vscode");
-const path = require("path");
const { LicenseClassifier } = require("../util/licenseClassifier");
+const { getFormatIconPath } = require("../util/formatIcons");
class PackageNode {
constructor(pkg, context) {
@@ -117,8 +117,7 @@ class PackageNode {
if (format === "raw") {
return new vscode.ThemeIcon("file-binary");
}
- const iconURI = "file_type_" + format + ".svg";
- return path.join(__filename, "..", "..", "media", "vscode_icons", iconURI);
+ return getFormatIconPath(format, this.context && this.context.extensionPath);
}
_buildTooltip() {
diff --git a/package.json b/package.json
index 834a64d..d1cbbfd 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
"publisher": "Cloudsmith",
"displayName": "Cloudsmith VS Code",
"description": "Access packages from a Cloudsmith instance.",
- "version": "2.1.1",
+ "version": "2.2.0",
"license": "SEE LICENSE IN LICENSE",
"homepage": "https://github.com/cloudsmith-io/cloudsmith-vscode-extension/blob/main/README.md",
"bugs": {
@@ -172,6 +172,16 @@
"title": "Show vulnerabilities",
"category": "Cloudsmith"
},
+ {
+ "command": "cloudsmith-vsc.showDepVulnerabilities",
+ "title": "Show vulnerabilities",
+ "category": "Cloudsmith"
+ },
+ {
+ "command": "cloudsmith-vsc.findDepSafeVersion",
+ "title": "Find safe version",
+ "category": "Cloudsmith"
+ },
{
"command": "cloudsmith-vsc.filterVulnerabilities",
"title": "Filter vulnerabilities",
@@ -190,16 +200,118 @@
"icon": "$(play)"
},
{
- "command": "cloudsmith-vsc.rescanDependencies",
+ "command": "cloudsmith-vsc.scanDependenciesPending",
+ "title": "Scan dependencies",
+ "category": "Cloudsmith",
+ "icon": "$(play)"
+ },
+ {
+ "command": "cloudsmith-vsc.scanDependenciesComplete",
"title": "Rescan dependencies",
"category": "Cloudsmith",
- "icon": "$(refresh)"
+ "icon": "$(play)"
},
{
- "command": "cloudsmith-vsc.changeDependencyFolder",
- "title": "Change project folder",
+ "command": "cloudsmith-vsc.pullDependencies",
+ "title": "Pull dependencies",
"category": "Cloudsmith",
- "icon": "$(folder-opened)"
+ "icon": "$(cloud-download)"
+ },
+ {
+ "command": "cloudsmith-vsc.pullSingleDependency",
+ "title": "Pull Dependency",
+ "category": "Cloudsmith",
+ "icon": "$(cloud-download)"
+ },
+ {
+ "command": "cloudsmith-vsc.cycleDepView",
+ "title": "Cycle dependency view",
+ "category": "Cloudsmith",
+ "icon": "$(symbol-enum)"
+ },
+ {
+ "command": "cloudsmith-vsc.cycleDepViewDirect",
+ "title": "View: Direct only (click to switch)",
+ "category": "Cloudsmith",
+ "icon": "$(list-selection)"
+ },
+ {
+ "command": "cloudsmith-vsc.cycleDepViewFlat",
+ "title": "View: Flat list (click to switch)",
+ "category": "Cloudsmith",
+ "icon": "$(list-unordered)"
+ },
+ {
+ "command": "cloudsmith-vsc.cycleDepViewTree",
+ "title": "View: Dependency tree (click to switch)",
+ "category": "Cloudsmith",
+ "icon": "$(list-tree)"
+ },
+ {
+ "command": "cloudsmith-vsc.depViewDirect",
+ "title": "Show direct dependencies",
+ "category": "Cloudsmith",
+ "icon": "$(list-selection)"
+ },
+ {
+ "command": "cloudsmith-vsc.depViewFlat",
+ "title": "Show all dependencies (flat)",
+ "category": "Cloudsmith",
+ "icon": "$(list-unordered)"
+ },
+ {
+ "command": "cloudsmith-vsc.depViewTree",
+ "title": "Show dependency tree",
+ "category": "Cloudsmith",
+ "icon": "$(list-tree)"
+ },
+ {
+ "command": "cloudsmith-vsc.depFilterVulnerable",
+ "title": "Show only vulnerable",
+ "category": "Cloudsmith",
+ "icon": "$(warning)"
+ },
+ {
+ "command": "cloudsmith-vsc.depFilterUncovered",
+ "title": "Show only not in Cloudsmith",
+ "category": "Cloudsmith",
+ "icon": "$(package)"
+ },
+ {
+ "command": "cloudsmith-vsc.depFilterRestrictiveLicense",
+ "title": "Show only restrictive licenses",
+ "category": "Cloudsmith",
+ "icon": "$(law)"
+ },
+ {
+ "command": "cloudsmith-vsc.depFilterPolicyViolation",
+ "title": "Show only policy violations",
+ "category": "Cloudsmith",
+ "icon": "$(shield)"
+ },
+ {
+ "command": "cloudsmith-vsc.depFilterClear",
+ "title": "Clear dependency filters",
+ "category": "Cloudsmith",
+ "icon": "$(clear-all)"
+ },
+ {
+ "command": "cloudsmith-vsc.depSortFilter",
+ "title": "Sort & filter dependencies",
+ "category": "Cloudsmith",
+ "icon": "$(filter)"
+ },
+ {
+ "command": "cloudsmith-vsc.depSortFilterActive",
+ "title": "Sort & filter dependencies",
+ "category": "Cloudsmith",
+ "icon": "$(filter-filled)"
+ },
+ {
+ "command": "cloudsmith-vsc.viewComplianceReport",
+ "title": "View compliance report",
+ "category": "Cloudsmith",
+ "icon": "$(graph)"
},
{
"command": "cloudsmith-vsc.copyInstallCommand",
@@ -347,12 +459,12 @@
},
{
"command": "cloudsmith-vsc.inspectPackage",
- "when": "view == cloudsmithDependencyHealthView && viewItem =~ /^(dependencyHealth|dependencyHealthBlocked|dependencyHealthViolated|dependencyHealthSyncing)$/",
+ "when": "view == cloudsmithDependencyHealthView && viewItem =~ /^(dependencyHealthFound|dependencyHealthVulnerable|dependencyHealthQuarantined|dependencyHealthSyncing)$/",
"group": "navigation"
},
{
"command": "cloudsmith-vsc.openPackage",
- "when": "view == cloudsmithDependencyHealthView && viewItem =~ /^(dependencyHealth|dependencyHealthBlocked|dependencyHealthViolated|dependencyHealthSyncing)$/",
+ "when": "view == cloudsmithDependencyHealthView && viewItem =~ /^(dependencyHealthFound|dependencyHealthVulnerable|dependencyHealthQuarantined|dependencyHealthSyncing)$/",
"group": "navigation"
},
{
@@ -417,7 +529,7 @@
},
{
"command": "cloudsmith-vsc.findSafeVersion",
- "when": "viewItem =~ /^(package|packageQuarantined|dependencyHealthBlocked|dependencyHealthViolated)$/",
+ "when": "viewItem =~ /^(package|packageQuarantined)$/",
"group": "navigation"
},
{
@@ -427,7 +539,17 @@
},
{
"command": "cloudsmith-vsc.showVulnerabilities",
- "when": "viewItem =~ /^(package|packageQuarantined|dependencyHealthBlocked|dependencyHealthViolated)$/",
+ "when": "viewItem =~ /^(package|packageQuarantined)$/",
+ "group": "navigation"
+ },
+ {
+ "command": "cloudsmith-vsc.showDepVulnerabilities",
+ "when": "view == cloudsmithDependencyHealthView && viewItem == dependencyHealthVulnerable",
+ "group": "navigation"
+ },
+ {
+ "command": "cloudsmith-vsc.findDepSafeVersion",
+ "when": "view == cloudsmithDependencyHealthView && viewItem == dependencyHealthVulnerable",
"group": "navigation"
},
{
@@ -442,17 +564,17 @@
},
{
"command": "cloudsmith-vsc.explainQuarantine",
- "when": "viewItem =~ /^(packageQuarantined|dependencyHealthBlocked)$/",
+ "when": "viewItem =~ /^(packageQuarantined|dependencyHealthQuarantined)$/",
"group": "navigation"
},
{
"command": "cloudsmith-vsc.copyInstallCommand",
- "when": "viewItem =~ /^(package|dependencyHealth|dependencyHealthViolated)$/",
+ "when": "viewItem =~ /^(package|dependencyHealthFound|dependencyHealthVulnerable)$/",
"group": "navigation"
},
{
"command": "cloudsmith-vsc.showInstallCommand",
- "when": "viewItem =~ /^(package|dependencyHealth|dependencyHealthViolated)$/",
+ "when": "viewItem =~ /^(package|dependencyHealthFound|dependencyHealthVulnerable)$/",
"group": "navigation"
},
{
@@ -467,9 +589,19 @@
},
{
"command": "cloudsmith-vsc.previewUpstreamResolution",
- "when": "view == cloudsmithDependencyHealthView && viewItem == dependencyHealthNotFound",
+ "when": "view == cloudsmithDependencyHealthView && viewItem =~ /^(dependencyHealthMissing|dependencyHealthUpstreamReachable|dependencyHealthUpstreamUnreachable)$/",
"group": "navigation"
},
+ {
+ "command": "cloudsmith-vsc.pullSingleDependency",
+ "when": "view == cloudsmithDependencyHealthView && viewItem =~ /^(dependencyHealthMissing|dependencyHealthUpstreamReachable)$/",
+ "group": "inline"
+ },
+ {
+ "command": "cloudsmith-vsc.pullSingleDependency",
+ "when": "view == cloudsmithDependencyHealthView && viewItem =~ /^(dependencyHealthMissing|dependencyHealthUpstreamReachable)$/",
+ "group": "1_pull"
+ },
{
"command": "cloudsmith-vsc.showPromotionStatus",
"when": "view == cloudsmithView && viewItem == package",
@@ -563,18 +695,63 @@
"when": "view == cloudsmithSearchView"
},
{
- "command": "cloudsmith-vsc.scanDependencies",
- "group": "navigation",
+ "command": "cloudsmith-vsc.scanDependenciesPending",
+ "group": "navigation@1",
+ "when": "view == cloudsmithDependencyHealthView && !cloudsmith.depScanComplete"
+ },
+ {
+ "command": "cloudsmith-vsc.scanDependenciesComplete",
+ "group": "navigation@1",
+ "when": "view == cloudsmithDependencyHealthView && cloudsmith.depScanComplete"
+ },
+ {
+ "command": "cloudsmith-vsc.pullDependencies",
+ "group": "navigation@2",
+ "when": "view == cloudsmithDependencyHealthView && cloudsmith.depScanComplete"
+ },
+ {
+ "command": "cloudsmith-vsc.cycleDepViewDirect",
+ "group": "navigation@3",
+ "when": "view == cloudsmithDependencyHealthView && cloudsmith.depViewMode == 'direct'"
+ },
+ {
+ "command": "cloudsmith-vsc.cycleDepViewFlat",
+ "group": "navigation@3",
+ "when": "view == cloudsmithDependencyHealthView && cloudsmith.depViewMode == 'flat'"
+ },
+ {
+ "command": "cloudsmith-vsc.cycleDepViewTree",
+ "group": "navigation@3",
+ "when": "view == cloudsmithDependencyHealthView && cloudsmith.depViewMode == 'tree'"
+ },
+ {
+ "command": "cloudsmith-vsc.depSortFilter",
+ "group": "navigation@4",
+ "when": "view == cloudsmithDependencyHealthView && !cloudsmith.depFilterActive"
+ },
+ {
+ "command": "cloudsmith-vsc.depSortFilterActive",
+ "group": "navigation@4",
+ "when": "view == cloudsmithDependencyHealthView && cloudsmith.depFilterActive"
+ },
+ {
+ "command": "cloudsmith-vsc.viewComplianceReport",
+ "group": "navigation@5",
+ "when": "view == cloudsmithDependencyHealthView && cloudsmith.depScanComplete"
+ },
+ {
+ "command": "cloudsmith-vsc.depViewDirect",
+ "group": "view@1",
"when": "view == cloudsmithDependencyHealthView"
},
{
- "command": "cloudsmith-vsc.rescanDependencies",
- "group": "navigation",
+ "command": "cloudsmith-vsc.depViewFlat",
+ "group": "view@2",
"when": "view == cloudsmithDependencyHealthView"
},
{
- "command": "cloudsmith-vsc.changeDependencyFolder",
- "group": "navigation",
+ "command": "cloudsmith-vsc.depViewTree",
+ "group": "view@3",
"when": "view == cloudsmithDependencyHealthView"
}
]
@@ -645,15 +822,20 @@
},
"cloudsmith-vsc.maxDependenciesToScan": {
"type": "integer",
- "default": 200,
+ "default": 10000,
"minimum": 1,
- "description": "Maximum number of dependencies to scan before truncating a dependency health run."
+ "description": "Maximum number of dependencies to display in the Dependency Health view. Pull operations always process all resolved dependencies."
},
"cloudsmith-vsc.showLicenseIndicators": {
"type": "boolean",
"default": true,
"description": "Show license risk classification on packages. When disabled, license nodes are hidden."
},
+ "cloudsmith-vsc.flagRestrictiveLicenses": {
+ "type": "boolean",
+ "default": true,
+ "description": "Highlight restrictive licenses inline in the Dependency Health view."
+ },
"cloudsmith-vsc.showDockerDigestCommand": {
"type": "boolean",
"default": false,
@@ -666,8 +848,14 @@
},
"cloudsmith-vsc.resolveTransitiveDependencies": {
"type": "boolean",
- "default": false,
- "description": "Use the package manager CLI to resolve transitive dependencies. Requires the package manager and installed project dependencies."
+ "default": true,
+ "description": "Parse lockfiles to resolve transitive dependencies. When disabled, only direct manifest dependencies are shown."
+ },
+ "cloudsmith-vsc.dependencyTreeDefaultView": {
+ "type": "string",
+ "enum": ["direct", "flat", "tree"],
+ "default": "flat",
+ "description": "Default view mode for the Dependency Health panel."
},
"cloudsmith-vsc.showLegacyPolicies": {
"type": "boolean",
diff --git a/test/complianceReportProvider.test.js b/test/complianceReportProvider.test.js
new file mode 100644
index 0000000..583c2f3
--- /dev/null
+++ b/test/complianceReportProvider.test.js
@@ -0,0 +1,150 @@
+const assert = require("assert");
+const vscode = require("vscode");
+const { ComplianceReportProvider } = require("../views/complianceReportProvider");
+const { buildComplianceReportData } = require("../views/dependencyHealthProvider");
+
+suite("ComplianceReportProvider", () => {
+ test("report data and HTML escape dynamic content", () => {
+ const dependencies = [
+ {
+ name: "evil'\"",
+ version: "1.0.0'\"",
+ format: "npm",
+ ecosystem: "npm",
+ isDirect: true,
+ cloudsmithStatus: "FOUND",
+ cloudsmithPackage: {
+ repository: "prod",
+ status_str: "Completed",
+ license: "MIT",
+ },
+ vulnerabilities: {
+ count: 2,
+ maxSeverity: "High",
+ severityCounts: { High: 2 },
+ hasFixAvailable: true,
+ entries: [{ fixVersion: "1.0.1" }],
+ detailsLoaded: true,
+ },
+ },
+ {
+ name: "license-risk",
+ version: "2.0.0",
+ format: "npm",
+ ecosystem: "npm",
+ isDirect: false,
+ cloudsmithStatus: "FOUND",
+ cloudsmithPackage: {
+ repository: "prod",
+ status_str: "Completed",
+ license: "GPL-3.0",
+ },
+ license: {
+ display: "GPL-3.0",
+ spdx: "GPL-3.0",
+ classification: "restrictive",
+ },
+ },
+ {
+ name: "policy-fail",
+ version: "3.0.0",
+ format: "pypi",
+ ecosystem: "pypi",
+ isDirect: true,
+ cloudsmithStatus: "FOUND",
+ cloudsmithPackage: {
+ repository: "prod",
+ status_str: "Quarantined",
+ },
+ policy: {
+ violated: true,
+ denied: true,
+ quarantined: true,
+ status: "Quarantined",
+ statusReason: "Blocked by policy ",
+ },
+ },
+ {
+ name: "missing-lib",
+ version: "0.1.0",
+ format: "npm",
+ ecosystem: "npm",
+ isDirect: true,
+ cloudsmithStatus: "NOT_FOUND",
+ upstreamStatus: "reachable",
+ upstreamDetail: "proxy ",
+ },
+ {
+ name: "missing-lib",
+ version: "0.1.0",
+ format: "npm",
+ ecosystem: "npm",
+ isDirect: false,
+ cloudsmithStatus: "NOT_FOUND",
+ upstreamStatus: "reachable",
+ upstreamDetail: "proxy ",
+ },
+ ];
+
+ const reportData = buildComplianceReportData("fixture ", dependencies, {
+ scanDate: "2026-04-05T12:30:00Z",
+ });
+
+ assert.strictEqual(reportData.summary.total, 4);
+ assert.strictEqual(reportData.summary.found, 3);
+ assert.strictEqual(reportData.summary.notFound, 1);
+ assert.strictEqual(reportData.summary.coveragePct, 75);
+ assert.strictEqual(reportData.summary.vulnCount, 1);
+ assert.strictEqual(reportData.summary.restrictiveLicenseCount, 1);
+ assert.strictEqual(reportData.summary.policyViolationCount, 1);
+
+ const provider = new ComplianceReportProvider({});
+ const html = provider._getHtml(reportData);
+
+ assert.match(html, /fixture <app>/);
+ assert.match(html, /evil<script>alert\(1\)<\/script>'"/);
+ assert.match(html, /1\.0\.0'"/);
+ assert.match(html, /proxy <prod>/);
+ assert.match(html, /Blocked by policy <rule>/);
+ assert.doesNotMatch(html, /