dependency-radar 0.7.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -34,6 +34,9 @@ This runs a scan against the current project and writes a self-contained `depend
34
34
  - **Full transitive tree** — shows depth, parent relationships, fan-in/fan-out, and dependency origins
35
35
  - **Workspace support** — works across npm, pnpm, and Yarn workspaces
36
36
  - **CI-friendly** — `--fail-on` flag lets you enforce licence and vulnerability policies in pipelines
37
+ - **Review-friendly outputs** — emit JSON, SARIF, CycloneDX SBOM, or SPDX SBOM artifacts from the same local scan
38
+ - **Change comparison** — compare a fresh scan with a previous `dependency-radar.json` to see added dependencies, removed dependencies, version changes, and new findings
39
+ - **Lockfile supply-chain signals** — flags git/local/tarball sources, missing integrity, unexpected registry hosts, and optional npm signature/provenance verification
37
40
  - **Completely offline-capable** — use `--offline` to skip registry calls; all package metadata is read from local `node_modules`
38
41
  - **Single self-contained HTML file** — no server needed; open it locally, attach it to a ticket, or share it with your team
39
42
 
@@ -94,8 +97,14 @@ The `scan` command is the default and can also be run explicitly as `npx depende
94
97
  | `--project <path>` | Path to the project to scan (defaults to current directory) |
95
98
  | `--quiet` | Suppress progress/info logs, browser opening, and footer messaging while keeping the final summary and failures visible |
96
99
  | `--out <path>` | Output path for the report file |
100
+ | `--format <format>` | Output format: `html`, `json`, `sarif`, `cyclonedx`, or `spdx` |
101
+ | `--sbom <format>` | Convenience alias for SBOM output: `cyclonedx` or `spdx` |
102
+ | `--target-node <major>` | Add Node major compatibility findings based on local `engines.node` metadata |
103
+ | `--audit-signatures` | Run `npm audit signatures` for registry signature/provenance verification (opt-in; skipped with `--offline`) |
104
+ | `--schema` | Print the current Dependency Radar JSON schema, or write it with `--out <path>` |
97
105
  | `--offline` | Skip `npm audit` and `npm outdated` (useful for offline/air-gapped scans) |
98
106
  | `--json` | Output JSON instead of HTML (`dependency-radar.json`) |
107
+ | `--timestamp` | Add a local timestamp to generated report filenames (`dependency-radar.YYYY-MM-DD_HH-mm-ss.html`) |
99
108
  | `--no-report` | Run analysis only; no HTML/JSON output written |
100
109
  | `--keep-temp` | Keep the temporary `.dependency-radar/` folder for debugging |
101
110
  | `--open` | Open the generated report using the system default browser |
@@ -145,6 +154,26 @@ Notes:
145
154
  - "Static import evidence" means Dependency Radar found local source imports for that package. It is a code-usage heuristic, not exploit reachability analysis.
146
155
  - "Introduced via root packages" and "Direct parents" are shown from the current scan model. The command does not currently print full ancestry chains.
147
156
 
157
+ ### Show why a dependency is present
158
+
159
+ Use `why` to print shortest dependency paths from direct dependencies to a package:
160
+
161
+ ```bash
162
+ npx dependency-radar why lodash
163
+ ```
164
+
165
+ This uses the same local scan model as the HTML report. When full paths are unavailable, it falls back to the package origins and direct parent evidence available in the report model.
166
+
167
+ ### Compare against a previous report
168
+
169
+ Use `compare` to scan the current project and compare it with an earlier JSON report:
170
+
171
+ ```bash
172
+ npx dependency-radar compare ./dependency-radar-before.json --json --offline
173
+ ```
174
+
175
+ The comparison highlights added dependencies, removed dependencies, one-version package changes, new findings, and resolved findings. This is useful in pull requests and release checks.
176
+
148
177
  ### CI policy enforcement (`--fail-on`)
149
178
 
150
179
  ```
@@ -161,6 +190,7 @@ Supported rules:
161
190
  | `licence-mismatch` | Fail if at least one dependency has a declared-vs-inferred licence mismatch |
162
191
  | `copyleft-detected` | Fail if strong copyleft (GPL/AGPL) appears in runtime dependencies |
163
192
  | `unknown-licence` | Fail if at least one dependency has neither declared nor inferred licence data |
193
+ | `supply-chain-source` | Fail if lockfile source signals detect git/local/tarball sources, missing integrity, or unexpected registry hosts |
164
194
 
165
195
  When rules are violated, Dependency Radar prints `✖ Policy violations detected:` and exits `1`. Unknown rules also exit `1` with a clear error message.
166
196
 
@@ -176,6 +206,42 @@ npx dependency-radar --open
176
206
  npx dependency-radar --project ./my-app --out ./reports/dependency-radar.html
177
207
  ```
178
208
 
209
+ ### Example: write SARIF for CI/code scanning
210
+
211
+ ```bash
212
+ npx dependency-radar --format sarif --out ./reports/dependency-radar.sarif
213
+ ```
214
+
215
+ ### Example: write an SBOM
216
+
217
+ ```bash
218
+ npx dependency-radar --sbom cyclonedx --out ./reports/bom.cdx.json
219
+ ```
220
+
221
+ ```bash
222
+ npx dependency-radar --sbom spdx --out ./reports/bom.spdx.json
223
+ ```
224
+
225
+ ### Example: check Node upgrade readiness signals
226
+
227
+ ```bash
228
+ npx dependency-radar --target-node 22
229
+ ```
230
+
231
+ ### Example: verify npm registry signatures/provenance
232
+
233
+ ```bash
234
+ npx dependency-radar --audit-signatures
235
+ ```
236
+
237
+ This runs `npm audit signatures` as an opt-in online check. It is skipped when `--offline` is used.
238
+
239
+ ### Example: write the JSON schema
240
+
241
+ ```bash
242
+ npx dependency-radar --schema --out ./reports/dependency-radar.schema.json
243
+ ```
244
+
179
245
  ### Example: keep temp files for debugging
180
246
  ```bash
181
247
  npx dependency-radar --keep-temp
@@ -241,7 +307,8 @@ The blocker detail counts can overlap: a single package may contribute to multip
241
307
  | pnpm | ✅ Lockfile-first (`pnpm-lock.yaml`) | ✅ | ✅ | ✅ |
242
308
  | Yarn Classic (v1) | ✅ Lockfile-first (`yarn.lock`) | ✅ | ✅ | ✅ |
243
309
  | Yarn Berry (v2+, node-modules linker) | ✅ Lockfile-first (`yarn.lock`) | ✅ | ⚠️ Plugin-dependent | ✅ |
244
- | Yarn Plug'n'Play | Not yet supported | | | |
310
+ | Yarn Plug'n'Play | ⚠️ Lockfile-derived graph only; package metadata may be incomplete without `node_modules` | | ⚠️ Plugin-dependent | |
311
+ | Bun | ⚠️ Text `bun.lock` parsing; binary `bun.lockb` is reported with a migration hint | N/A | ❌ | ⚠️ package.json workspaces only |
245
312
 
246
313
  ## Requirements
247
314
 
@@ -252,12 +319,13 @@ The blocker detail counts can overlap: a single package may contribute to multip
252
319
 
253
320
  When you run `npx dependency-radar` (or `dependency-radar scan`), the CLI executes this pipeline:
254
321
 
255
- 1. Parse CLI options (`--project`, `--out`, `--offline`, `--json`, `--no-report`, `--keep-temp`, `--open`, `--fail-on`).
322
+ 1. Parse CLI options (`--project`, `--out`, `--offline`, `--json`, `--timestamp`, `--no-report`, `--keep-temp`, `--open`, `--fail-on`, `--audit-signatures`, `--schema`).
256
323
  2. Detect workspace/package-manager context:
257
324
  - Workspace roots from `pnpm-workspace.yaml` or `package.json#workspaces`
258
325
  - Dependency policy from `package.json` and `pnpm-workspace.yaml` overrides/resolutions
259
326
  - Package manager from `packageManager`, lockfiles, and installed metadata
260
327
  - Yarn Plug'n'Play detection (`.pnp.cjs`/`.pnp.js` or `.yarnrc.yml nodeLinker: pnp`)
328
+ - Bun text lockfile detection (`bun.lock`; binary `bun.lockb` is not parsed)
261
329
  3. Create a temporary `.dependency-radar/` directory inside the scanned project.
262
330
  4. For each workspace package (or just the project root in single-package mode), collect dependency graph data:
263
331
  - Lockfile-first graph parsing (`pnpm-lock.yaml`, `npm-shrinkwrap.json`/`package-lock.json`, `yarn.lock`)
@@ -267,6 +335,8 @@ When you run `npx dependency-radar` (or `dependency-radar scan`), the CLI execut
267
335
  - Vulnerabilities (`npm audit` / `pnpm audit` / `yarn audit` or `yarn npm audit`)
268
336
  - Version drift (`npm outdated` / `pnpm outdated` / `yarn outdated`, where available)
269
337
  - Source import graph (static import/require parsing in `src/` or project root)
338
+ - Lockfile supply-chain source signals
339
+ - Optional npm registry signature/provenance verification (`--audit-signatures`)
270
340
  6. Normalize outputs into one internal shape and merge workspace package results.
271
341
  - PNPM lock/CLI dependency trees are filtered to installed-only packages (non-installed optional/platform variants are dropped)
272
342
  7. Resolve and crawl installed package directories in `node_modules` to collect local metadata:
@@ -278,10 +348,15 @@ When you run `npx dependency-radar` (or `dependency-radar scan`), the CLI execut
278
348
  - Root-cause/origin and runtime-impact heuristics
279
349
  - Install-time execution signals
280
350
  - Local package metadata (`description`, links, deprecation, TypeScript type availability, installed file count, CLI `bin` presence)
281
- 9. Write final output as either:
351
+ 9. Build normalized findings from the aggregated dependency model:
352
+ - Vulnerabilities, license review items, install-time execution surface, native bindings, deprecated packages, target Node compatibility findings, lockfile source signals, and npm signature/provenance failures
353
+ 10. Write final output as one of:
282
354
  - `dependency-radar.html` (self-contained report), or
283
355
  - `dependency-radar.json` (raw aggregated model)
284
- 10. Remove `.dependency-radar/` unless `--keep-temp` is set.
356
+ - SARIF (`--format sarif`)
357
+ - CycloneDX SBOM (`--format cyclonedx` / `--sbom cyclonedx`)
358
+ - SPDX SBOM (`--format spdx` / `--sbom spdx`)
359
+ 11. Remove `.dependency-radar/` unless `--keep-temp` is set.
285
360
 
286
361
  The scan is local-first: package metadata is read from `node_modules`; only audit/outdated commands require registry access.
287
362
 
@@ -400,7 +475,7 @@ The JSON schema matches the `AggregatedData` TypeScript interface in `src/types.
400
475
 
401
476
  ```ts
402
477
  export interface AggregatedData {
403
- schemaVersion: '1.3'; // Report schema version for compatibility checks
478
+ schemaVersion: '1.4'; // Report schema version for compatibility checks
404
479
  generatedAt: string; // ISO timestamp when the scan finished
405
480
  dependencyRadarVersion: string; // CLI version that produced the report
406
481
  git: {
@@ -438,11 +513,12 @@ export interface AggregatedData {
438
513
  nodeVersion: string; // Node.js version from process.versions.node
439
514
  runtimeVersion: string; // Node.js runtime version from process.version
440
515
  minRequiredMajor: number; // Strictest Node major required by dependency engines (0 if unknown)
516
+ targetNodeMajor?: number; // Node major passed through --target-node
441
517
  platform?: string; // OS platform (process.platform)
442
518
  arch?: string; // CPU architecture (process.arch)
443
519
  ci?: boolean; // True when CI indicators are detected
444
520
  packageManagerField?: string; // package.json packageManager field (e.g. pnpm@9.1.0)
445
- packageManager?: 'npm' | 'pnpm' | 'yarn'; // Package manager used for dependency/audit/outdated collection
521
+ packageManager?: 'npm' | 'pnpm' | 'yarn' | 'bun'; // Package manager used for dependency/audit/outdated collection
446
522
  packageManagerVersion?: string; // Version of the selected package manager (when available)
447
523
  toolVersions?: {
448
524
  npm?: string;
@@ -452,7 +528,7 @@ export interface AggregatedData {
452
528
  };
453
529
  workspaces: {
454
530
  enabled: boolean; // True when the scan used workspace aggregation
455
- type?: 'npm' | 'pnpm' | 'yarn' | 'none'; // Workspace mode (CLI currently always emits this)
531
+ type?: 'npm' | 'pnpm' | 'yarn' | 'bun' | 'none'; // Workspace mode (CLI currently always emits this)
456
532
  packageCount?: number; // Number of workspace packages scanned (CLI currently always emits this)
457
533
  workspacePackages?: WorkspacePackage[]; // Lightweight first-party workspace metadata
458
534
  };
@@ -460,7 +536,32 @@ export interface AggregatedData {
460
536
  dependencyCount: number; // Total EXTERNAL dependencies in the graph
461
537
  directCount: number; // External dependencies listed in package.json
462
538
  transitiveCount: number; // External dependencies pulled in by other dependencies
539
+ findingCount?: number; // Number of normalized findings generated from the dependency model
540
+ };
541
+ supplyChain?: {
542
+ signals: Array<{
543
+ type:
544
+ | 'git-dependency'
545
+ | 'file-dependency'
546
+ | 'non-registry-tarball'
547
+ | 'missing-integrity'
548
+ | 'unexpected-registry-host'
549
+ | 'signature-verification-failed'
550
+ | 'signature-verification-unavailable';
551
+ packageName?: string;
552
+ packageVersion?: string;
553
+ packageId?: string;
554
+ source: string;
555
+ detail: string;
556
+ }>;
557
+ signatureAudit?: {
558
+ attempted: boolean;
559
+ ok: boolean;
560
+ output?: string;
561
+ error?: string;
562
+ };
463
563
  };
564
+ findings?: DependencyFinding[]; // Normalized review/CI findings
464
565
  dependencies: Record<string, DependencyRecord>; // External third-party packages keyed by name@version
465
566
  }
466
567
 
@@ -6,6 +6,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.aggregateData = aggregateData;
7
7
  const utils_1 = require("./utils");
8
8
  const license_1 = require("./license");
9
+ const findings_1 = require("./findings");
10
+ const nodeEngine_1 = require("./nodeEngine");
9
11
  const promises_1 = __importDefault(require("fs/promises"));
10
12
  const path_1 = __importDefault(require("path"));
11
13
  const os_1 = __importDefault(require("os"));
@@ -286,7 +288,7 @@ function isWorkspacePackageNode(node, input) {
286
288
  return false;
287
289
  }
288
290
  async function aggregateData(input) {
289
- var _a, _b, _c, _d, _e, _f, _g, _h;
291
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
290
292
  const pkg = input.pkgOverride || (await (0, utils_1.readPackageJson)(input.projectPath));
291
293
  let projectPkg = input.projectPackageJson;
292
294
  if (!projectPkg) {
@@ -305,7 +307,8 @@ async function aggregateData(input) {
305
307
  const importGraph = normalizeImportGraph((_c = input.importGraphResult) === null || _c === void 0 ? void 0 : _c.data);
306
308
  const usageResult = buildUsageSummary(importGraph, input.projectPath);
307
309
  const outdatedById = buildOutdatedMap(input.outdatedResult);
308
- const outdatedUnknownNames = new Set(((_d = input.outdatedResult) === null || _d === void 0 ? void 0 : _d.unknownNames) || []);
310
+ const supplyChain = normalizeSupplyChain((_d = input.supplyChainResult) === null || _d === void 0 ? void 0 : _d.data);
311
+ const outdatedUnknownNames = new Set(((_e = input.outdatedResult) === null || _e === void 0 ? void 0 : _e.unknownNames) || []);
309
312
  const packageMetaCache = new Map();
310
313
  const resolvePaths = input.resolvePaths && input.resolvePaths.length > 0
311
314
  ? input.resolvePaths
@@ -346,7 +349,7 @@ async function aggregateData(input) {
346
349
  const runtimeImpact = usageResult.runtimeImpact.get(node.name);
347
350
  const introduction = determineIntroduction(direct, scope, rootCauses, runtimeImpact);
348
351
  const parentIds = Array.from(node.parents).sort();
349
- const origins = buildOrigins(rootCauses, parentIds, (_e = input.workspaceUsage) === null || _e === void 0 ? void 0 : _e.get(node.name), input.workspaceEnabled, MAX_TOP_ROOT_PACKAGES, MAX_TOP_PARENT_PACKAGES);
352
+ const origins = buildOrigins(rootCauses, parentIds, (_f = input.workspaceUsage) === null || _f === void 0 ? void 0 : _f.get(node.name), input.workspaceEnabled, MAX_TOP_ROOT_PACKAGES, MAX_TOP_PARENT_PACKAGES);
350
353
  const execution = packageInsights.execution;
351
354
  const id = node.key;
352
355
  const upgrade = buildUpgradeBlock(packageInsights);
@@ -359,7 +362,10 @@ async function aggregateData(input) {
359
362
  ...(outdated ? { outdatedStatus: outdated.status } : {}),
360
363
  ...((outdated === null || outdated === void 0 ? void 0 : outdated.latestVersion) ? { latestVersion: outdated.latestVersion } : {}),
361
364
  ...((upgrade === null || upgrade === void 0 ? void 0 : upgrade.blockers) ? { blockers: upgrade.blockers } : {}),
362
- ...((upgrade === null || upgrade === void 0 ? void 0 : upgrade.blocksNodeMajor) ? { blocksNodeMajor: upgrade.blocksNodeMajor } : {})
365
+ ...((upgrade === null || upgrade === void 0 ? void 0 : upgrade.blocksNodeMajor) ? { blocksNodeMajor: upgrade.blocksNodeMajor } : {}),
366
+ ...(typeof input.targetNodeMajor === 'number' && packageInsights.nodeEngine
367
+ ? { targetNodeCompatible: (0, nodeEngine_1.isNodeEngineTargetCompatible)(packageInsights.nodeEngine, input.targetNodeMajor) }
368
+ : {})
363
369
  };
364
370
  dependencies[id] = {
365
371
  package: {
@@ -372,9 +378,9 @@ async function aggregateData(input) {
372
378
  deprecated: packageInsights.deprecated,
373
379
  links: {
374
380
  npm: `https://www.npmjs.com/package/${node.name}`,
375
- ...(((_f = packageInsights.links) === null || _f === void 0 ? void 0 : _f.repository) ? { repository: packageInsights.links.repository } : {}),
376
- ...(((_g = packageInsights.links) === null || _g === void 0 ? void 0 : _g.homepage) ? { homepage: packageInsights.links.homepage } : {}),
377
- ...(((_h = packageInsights.links) === null || _h === void 0 ? void 0 : _h.bugs) ? { bugs: packageInsights.links.bugs } : {})
381
+ ...(((_g = packageInsights.links) === null || _g === void 0 ? void 0 : _g.repository) ? { repository: packageInsights.links.repository } : {}),
382
+ ...(((_h = packageInsights.links) === null || _h === void 0 ? void 0 : _h.homepage) ? { homepage: packageInsights.links.homepage } : {}),
383
+ ...(((_j = packageInsights.links) === null || _j === void 0 ? void 0 : _j.bugs) ? { bugs: packageInsights.links.bugs } : {})
378
384
  }
379
385
  },
380
386
  compliance: {
@@ -416,8 +422,8 @@ async function aggregateData(input) {
416
422
  const nodeVersion = process.versions.node;
417
423
  const dependencyCount = nodes.length;
418
424
  const transitiveCount = dependencyCount - directCount;
419
- return {
420
- schemaVersion: '1.3',
425
+ const aggregated = {
426
+ schemaVersion: '1.4',
421
427
  generatedAt: new Date().toISOString(),
422
428
  dependencyRadarVersion,
423
429
  git: {
@@ -428,6 +434,7 @@ async function aggregateData(input) {
428
434
  nodeVersion,
429
435
  runtimeVersion,
430
436
  minRequiredMajor: minRequiredMajor !== null && minRequiredMajor !== void 0 ? minRequiredMajor : 0,
437
+ ...(typeof input.targetNodeMajor === 'number' ? { targetNodeMajor: input.targetNodeMajor } : {}),
431
438
  ...(input.platform ? { platform: input.platform } : {}),
432
439
  ...(input.arch ? { arch: input.arch } : {}),
433
440
  ...(typeof input.ci === 'boolean' ? { ci: input.ci } : {}),
@@ -449,8 +456,27 @@ async function aggregateData(input) {
449
456
  directCount,
450
457
  transitiveCount
451
458
  },
459
+ ...(supplyChain ? { supplyChain } : {}),
452
460
  dependencies
453
461
  };
462
+ const findings = (0, findings_1.buildDependencyFindings)(aggregated, { targetNodeMajor: input.targetNodeMajor });
463
+ aggregated.findings = findings;
464
+ aggregated.summary.findingCount = findings.length;
465
+ return aggregated;
466
+ }
467
+ function normalizeSupplyChain(data) {
468
+ if (!data || typeof data !== 'object')
469
+ return undefined;
470
+ const signals = Array.isArray(data.signals) ? data.signals : [];
471
+ const signatureAudit = data.signatureAudit && typeof data.signatureAudit === 'object'
472
+ ? data.signatureAudit
473
+ : undefined;
474
+ if (signals.length === 0 && !signatureAudit)
475
+ return undefined;
476
+ return {
477
+ signals,
478
+ ...(signatureAudit ? { signatureAudit } : {})
479
+ };
454
480
  }
455
481
  function deriveMinRequiredMajor(engineRanges) {
456
482
  let strictest;