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
67 changes: 55 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,55 @@ npx dependency-radar

This runs a scan against the current project and writes a self-contained `dependency-radar.html` report you can open locally, share with teammates, or attach to tickets and documentation.

You can see a [Dependency Radar example report](https://www.dependency-radar.com/examples/dependency-radar.html).

---

![Dependency Radar – dependency list view](./docs/screenshot-01.jpg)
*List view: search, filter, and drill into every dependency — licence, vulnerabilities, install risk, depth, origins, and more.*
*List view: search, filter, and drill into every dependency, licence, vulnerabilities, install risk, depth, origins, and more.*

![Dependency Radar – expanded dependency](./docs/screenshot-03.jpg)
*Expanded dependency view: scan the key risk signals first, then drill into status, scope, origins, install behaviour, licence, vulnerabilities, and upgrade blockers.*

![Dependency Radar – interactive dependency graph view](./docs/screenshot-02.jpg)
*Graph view: explore the full dependency tree visually, with direct, dev, and transitive relationships at a glance.*

---

## Before you run a random `npx` command

It is reasonable to be cautious about running a new CLI tool inside your project.

Dependency Radar is designed to be inspectable and low-friction to evaluate:

- the CLI has no runtime npm dependencies
- scans run on your machine
- Dependency Radar does not modify your `package.json`, lockfile, or installed dependencies
- Dependency Radar does not upload your source code or generated reports during a normal CLI scan
- reports are written to disk as local files
- the only default network activity is package-manager-backed audit and outdated checks, which query the configured package registry for dependency metadata
- use `--offline` for a no-registry-call scan

You can inspect the source on GitHub, view the npm package metadata, or start with an offline scan:

```bash
npx dependency-radar --offline
```

Security issues should be reported privately; see [SECURITY.md](./SECURITY.md).

## What the CLI accesses

| Area | What happens |
|---|---|
| Project files | Reads package manifests, lockfiles, and installed dependency metadata |
| `node_modules` | Reads package metadata and selected files for dependency analysis |
| Output files | Writes reports/SBOMs only where requested |
| Network | Runs package-manager audit/outdated checks by default, which query the configured package registry for dependency metadata |
| Offline mode | `--offline` skips audit, outdated, signature verification, and targeted registry enrichment checks |
| Source code upload | No source code or generated reports are uploaded during a normal CLI scan |
| Project mutation | Dependency Radar does not install, update, remove, or rewrite dependencies |

## What you get

- **Vulnerability scanning** — runs `npm audit` / `pnpm audit` / `yarn audit` and surfaces advisories with severity, fix availability, and reachability heuristics
Expand Down Expand Up @@ -59,10 +98,13 @@ This runs a scan against the current project and writes a self-contained `depend

## What it is not

- Not a CI service or hosted scanning platform
- Not a replacement for dedicated security scanners
- Not a bundler or build tool
Dependency Radar is a review and triage tool. It makes dependency risk easier to see, but it does not make security decisions for you.

- Not a hosted scanning platform or CI service
- Not a malware detector or guarantee that a package is safe
- Not a replacement for dedicated security scanners, manual review, or threat modelling
- Not a dependency updater
- Not a bundler, package manager, or build tool

## Why this exists

Expand All @@ -79,14 +121,16 @@ Dependency Radar exists to make those hidden signals visible in one place, from

## Need to share findings with leadership?

The CLI tool is free and fully functional forever.
The CLI tool is free and fully functional forever. It does not require an account or upload during normal use.

If you need to communicate dependency risk beyond engineering (CTO, compliance, security, clients, or investors), the optional premium service adds executive summaries, presentation-ready reports, and deeper enrichment signals that are not available in the standard local scan.

These include ecosystem and maintenance insights such as whether a dependency is archived, deprecated upstream, actively maintained, or showing signs of stagnation, helping you prioritise risk in larger portfolios or during technical due diligence.

See https://dependency-radar.com for details.

The free CLI does not require an account or upload. The optional premium service is separate and only applies if you choose to use it.

---

## Usage
Expand Down Expand Up @@ -436,7 +480,7 @@ When you run `npx dependency-radar` (or `dependency-radar scan`), the CLI execut
- SPDX SBOM (`--format spdx` / `--sbom spdx`)
11. Remove `.dependency-radar/` unless `--keep-temp` is set.

The scan is local-first: package metadata is read from `node_modules`. Audit/outdated commands, optional signature checks, and targeted registry enrichment require registry access and are skipped or disabled with `--offline`.
The scan runs on your machine: package metadata is read from `node_modules`. Audit/outdated commands, optional signature checks, and targeted registry enrichment require registry access and are skipped or disabled with `--offline`.

The `explain` command reuses this same pipeline with report writing disabled, then filters the in-memory model down to a single package for terminal output.

Expand Down Expand Up @@ -778,7 +822,7 @@ For full details and any future changes, see `src/types.ts`.
## Notes

- The target project must have dependencies installed (run `npm install`, `pnpm install`, or `yarn install` first).
- The scan runs on your machine and does not upload your code or dependencies anywhere.
- The scan runs on your machine and does not upload your source code or generated reports during a normal CLI scan.
- `npm audit`, `pnpm audit`, `yarn npm audit`, corresponding `outdated` commands, optional npm signature checks, and targeted registry enrichment perform registry lookups; use `--offline` for offline-only scans.
- On some Yarn Berry setups, `yarn outdated` is not available; the scan continues and marks outdated data as unavailable.
- A temporary `.dependency-radar/` folder is created during the scan to store intermediate tool output.
Expand Down Expand Up @@ -876,16 +920,15 @@ For pull requests, small focused changes are easiest to review. Before opening a
npm run build
npm run test:unit
```

For larger dependency graph, package manager, report output, or supply-chain signal changes, also run the relevant fixture tests where practical.

Please do not report suspected security vulnerabilities in public issues. See SECURITY.md for private reporting guidance.
Please do not report suspected security vulnerabilities in public issues. See [SECURITY.md](./SECURITY.md) for private reporting guidance.

Release notes are published through GitHub Releases, which act as the project changelog.

## Releases and changelog

Dependency Radar release notes are published through GitHub Releases:
## Releases and changelog

https://github.com/JosephMaynard/dependency-radar/releases
Release notes are published through [GitHub Releases](https://github.com/JosephMaynard/dependency-radar/releases), which act as the project changelog.

Each release summarises notable changes, fixes, and compatibility notes where relevant.
4 changes: 2 additions & 2 deletions dist/report-assets.js

Large diffs are not rendered by default.

162 changes: 162 additions & 0 deletions dist/reportDetailRules.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.reportVulnerabilityTotal = reportVulnerabilityTotal;
exports.reportAllExecutionSignals = reportAllExecutionSignals;
exports.buildReportOverallRisk = buildReportOverallRisk;
exports.buildReportKeyPoints = buildReportKeyPoints;
const EXECUTION_SIGNAL_LABELS = {
'network-access': 'Accesses network during install',
'dynamic-exec': 'Uses dynamic execution',
'child-process': 'Spawns child processes',
encoding: 'Uses encoding/decoding logic',
obfuscated: 'Contains obfuscated/minified install logic',
'reads-env': 'Reads environment variables',
'reads-home': 'Reads user home directory',
'uses-ssh': 'Uses SSH configuration/keys'
};
function reportVulnerabilityTotal(summary) {
return summary.critical + summary.high + summary.moderate + summary.low;
}
function reportAllExecutionSignals(dep) {
var _a, _b, _c;
return Array.from(new Set([
...(((_b = (_a = dep.execution) === null || _a === void 0 ? void 0 : _a.scripts) === null || _b === void 0 ? void 0 : _b.signals) || []),
...(((_c = dep.execution) === null || _c === void 0 ? void 0 : _c.signals) || [])
]));
}
function maxRisk(risks) {
if (risks.includes('red'))
return 'red';
if (risks.includes('amber'))
return 'amber';
return 'green';
}
function buildReportOverallRisk(dep, summary, supplyChainSignalCount = 0) {
var _a, _b, _c, _d, _e, _f;
const installRisk = ((_a = dep.execution) === null || _a === void 0 ? void 0 : _a.risk) || 'green';
const supplyChainRisk = supplyChainSignalCount > 0 || (((_c = (_b = dep.packaging) === null || _b === void 0 ? void 0 : _b.signals) === null || _c === void 0 ? void 0 : _c.length) || 0) > 0
? 'amber'
: 'green';
const maintenanceRisk = (((_f = (_e = (_d = dep.supplyChain) === null || _d === void 0 ? void 0 : _d.registry) === null || _e === void 0 ? void 0 : _e.signals) === null || _f === void 0 ? void 0 : _f.length) || 0) > 0 ? 'amber' : 'green';
return maxRisk([
summary.risk,
dep.compliance.licenseRisk,
installRisk,
supplyChainRisk,
maintenanceRisk
]);
}
function titleCaseValue(value) {
return value
.split(/[-_\s]+/)
.filter(Boolean)
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join(' ');
}
function scopeLabel(scope) {
if (scope === 'runtime')
return 'Runtime';
if (scope === 'dev')
return 'Dev';
if (scope === 'optional')
return 'Optional';
if (scope === 'peer')
return 'Peer';
return titleCaseValue(scope);
}
function toneLabel(tone) {
if (tone === 'red')
return 'High';
if (tone === 'amber')
return 'Medium';
return 'Low';
}
function formatLicenseStatus(status) {
switch (status) {
case 'declared-only':
return 'Declared';
case 'inferred-only':
return 'Inferred';
case 'match':
return 'Declared + Inferred (match)';
case 'mismatch':
return 'Declared + Inferred (mismatch)';
case 'invalid-spdx':
return 'Invalid SPDX';
default:
return 'Unknown';
}
}
function formatModerateLow(summary) {
const parts = [];
if (summary.moderate)
parts.push(`${summary.moderate} moderate`);
if (summary.low)
parts.push(`${summary.low} low`);
return parts.join(', ');
}
function buildReportKeyPoints(dep, summary) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
const points = [];
const vulnTotal = reportVulnerabilityTotal(summary);
const hasFix = (_a = dep.security.advisories) === null || _a === void 0 ? void 0 : _a.some((adv) => adv.fixAvailable);
if (summary.critical || summary.high) {
const highTotal = summary.critical + summary.high;
points.push(`${highTotal} critical/high ${highTotal === 1 ? 'vulnerability' : 'vulnerabilities'}${hasFix ? ', fix available' : ''}`);
}
if (summary.moderate || summary.low) {
const lowerTotal = summary.moderate + summary.low;
points.push(`${formatModerateLow(summary)} ${lowerTotal === 1 ? 'vulnerability' : 'vulnerabilities'}${hasFix ? ', fix available' : ''}`);
}
if (dep.compliance.licenseRisk !== 'green') {
points.push('Licence status: ' + formatLicenseStatus(dep.compliance.license.status));
}
if (dep.upgrade.blocksNodeMajor)
points.push('Blocks Node major upgrade');
if ((_b = dep.upgrade.blockers) === null || _b === void 0 ? void 0 : _b.length) {
points.push(`${dep.upgrade.blockers.length} upgrade ${dep.upgrade.blockers.length === 1 ? 'blocker' : 'blockers'} detected`);
}
const executionRisk = ((_c = dep.execution) === null || _c === void 0 ? void 0 : _c.risk) || 'green';
if (executionRisk !== 'green')
points.push(`${toneLabel(executionRisk)} install-time execution risk`);
if ((_f = (_e = (_d = dep.execution) === null || _d === void 0 ? void 0 : _d.scripts) === null || _e === void 0 ? void 0 : _e.hooks) === null || _f === void 0 ? void 0 : _f.length) {
points.push('Runs ' +
dep.execution.scripts.hooks.slice(0, 2).join(', ') +
' lifecycle script' +
(dep.execution.scripts.hooks.length === 1 ? '' : 's'));
}
reportAllExecutionSignals(dep)
.slice(0, 3)
.forEach((signal) => points.push(EXECUTION_SIGNAL_LABELS[signal]));
if ((_h = (_g = dep.packaging) === null || _g === void 0 ? void 0 : _g.signals) === null || _h === void 0 ? void 0 : _h.length) {
points.push(`${dep.packaging.signals.length} package content ${dep.packaging.signals.length === 1 ? 'signal' : 'signals'}`);
}
if ((_l = (_k = (_j = dep.supplyChain) === null || _j === void 0 ? void 0 : _j.registry) === null || _k === void 0 ? void 0 : _k.signals) === null || _l === void 0 ? void 0 : _l.length) {
points.push(`${dep.supplyChain.registry.signals.length} registry metadata ${dep.supplyChain.registry.signals.length === 1 ? 'signal' : 'signals'}`);
}
if (dep.usage.direct) {
points.push(`Direct ${scopeLabel(dep.usage.scope).toLowerCase()} dependency`);
}
else {
const intro = ((_m = dep.usage.origins.topParentPackages) === null || _m === void 0 ? void 0 : _m[0])
? ` introduced by ${dep.usage.origins.topParentPackages[0]}`
: '';
points.push(`Transitive ${scopeLabel(dep.usage.scope).toLowerCase()} dependency${intro}`);
}
if (dep.usage.depth > 1)
points.push(`Dependency depth ${dep.usage.depth}`);
if (points.length === 0 || (vulnTotal === 0 && executionRisk === 'green' && dep.compliance.licenseRisk === 'green' && points.length < 3)) {
[
'No known vulnerabilities',
'No install-time execution signals detected',
'Licence status appears consistent',
dep.usage.direct
? `Direct ${scopeLabel(dep.usage.scope).toLowerCase()} dependency`
: `Transitive ${scopeLabel(dep.usage.scope).toLowerCase()} dependency`
].forEach((point) => {
if (!points.includes(point))
points.push(point);
});
}
return Array.from(new Set(points)).slice(0, 8);
}
Binary file modified docs/screenshot-01.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screenshot-02.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshot-03.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion report-ui/dist/report.css

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion report-ui/dist/report.iife.js

Large diffs are not rendered by default.

Loading