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/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, /