Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@
"workbench.colorCustomizations": {
"activityBar.background": "#283107",
"titleBar.activeBackground": "#38450A",
"titleBar.activeForeground": "#F8FCE9"
"titleBar.activeForeground": "#F8FCE9",
"titleBar.inactiveBackground": "#283107",
"titleBar.inactiveForeground": "#F8FCE9",
"statusBar.background": "#283107",
"statusBar.foreground": "#F8FCE9",
"statusBar.debuggingBackground": "#283107",
"statusBar.debuggingForeground": "#F8FCE9",
"statusBar.noFolderBackground": "#283107",
"statusBar.noFolderForeground": "#F8FCE9"
}
}
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ See https://dependency-radar.com for details.

When you run `npx dependency-radar` (or `dependency-radar scan`), the CLI executes this pipeline:

1. Parse CLI options (`--project`, `--out`, `--offline`, `--json`, `--keep-temp`, `--open`).
1. Parse CLI options (`--project`, `--out`, `--offline`, `--json`, `--no-report`, `--keep-temp`, `--open`, `--fail-on`).
2. Detect workspace/package-manager context:
- Workspace roots from `pnpm-workspace.yaml` or `package.json#workspaces`
- Dependency policy from `package.json` and `pnpm-workspace.yaml` overrides/resolutions
Expand Down Expand Up @@ -234,6 +234,28 @@ Open the generated report using the system default:
npx dependency-radar --open
```

Fail the command in CI when selected policy rules are violated:

```bash
npx dependency-radar --fail-on reachable-vuln,licence-mismatch
```

Supported `--fail-on` rules:

- `reachable-vuln` - Fail if at least one reachable runtime vulnerability is present
- `production-vuln` - Fail if at least one runtime vulnerability is present (reachability ignored)
- `high-severity-vuln` - Fail if at least one high/critical vulnerability is present
- `licence-mismatch` - Fail if at least one dependency has a declared-vs-inferred licence mismatch
- `copyleft-detected` - Fail if strong copyleft (GPL/AGPL) appears in runtime dependencies
- `unknown-licence` - Fail if at least one dependency has neither declared nor inferred licence data

`--fail-on` behavior:

- Unknown rules return exit code `1` with a clear error.
- When selected rules are violated, Dependency Radar prints `✖ Policy violations detected:` and exits `1`.
- With no `--fail-on` flag, scans do not fail due to policy checks.
- Exit codes for policy checks are only `0` (pass) or `1` (fail).

Show options:

```bash
Expand Down
118 changes: 118 additions & 0 deletions src/cli.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { spawnSync } from 'child_process';
import path from 'path';
import { describe, expect, it } from 'vitest';

const ANSI_ESCAPE_REGEX = new RegExp('\\x1b\\[[0-9;]*[A-Za-z]', 'g');

function stripAnsi(value: string): string {
return value.replace(ANSI_ESCAPE_REGEX, '');
}

describe('cli summary output', () => {
it(
'prints summary bullets and keeps CTA as the final line',
{ timeout: 30000 },
() => {
const repoRoot = path.resolve(__dirname, '..');
const tsNodeBin = path.join(
repoRoot,
'node_modules',
'ts-node',
'dist',
'bin.js',
);
const cliPath = path.join(repoRoot, 'src', 'cli.ts');

const result = spawnSync(
process.execPath,
[tsNodeBin, cliPath, 'scan', '--project', repoRoot, '--offline', '--no-report'],
{
cwd: repoRoot,
encoding: 'utf8',
env: {
...process.env,
NO_COLOR: '1',
},
},
);

expect(result.status).toBe(0);

const output = stripAnsi(`${result.stdout}\n${result.stderr}`).replace(/\r/g, '');
const stdoutOutput = stripAnsi(result.stdout).replace(/\r/g, '');

expect(output).toContain('Summary:');
expect(output).toMatch(/• Direct deps scanned: \d+/);
expect(output).toMatch(/• Transitive deps scanned: \d+/);
expect(output).toMatch(/• Vulnerable packages: \d+ \(\d+ reachable\)/);
expect(output).toMatch(/• Unused installed deps: \d+/);
expect(output).toMatch(/• License mismatches: \d+/);
expect(output).toMatch(/• Major upgrade blockers: \d+/);

const blockerTotalMatch = output.match(/• Major upgrade blockers: (\d+)/);
const blockerTotal = blockerTotalMatch
? Number.parseInt(blockerTotalMatch[1], 10)
: 0;
if (blockerTotal > 0) {
expect(output).toMatch(/^\s{3}- \d+ .+/m);
}

expect(output).toContain(
'Enrich this scan with maintenance signals, upgrade readiness, and risk modelling at dependency-radar.com',
);
expect(stdoutOutput.trim().endsWith(
'Enrich this scan with maintenance signals, upgrade readiness, and risk modelling at dependency-radar.com',
)).toBe(true);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
},
);

it(
'exits non-zero and prints policy violations when --fail-on is triggered',
{ timeout: 30000 },
() => {
const repoRoot = path.resolve(__dirname, '..');
const tsNodeBin = path.join(
repoRoot,
'node_modules',
'ts-node',
'dist',
'bin.js',
);
const cliPath = path.join(repoRoot, 'src', 'cli.ts');
const fixtureProject = path.join(
repoRoot,
'test-fixtures',
'license-edge-cases',
);

const result = spawnSync(
process.execPath,
[
tsNodeBin,
cliPath,
'scan',
'--project',
fixtureProject,
'--offline',
'--no-report',
'--fail-on',
'licence-mismatch',
],
{
cwd: repoRoot,
encoding: 'utf8',
env: {
...process.env,
NO_COLOR: '1',
},
},
);

expect(result.status).toBe(1);

const output = stripAnsi(`${result.stdout}\n${result.stderr}`).replace(/\r/g, '');
expect(output).toContain('Policy violations detected');
expect(output).toContain('licence mismatch');
},
);
});
69 changes: 62 additions & 7 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ import { runPackageAudit } from "./runners/npmAudit";
import { runNpmLs } from "./runners/npmLs";
import { runPackageOutdated } from "./runners/npmOutdated";
import { renderReport } from "./report";
import {
SUPPORTED_FAIL_ON_RULES,
evaluatePolicyViolations,
parseFailOnRules,
} from "./failOn";
import type { FailOnRule, PolicyViolation } from "./failOn";
import type {
AggregatedData,
OutdatedEntry,
Expand Down Expand Up @@ -1049,13 +1055,14 @@ interface CliOptions {
json: boolean;
open: boolean;
noReport: boolean;
failOn: Set<FailOnRule>;
}

/**
* Parse command-line tokens into a populated CliOptions object.
*
* Recognizes a leading non-flag token as the command and the following flags:
* --project, --out, --keep-temp, --offline, --json, --open, --no-report, and --help / -h.
* --project, --out, --keep-temp, --offline, --json, --open, --no-report, --fail-on, and --help / -h.
* The --offline flag disables both audit and outdated checks.
*
* @param argv - Array of CLI tokens (typically process.argv.slice(2))
Expand All @@ -1072,6 +1079,7 @@ function parseArgs(argv: string[]): CliOptions {
json: false,
open: false,
noReport: false,
failOn: new Set<FailOnRule>(),
};

const args = [...argv];
Expand All @@ -1091,6 +1099,28 @@ function parseArgs(argv: string[]): CliOptions {
} else if (arg === "--json") opts.json = true;
else if (arg === "--open") opts.open = true;
else if (arg === "--no-report") opts.noReport = true;
else if (arg === "--fail-on") {
const value = args.shift();
if (!value) {
console.error(
"Missing value for --fail-on. Provide a comma-separated list of rules.",
);
process.exit(1);
}
let rules: Set<FailOnRule>;
try {
rules = parseFailOnRules(value);
} catch (err: any) {
console.error(
err instanceof Error ? err.message : "Invalid --fail-on rules.",
);
process.exit(1);
return opts;
}
for (const rule of rules) {
opts.failOn.add(rule);
}
}
else if (arg === "--help" || arg === "-h") {
printHelp();
process.exit(0);
Expand All @@ -1104,7 +1134,7 @@ function parseArgs(argv: string[]): CliOptions {
* Print the CLI usage and available options to the console.
*
* Displays the command synopsis and descriptions for supported flags including
* --project, --out, --json, --no-report, --keep-temp, --offline, and --open.
* --project, --out, --json, --no-report, --keep-temp, --offline, --open, and --fail-on.
*/
function printHelp(): void {
console.log(`dependency-radar [scan] [options]
Expand All @@ -1119,6 +1149,9 @@ Options:
--keep-temp Keep .dependency-radar folder
--offline Skip npm audit and npm outdated (useful for offline scans)
--open Open the generated report using the system default application
--fail-on <rules> Fail with exit code 1 when selected rules are violated
Supported: reachable-vuln, production-vuln, high-severity-vuln,
licence-mismatch, copyleft-detected, unknown-licence
`);
}

Expand Down Expand Up @@ -1291,6 +1324,20 @@ type CliSummary = {
};
};

/**
* Print policy violation messages to stdout as a human-readable list when any exist.
*
* @param violations - An array of policy violations to display; each violation's `message` will be printed as a list item. If the array is empty, nothing is printed.
*/
function printPolicyViolations(violations: PolicyViolation[]): void {
if (violations.length === 0) return;
console.log("");
console.log(colorLeadingSymbol("✖ Policy violations detected:"));
for (const violation of violations) {
console.log(`- ${violation.message}`);
}
}

/**
* Produce a concise CLI summary from aggregated workspace data.
*
Expand Down Expand Up @@ -1465,13 +1512,13 @@ function printCliSummary(summary: CliSummary): void {
}

/**
* Orchestrates the CLI "scan" command to collect, merge, and output dependency data for a project or workspace.
* Run the CLI "scan" command to collect and aggregate dependency data for a project or workspace.
*
* Detects workspace type and package manager, runs per-package collectors (audit, dependency tree, import graph, outdated),
* merges collected signals into a workspace-level model, and writes a JSON or HTML report to the configured output path.
* Manages a temporary working directory (created under the project as .dependency-radar), respects CLI options such as
* JSON output, audit/outdated toggles, keeping the temp directory, and optionally opening the generated output with the
* system default application. Exits the process with a non-zero code on fatal errors. */
* merges collected signals into a workspace-level model, and writes a JSON or HTML report according to CLI options.
* Manages a temporary working directory and optionally opens the generated report. Exits the process with a non-zero code
* on fatal errors or when configured policy violations are detected.
*/
async function run(): Promise<void> {
const opts = parseArgs(process.argv.slice(2));
if (opts.command !== "scan") {
Expand All @@ -1483,6 +1530,7 @@ async function run(): Promise<void> {
const shouldWriteArtifacts = !opts.noReport;
const projectPath = path.resolve(opts.project);
let summary: CliSummary | undefined;
let policyViolations: PolicyViolation[] = [];
if (opts.noReport && opts.keepTemp) {
console.log(
statusLine("⚠", "--keep-temp is ignored when --no-report is enabled."),
Expand Down Expand Up @@ -1817,6 +1865,7 @@ async function run(): Promise<void> {
summary = buildCliSummary(aggregated, {
importGraphComplete,
});
policyViolations = evaluatePolicyViolations(aggregated, opts.failOn);

if (workspace.type !== "none") {
console.log(
Expand Down Expand Up @@ -1895,10 +1944,16 @@ async function run(): Promise<void> {
console.log("");
}

printPolicyViolations(policyViolations);

// Always show CTA as the last output
console.log(
"Enrich this scan with maintenance signals, upgrade readiness, and risk modelling at dependency-radar.com",
);

if (policyViolations.length > 0) {
process.exit(1);
}
}

run();
Expand Down
Loading