dependency-radar 0.7.0 → 0.8.0
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 +108 -7
- package/dist/aggregator.js +35 -9
- package/dist/cli.js +347 -39
- package/dist/compare.js +79 -0
- package/dist/failOn.js +16 -2
- package/dist/findings.js +166 -0
- package/dist/generated/spdx.js +3 -0
- package/dist/nodeEngine.js +181 -0
- package/dist/outputFormats.js +185 -0
- package/dist/report-assets.js +2 -2
- package/dist/report.js +137 -71
- package/dist/runners/importGraphRunner.js +9 -5
- package/dist/runners/lockfileGraph.js +144 -1
- package/dist/runners/lockfileSignals.js +303 -0
- package/dist/runners/npmLs.js +15 -0
- package/dist/schema.js +107 -0
- package/dist/utils.js +62 -3
- package/dist/why.js +69 -0
- package/dist/workspaceFilter.js +25 -0
- package/package.json +5 -4
- package/dist/runners/depcheckRunner.js +0 -23
- package/dist/runners/licenseChecker.js +0 -33
- package/dist/runners/madgeRunner.js +0 -29
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runLockfileSupplyChainSignals = runLockfileSupplyChainSignals;
|
|
7
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const utils_1 = require("../utils");
|
|
10
|
+
const DEFAULT_REGISTRY_HOSTS = new Set(['registry.npmjs.org']);
|
|
11
|
+
function stripJsonComments(raw) {
|
|
12
|
+
let out = '';
|
|
13
|
+
let quote;
|
|
14
|
+
let escaped = false;
|
|
15
|
+
for (let i = 0; i < raw.length; i += 1) {
|
|
16
|
+
const ch = raw[i];
|
|
17
|
+
const next = raw[i + 1];
|
|
18
|
+
if (quote) {
|
|
19
|
+
out += ch;
|
|
20
|
+
if (escaped)
|
|
21
|
+
escaped = false;
|
|
22
|
+
else if (ch === '\\')
|
|
23
|
+
escaped = true;
|
|
24
|
+
else if (ch === quote)
|
|
25
|
+
quote = undefined;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (ch === '"' || ch === "'") {
|
|
29
|
+
quote = ch;
|
|
30
|
+
out += ch;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (ch === '/' && next === '/') {
|
|
34
|
+
while (i < raw.length && raw[i] !== '\n')
|
|
35
|
+
i += 1;
|
|
36
|
+
out += '\n';
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (ch === '/' && next === '*') {
|
|
40
|
+
i += 2;
|
|
41
|
+
while (i < raw.length && !(raw[i] === '*' && raw[i + 1] === '/'))
|
|
42
|
+
i += 1;
|
|
43
|
+
i += 1;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
out += ch;
|
|
47
|
+
}
|
|
48
|
+
return out.replace(/,\s*([}\]])/g, '$1');
|
|
49
|
+
}
|
|
50
|
+
function normalizeExpectedHosts(hosts) {
|
|
51
|
+
const normalized = new Set(DEFAULT_REGISTRY_HOSTS);
|
|
52
|
+
for (const host of hosts || []) {
|
|
53
|
+
const trimmed = host.trim().toLowerCase();
|
|
54
|
+
if (trimmed)
|
|
55
|
+
normalized.add(trimmed);
|
|
56
|
+
}
|
|
57
|
+
return normalized;
|
|
58
|
+
}
|
|
59
|
+
function addSignal(signals, seen, signal) {
|
|
60
|
+
const key = `${signal.type}|${signal.packageName || ''}|${signal.packageVersion || ''}|${signal.source}|${signal.detail}`;
|
|
61
|
+
if (seen.has(key))
|
|
62
|
+
return;
|
|
63
|
+
seen.add(key);
|
|
64
|
+
signals.push(signal);
|
|
65
|
+
}
|
|
66
|
+
function packageFromKey(key) {
|
|
67
|
+
var _a, _b;
|
|
68
|
+
const cleaned = key.replace(/^\/?/, '');
|
|
69
|
+
const afterLastNodeModules = ((_a = cleaned.split('/node_modules/').pop()) === null || _a === void 0 ? void 0 : _a.replace(/^node_modules\//, '')) || cleaned;
|
|
70
|
+
const parts = afterLastNodeModules.split('/').filter(Boolean);
|
|
71
|
+
if (((_b = parts[0]) === null || _b === void 0 ? void 0 : _b.startsWith('@')) && parts[1]) {
|
|
72
|
+
return { name: `${parts[0]}/${parts[1]}` };
|
|
73
|
+
}
|
|
74
|
+
return { name: parts[0] || undefined };
|
|
75
|
+
}
|
|
76
|
+
function packageNameFromSelector(selector) {
|
|
77
|
+
var _a;
|
|
78
|
+
const first = (_a = selector.split(',')[0]) === null || _a === void 0 ? void 0 : _a.trim();
|
|
79
|
+
if (!first)
|
|
80
|
+
return undefined;
|
|
81
|
+
const normalized = first.replace(/^["']|["']$/g, '');
|
|
82
|
+
if (normalized.startsWith('@')) {
|
|
83
|
+
const parts = normalized.split('@');
|
|
84
|
+
const scopedName = parts.length >= 3 ? `@${parts[1]}` : normalized;
|
|
85
|
+
return scopedName || undefined;
|
|
86
|
+
}
|
|
87
|
+
const atIndex = normalized.indexOf('@');
|
|
88
|
+
return atIndex > 0 ? normalized.slice(0, atIndex) : normalized;
|
|
89
|
+
}
|
|
90
|
+
function inspectResolvedUrl(signals, seen, sourceFile, packageName, packageVersion, value, expectedHosts) {
|
|
91
|
+
const lower = value.toLowerCase();
|
|
92
|
+
if (lower.startsWith('git+') || lower.startsWith('git://') || lower.startsWith('github:') || lower.includes('github.com:')) {
|
|
93
|
+
addSignal(signals, seen, {
|
|
94
|
+
type: 'git-dependency',
|
|
95
|
+
packageName,
|
|
96
|
+
packageVersion,
|
|
97
|
+
source: sourceFile,
|
|
98
|
+
detail: `${packageName || 'dependency'} resolves from git source ${value}`
|
|
99
|
+
});
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (lower.startsWith('file:') || lower.startsWith('link:') || lower.startsWith('portal:')) {
|
|
103
|
+
addSignal(signals, seen, {
|
|
104
|
+
type: 'file-dependency',
|
|
105
|
+
packageName,
|
|
106
|
+
packageVersion,
|
|
107
|
+
source: sourceFile,
|
|
108
|
+
detail: `${packageName || 'dependency'} resolves from local source ${value}`
|
|
109
|
+
});
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (!/^https?:\/\//i.test(value))
|
|
113
|
+
return;
|
|
114
|
+
try {
|
|
115
|
+
const url = new URL(value);
|
|
116
|
+
const host = url.host.toLowerCase();
|
|
117
|
+
if (!expectedHosts.has(host)) {
|
|
118
|
+
addSignal(signals, seen, {
|
|
119
|
+
type: value.endsWith('.tgz') ? 'non-registry-tarball' : 'unexpected-registry-host',
|
|
120
|
+
packageName,
|
|
121
|
+
packageVersion,
|
|
122
|
+
source: sourceFile,
|
|
123
|
+
detail: `${packageName || 'dependency'} resolves from ${host}`
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// Ignore malformed URL strings.
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function inspectNpmLockObject(obj, sourceFile, expectedHosts) {
|
|
132
|
+
const signals = [];
|
|
133
|
+
const seen = new Set();
|
|
134
|
+
const packages = (obj === null || obj === void 0 ? void 0 : obj.packages) && typeof obj.packages === 'object'
|
|
135
|
+
? obj.packages
|
|
136
|
+
: undefined;
|
|
137
|
+
if (packages) {
|
|
138
|
+
for (const [key, entry] of Object.entries(packages)) {
|
|
139
|
+
if (!entry || typeof entry !== 'object' || key === '')
|
|
140
|
+
continue;
|
|
141
|
+
const pkg = packageFromKey(key);
|
|
142
|
+
const resolved = typeof entry.resolved === 'string' ? entry.resolved : '';
|
|
143
|
+
if (resolved)
|
|
144
|
+
inspectResolvedUrl(signals, seen, sourceFile, pkg.name, entry.version || pkg.version, resolved, expectedHosts);
|
|
145
|
+
if (resolved && !entry.integrity && !resolved.startsWith('file:') && !resolved.startsWith('link:')) {
|
|
146
|
+
addSignal(signals, seen, {
|
|
147
|
+
type: 'missing-integrity',
|
|
148
|
+
packageName: pkg.name,
|
|
149
|
+
packageVersion: entry.version || pkg.version,
|
|
150
|
+
source: sourceFile,
|
|
151
|
+
detail: `${pkg.name || key} has a resolved source but no integrity field`
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return signals;
|
|
156
|
+
}
|
|
157
|
+
const dependencies = (obj === null || obj === void 0 ? void 0 : obj.dependencies) && typeof obj.dependencies === 'object' ? obj.dependencies : {};
|
|
158
|
+
for (const [name, entry] of Object.entries(dependencies)) {
|
|
159
|
+
if (!entry || typeof entry !== 'object')
|
|
160
|
+
continue;
|
|
161
|
+
const resolved = typeof entry.resolved === 'string' ? entry.resolved : '';
|
|
162
|
+
if (resolved)
|
|
163
|
+
inspectResolvedUrl(signals, seen, sourceFile, name, entry.version, resolved, expectedHosts);
|
|
164
|
+
if (resolved && !entry.integrity && !resolved.startsWith('file:') && !resolved.startsWith('link:')) {
|
|
165
|
+
addSignal(signals, seen, {
|
|
166
|
+
type: 'missing-integrity',
|
|
167
|
+
packageName: name,
|
|
168
|
+
packageVersion: entry.version,
|
|
169
|
+
source: sourceFile,
|
|
170
|
+
detail: `${name} has a resolved source but no integrity field`
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return signals;
|
|
175
|
+
}
|
|
176
|
+
function inspectTextLock(raw, sourceFile, expectedHosts) {
|
|
177
|
+
const signals = [];
|
|
178
|
+
const seen = new Set();
|
|
179
|
+
let currentName;
|
|
180
|
+
let currentVersion;
|
|
181
|
+
let currentHasIntegrity = false;
|
|
182
|
+
let currentResolved = '';
|
|
183
|
+
const flush = () => {
|
|
184
|
+
if (currentResolved && !currentHasIntegrity && !currentResolved.startsWith('file:') && !currentResolved.startsWith('link:')) {
|
|
185
|
+
addSignal(signals, seen, {
|
|
186
|
+
type: 'missing-integrity',
|
|
187
|
+
packageName: currentName,
|
|
188
|
+
packageVersion: currentVersion,
|
|
189
|
+
source: sourceFile,
|
|
190
|
+
detail: `${currentName || 'dependency'} has a resolved source but no integrity field`
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
for (const rawLine of raw.split(/\r?\n/)) {
|
|
195
|
+
const line = rawLine.trim();
|
|
196
|
+
if (!line || line.startsWith('#'))
|
|
197
|
+
continue;
|
|
198
|
+
if (!rawLine.startsWith(' ') && line.endsWith(':')) {
|
|
199
|
+
flush();
|
|
200
|
+
currentHasIntegrity = false;
|
|
201
|
+
currentResolved = '';
|
|
202
|
+
const selector = line.slice(0, -1).replace(/^["']|["']$/g, '');
|
|
203
|
+
currentName = packageNameFromSelector(selector);
|
|
204
|
+
currentVersion = undefined;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const versionMatch = line.match(/^version\s+["']?([^"'\s]+)["']?/);
|
|
208
|
+
if (versionMatch)
|
|
209
|
+
currentVersion = versionMatch[1];
|
|
210
|
+
const resolvedMatch = line.match(/^(resolved|resolution)\s+["']?([^"']+)["']?/);
|
|
211
|
+
if (resolvedMatch) {
|
|
212
|
+
currentResolved = resolvedMatch[2];
|
|
213
|
+
inspectResolvedUrl(signals, seen, sourceFile, currentName, currentVersion, currentResolved, expectedHosts);
|
|
214
|
+
}
|
|
215
|
+
if (/^integrity\s+/.test(line) || /checksum:\s+/.test(line)) {
|
|
216
|
+
currentHasIntegrity = true;
|
|
217
|
+
}
|
|
218
|
+
const specMatch = line.match(/(?:^|["'\s])((?:git\+|git:\/\/|github:|file:|link:|portal:|https?:\/\/)[^"'\s]+)/);
|
|
219
|
+
if (specMatch)
|
|
220
|
+
inspectResolvedUrl(signals, seen, sourceFile, currentName, currentVersion, specMatch[1], expectedHosts);
|
|
221
|
+
}
|
|
222
|
+
flush();
|
|
223
|
+
return signals;
|
|
224
|
+
}
|
|
225
|
+
async function collectLockfileSignals(projectPath, expectedHosts) {
|
|
226
|
+
const signals = [];
|
|
227
|
+
const candidates = ['package-lock.json', 'npm-shrinkwrap.json', 'pnpm-lock.yaml', 'yarn.lock', 'bun.lock'];
|
|
228
|
+
for (const fileName of candidates) {
|
|
229
|
+
const filePath = path_1.default.join(projectPath, fileName);
|
|
230
|
+
if (!(await (0, utils_1.pathExists)(filePath)))
|
|
231
|
+
continue;
|
|
232
|
+
const raw = await promises_1.default.readFile(filePath, 'utf8');
|
|
233
|
+
if (fileName === 'package-lock.json' || fileName === 'npm-shrinkwrap.json') {
|
|
234
|
+
try {
|
|
235
|
+
signals.push(...inspectNpmLockObject(JSON.parse(raw), fileName, expectedHosts));
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
signals.push(...inspectTextLock(raw, fileName, expectedHosts));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
else if (fileName === 'bun.lock' && raw.trim().startsWith('{')) {
|
|
242
|
+
try {
|
|
243
|
+
signals.push(...inspectNpmLockObject(JSON.parse(stripJsonComments(raw)), fileName, expectedHosts));
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
signals.push(...inspectTextLock(raw, fileName, expectedHosts));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
signals.push(...inspectTextLock(raw, fileName, expectedHosts));
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return signals;
|
|
254
|
+
}
|
|
255
|
+
async function runNpmAuditSignatures(projectPath) {
|
|
256
|
+
try {
|
|
257
|
+
const result = await (0, utils_1.runCommand)('npm', ['audit', 'signatures'], { cwd: projectPath });
|
|
258
|
+
const output = `${result.stdout || ''}${result.stderr ? `\n${result.stderr}` : ''}`.trim();
|
|
259
|
+
return {
|
|
260
|
+
attempted: true,
|
|
261
|
+
ok: result.code === 0,
|
|
262
|
+
status: result.code === 0 ? 'verified' : 'failed',
|
|
263
|
+
...(output ? { output } : {}),
|
|
264
|
+
...(result.code === 0 ? {} : { error: `npm audit signatures exited with code ${result.code}` })
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
catch (err) {
|
|
268
|
+
return {
|
|
269
|
+
attempted: true,
|
|
270
|
+
ok: false,
|
|
271
|
+
status: 'failed',
|
|
272
|
+
error: err instanceof Error ? err.message : String(err)
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
async function runLockfileSupplyChainSignals(projectPath, tempDir, options = {}) {
|
|
277
|
+
const persistToDisk = options.persistToDisk !== false;
|
|
278
|
+
const targetFile = path_1.default.join(tempDir, 'supply-chain-signals.json');
|
|
279
|
+
try {
|
|
280
|
+
const signals = await collectLockfileSignals(projectPath, normalizeExpectedHosts(options.expectedRegistryHosts));
|
|
281
|
+
const signatureAudit = options.auditSignatures && !options.offline
|
|
282
|
+
? await runNpmAuditSignatures(projectPath)
|
|
283
|
+
: options.auditSignatures && options.offline
|
|
284
|
+
? { attempted: false, ok: false, status: 'skipped', error: 'skipped (--offline)' }
|
|
285
|
+
: undefined;
|
|
286
|
+
const data = {
|
|
287
|
+
signals,
|
|
288
|
+
...(signatureAudit ? { signatureAudit } : {})
|
|
289
|
+
};
|
|
290
|
+
if (persistToDisk)
|
|
291
|
+
await (0, utils_1.writeJsonFile)(targetFile, data);
|
|
292
|
+
return { ok: true, data, ...(persistToDisk ? { file: targetFile } : {}) };
|
|
293
|
+
}
|
|
294
|
+
catch (err) {
|
|
295
|
+
if (persistToDisk)
|
|
296
|
+
await (0, utils_1.writeJsonFile)(targetFile, { error: String(err) });
|
|
297
|
+
return {
|
|
298
|
+
ok: false,
|
|
299
|
+
error: `lockfile supply-chain signal collection failed: ${String(err)}`,
|
|
300
|
+
...(persistToDisk ? { file: targetFile } : {})
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
}
|
package/dist/runners/npmLs.js
CHANGED
|
@@ -30,9 +30,24 @@ async function runNpmLs(projectPath, tempDir, tool = 'npm', options = {}) {
|
|
|
30
30
|
}
|
|
31
31
|
return { ok: true, data: lockfileTree.data, ...(persistToDisk ? { file: targetFile } : {}) };
|
|
32
32
|
}
|
|
33
|
+
if (tool === 'bun') {
|
|
34
|
+
const lockbPath = path_1.default.join(projectPath, 'bun.lockb');
|
|
35
|
+
if (fs_1.default.existsSync(lockbPath) && !fs_1.default.existsSync(path_1.default.join(projectPath, 'bun.lock'))) {
|
|
36
|
+
const error = 'Binary bun.lockb is not supported. Run `bun install --save-text-lockfile --frozen-lockfile --lockfile-only` and commit bun.lock.';
|
|
37
|
+
if (persistToDisk)
|
|
38
|
+
await (0, utils_1.writeJsonFile)(targetFile, { error });
|
|
39
|
+
return { ok: false, error, ...(persistToDisk ? { file: targetFile } : {}) };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
33
42
|
if (tool === 'pnpm') {
|
|
34
43
|
return await runPnpmLsWithFallback(projectPath, targetFile, options);
|
|
35
44
|
}
|
|
45
|
+
if (tool === 'bun') {
|
|
46
|
+
const error = 'bun.lock could not be parsed and Bun has no supported list fallback in this release.';
|
|
47
|
+
if (persistToDisk)
|
|
48
|
+
await (0, utils_1.writeJsonFile)(targetFile, { error });
|
|
49
|
+
return { ok: false, error, ...(persistToDisk ? { file: targetFile } : {}) };
|
|
50
|
+
}
|
|
36
51
|
const { args, normalize } = buildLsCommand(tool);
|
|
37
52
|
const result = await (0, utils_1.runCommand)(tool, args, { cwd: projectPath });
|
|
38
53
|
const parsed = parseJsonOutput(result.stdout);
|
package/dist/schema.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.REPORT_JSON_SCHEMA = exports.REPORT_SCHEMA_VERSION = void 0;
|
|
4
|
+
exports.renderReportJsonSchema = renderReportJsonSchema;
|
|
5
|
+
exports.REPORT_SCHEMA_VERSION = '1.4';
|
|
6
|
+
exports.REPORT_JSON_SCHEMA = {
|
|
7
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
8
|
+
$id: 'https://dependency-radar.com/schemas/dependency-radar-1.4.schema.json',
|
|
9
|
+
title: 'Dependency Radar Report',
|
|
10
|
+
type: 'object',
|
|
11
|
+
required: ['schemaVersion', 'generatedAt', 'dependencyRadarVersion', 'project', 'environment', 'workspaces', 'summary', 'dependencies'],
|
|
12
|
+
properties: {
|
|
13
|
+
schemaVersion: { const: exports.REPORT_SCHEMA_VERSION },
|
|
14
|
+
generatedAt: { type: 'string', format: 'date-time' },
|
|
15
|
+
dependencyRadarVersion: { type: 'string' },
|
|
16
|
+
git: {
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: { branch: { type: 'string' } },
|
|
19
|
+
additionalProperties: true
|
|
20
|
+
},
|
|
21
|
+
project: { type: 'object', additionalProperties: true },
|
|
22
|
+
environment: { type: 'object', additionalProperties: true },
|
|
23
|
+
workspaces: { type: 'object', additionalProperties: true },
|
|
24
|
+
summary: {
|
|
25
|
+
type: 'object',
|
|
26
|
+
required: ['dependencyCount', 'directCount', 'transitiveCount'],
|
|
27
|
+
properties: {
|
|
28
|
+
dependencyCount: { type: 'number' },
|
|
29
|
+
directCount: { type: 'number' },
|
|
30
|
+
transitiveCount: { type: 'number' },
|
|
31
|
+
findingCount: { type: 'number' }
|
|
32
|
+
},
|
|
33
|
+
additionalProperties: true
|
|
34
|
+
},
|
|
35
|
+
supplyChain: {
|
|
36
|
+
type: 'object',
|
|
37
|
+
required: ['signals'],
|
|
38
|
+
properties: {
|
|
39
|
+
signals: {
|
|
40
|
+
type: 'array',
|
|
41
|
+
items: {
|
|
42
|
+
type: 'object',
|
|
43
|
+
required: ['type', 'source', 'detail'],
|
|
44
|
+
properties: {
|
|
45
|
+
type: {
|
|
46
|
+
enum: [
|
|
47
|
+
'git-dependency',
|
|
48
|
+
'file-dependency',
|
|
49
|
+
'non-registry-tarball',
|
|
50
|
+
'missing-integrity',
|
|
51
|
+
'unexpected-registry-host'
|
|
52
|
+
]
|
|
53
|
+
},
|
|
54
|
+
packageName: { type: 'string' },
|
|
55
|
+
packageVersion: { type: 'string' },
|
|
56
|
+
packageId: { type: 'string' },
|
|
57
|
+
source: { type: 'string' },
|
|
58
|
+
detail: { type: 'string' }
|
|
59
|
+
},
|
|
60
|
+
additionalProperties: false
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
signatureAudit: {
|
|
64
|
+
type: 'object',
|
|
65
|
+
required: ['attempted', 'ok'],
|
|
66
|
+
properties: {
|
|
67
|
+
attempted: { type: 'boolean' },
|
|
68
|
+
ok: { type: 'boolean' },
|
|
69
|
+
status: { enum: ['verified', 'failed', 'skipped'] },
|
|
70
|
+
output: { type: 'string' },
|
|
71
|
+
error: { type: 'string' }
|
|
72
|
+
},
|
|
73
|
+
additionalProperties: false
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
additionalProperties: false
|
|
77
|
+
},
|
|
78
|
+
findings: {
|
|
79
|
+
type: 'array',
|
|
80
|
+
items: {
|
|
81
|
+
type: 'object',
|
|
82
|
+
required: ['id', 'category', 'severity', 'packageId', 'packageName', 'packageVersion', 'title', 'message'],
|
|
83
|
+
properties: {
|
|
84
|
+
id: { type: 'string' },
|
|
85
|
+
category: { enum: ['security', 'license', 'execution', 'upgrade', 'supply-chain'] },
|
|
86
|
+
severity: { enum: ['info', 'warning', 'error'] },
|
|
87
|
+
packageId: { type: 'string' },
|
|
88
|
+
packageName: { type: 'string' },
|
|
89
|
+
packageVersion: { type: 'string' },
|
|
90
|
+
title: { type: 'string' },
|
|
91
|
+
message: { type: 'string' },
|
|
92
|
+
evidence: { type: 'string' },
|
|
93
|
+
recommendation: { type: 'string' }
|
|
94
|
+
},
|
|
95
|
+
additionalProperties: false
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
dependencies: {
|
|
99
|
+
type: 'object',
|
|
100
|
+
additionalProperties: { type: 'object', additionalProperties: true }
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
additionalProperties: true
|
|
104
|
+
};
|
|
105
|
+
function renderReportJsonSchema() {
|
|
106
|
+
return JSON.stringify(exports.REPORT_JSON_SCHEMA, null, 2);
|
|
107
|
+
}
|
package/dist/utils.js
CHANGED
|
@@ -25,6 +25,13 @@ const promises_1 = __importDefault(require("fs/promises"));
|
|
|
25
25
|
const path_1 = __importDefault(require("path"));
|
|
26
26
|
function runCommand(command, args, options = {}) {
|
|
27
27
|
return new Promise((resolve, reject) => {
|
|
28
|
+
var _a;
|
|
29
|
+
const validPositive = (value, fallback) => {
|
|
30
|
+
const parsed = typeof value === 'number' ? value : Number(value);
|
|
31
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
32
|
+
};
|
|
33
|
+
const timeoutMs = validPositive((_a = options.timeoutMs) !== null && _a !== void 0 ? _a : process.env.DEPENDENCY_RADAR_COMMAND_TIMEOUT_MS, 120000);
|
|
34
|
+
const maxOutputBytes = validPositive(options.maxOutputBytes, 50 * 1024 * 1024);
|
|
28
35
|
const child = (0, child_process_1.spawn)(command, args, {
|
|
29
36
|
cwd: options.cwd,
|
|
30
37
|
shell: false,
|
|
@@ -32,10 +39,62 @@ function runCommand(command, args, options = {}) {
|
|
|
32
39
|
});
|
|
33
40
|
const stdoutChunks = [];
|
|
34
41
|
const stderrChunks = [];
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
42
|
+
let totalBytes = 0;
|
|
43
|
+
let settled = false;
|
|
44
|
+
let timedOut = false;
|
|
45
|
+
let outputExceeded = false;
|
|
46
|
+
function terminate() {
|
|
47
|
+
var _a, _b;
|
|
48
|
+
child.kill('SIGTERM');
|
|
49
|
+
(_b = (_a = setTimeout(() => {
|
|
50
|
+
if (!settled)
|
|
51
|
+
child.kill('SIGKILL');
|
|
52
|
+
}, 2000)).unref) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
53
|
+
}
|
|
54
|
+
const timer = timeoutMs > 0
|
|
55
|
+
? setTimeout(() => {
|
|
56
|
+
timedOut = true;
|
|
57
|
+
terminate();
|
|
58
|
+
}, timeoutMs)
|
|
59
|
+
: undefined;
|
|
60
|
+
function collect(chunks, data) {
|
|
61
|
+
const nextBytes = totalBytes + data.length;
|
|
62
|
+
if (nextBytes > maxOutputBytes) {
|
|
63
|
+
outputExceeded = true;
|
|
64
|
+
const remaining = Math.max(0, maxOutputBytes - totalBytes);
|
|
65
|
+
if (remaining > 0)
|
|
66
|
+
chunks.push(Buffer.from(data.subarray(0, remaining)));
|
|
67
|
+
totalBytes = maxOutputBytes;
|
|
68
|
+
terminate();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
chunks.push(Buffer.from(data));
|
|
72
|
+
totalBytes = nextBytes;
|
|
73
|
+
}
|
|
74
|
+
child.stdout.on('data', (d) => {
|
|
75
|
+
collect(stdoutChunks, Buffer.from(d));
|
|
76
|
+
});
|
|
77
|
+
child.stderr.on('data', (d) => {
|
|
78
|
+
collect(stderrChunks, Buffer.from(d));
|
|
79
|
+
});
|
|
80
|
+
child.on('error', (err) => {
|
|
81
|
+
if (timer)
|
|
82
|
+
clearTimeout(timer);
|
|
83
|
+
settled = true;
|
|
84
|
+
reject(err);
|
|
85
|
+
});
|
|
38
86
|
child.on('close', (code) => {
|
|
87
|
+
if (timer)
|
|
88
|
+
clearTimeout(timer);
|
|
89
|
+
settled = true;
|
|
90
|
+
if (timedOut) {
|
|
91
|
+
reject(new Error(`${command} timed out after ${timeoutMs}ms`));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (outputExceeded) {
|
|
95
|
+
reject(new Error(`${command} output exceeded ${maxOutputBytes} bytes`));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
39
98
|
resolve({
|
|
40
99
|
stdout: Buffer.concat(stdoutChunks).toString('utf8'),
|
|
41
100
|
stderr: Buffer.concat(stderrChunks).toString('utf8'),
|
package/dist/why.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.findDependencyPaths = findDependencyPaths;
|
|
4
|
+
exports.formatWhyOutput = formatWhyOutput;
|
|
5
|
+
function directDependencyIds(data) {
|
|
6
|
+
return Object.values(data.dependencies || {})
|
|
7
|
+
.filter((dep) => dep.usage.direct)
|
|
8
|
+
.map((dep) => dep.package.id)
|
|
9
|
+
.sort();
|
|
10
|
+
}
|
|
11
|
+
function childIds(dep) {
|
|
12
|
+
return Object.values(dep.graph.subDeps || {})
|
|
13
|
+
.flatMap((group) => Object.values(group || {}))
|
|
14
|
+
.map((entry) => entry[1])
|
|
15
|
+
.filter((resolved) => Boolean(resolved));
|
|
16
|
+
}
|
|
17
|
+
function findDependencyPaths(data, packageName, options = {}) {
|
|
18
|
+
var _a;
|
|
19
|
+
const limit = (_a = options.limit) !== null && _a !== void 0 ? _a : 10;
|
|
20
|
+
const targetIds = new Set(Object.values(data.dependencies || {})
|
|
21
|
+
.filter((dep) => dep.package.name === packageName || dep.package.id === packageName)
|
|
22
|
+
.map((dep) => dep.package.id));
|
|
23
|
+
if (targetIds.size === 0)
|
|
24
|
+
return [];
|
|
25
|
+
const paths = [];
|
|
26
|
+
const queue = directDependencyIds(data).map((id) => [id]);
|
|
27
|
+
while (queue.length > 0 && paths.length < limit) {
|
|
28
|
+
const path = queue.shift();
|
|
29
|
+
const currentId = path[path.length - 1];
|
|
30
|
+
if (targetIds.has(currentId)) {
|
|
31
|
+
paths.push(path);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const current = data.dependencies[currentId];
|
|
35
|
+
if (!current)
|
|
36
|
+
continue;
|
|
37
|
+
for (const child of childIds(current).sort()) {
|
|
38
|
+
if (path.includes(child))
|
|
39
|
+
continue;
|
|
40
|
+
queue.push([...path, child]);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return paths;
|
|
44
|
+
}
|
|
45
|
+
function formatWhyOutput(data, packageName) {
|
|
46
|
+
const paths = findDependencyPaths(data, packageName);
|
|
47
|
+
const matches = Object.values(data.dependencies || {})
|
|
48
|
+
.filter((dep) => dep.package.name === packageName || dep.package.id === packageName)
|
|
49
|
+
.sort((a, b) => a.package.id.localeCompare(b.package.id));
|
|
50
|
+
if (paths.length === 0 && matches.length === 0)
|
|
51
|
+
return `Package not found or no path identified: ${packageName}`;
|
|
52
|
+
const lines = [`Dependency paths for ${packageName}`, '-'.repeat(Math.max(24, packageName.length + 21)), ''];
|
|
53
|
+
if (paths.length === 0) {
|
|
54
|
+
for (const dep of matches) {
|
|
55
|
+
lines.push(dep.package.id);
|
|
56
|
+
if (dep.usage.origins.topRootPackages.length > 0) {
|
|
57
|
+
lines.push(` roots: ${dep.usage.origins.topRootPackages.map((root) => `${root.name}@${root.version}`).join(', ')}`);
|
|
58
|
+
}
|
|
59
|
+
if (dep.usage.origins.topParentPackages.length > 0) {
|
|
60
|
+
lines.push(` parents: ${dep.usage.origins.topParentPackages.join(', ')}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return lines.join('\n');
|
|
64
|
+
}
|
|
65
|
+
for (const path of paths) {
|
|
66
|
+
lines.push(path.join(' -> '));
|
|
67
|
+
}
|
|
68
|
+
return lines.join('\n');
|
|
69
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildWorkspaceFilterOptions = buildWorkspaceFilterOptions;
|
|
4
|
+
function buildWorkspaceFilterOptions(report) {
|
|
5
|
+
if (!report.workspaces.enabled)
|
|
6
|
+
return [];
|
|
7
|
+
const names = new Set();
|
|
8
|
+
(report.workspaces.workspacePackages || []).forEach((workspace) => {
|
|
9
|
+
if (workspace.name)
|
|
10
|
+
names.add(workspace.name);
|
|
11
|
+
});
|
|
12
|
+
Object.values(report.dependencies || {}).forEach((dep) => {
|
|
13
|
+
(dep.usage.origins.workspaces || []).forEach((workspaceName) => {
|
|
14
|
+
if (workspaceName)
|
|
15
|
+
names.add(workspaceName);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
return Array.from(names).sort((a, b) => {
|
|
19
|
+
if (a === 'root')
|
|
20
|
+
return -1;
|
|
21
|
+
if (b === 'root')
|
|
22
|
+
return 1;
|
|
23
|
+
return a.localeCompare(b);
|
|
24
|
+
});
|
|
25
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dependency-radar",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Local-first dependency analysis tool that generates a single HTML report showing risk, size, usage, and structure of your project's dependencies.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -10,11 +10,12 @@
|
|
|
10
10
|
"dist"
|
|
11
11
|
],
|
|
12
12
|
"scripts": {
|
|
13
|
+
"clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
13
14
|
"dev:report": "cd report-ui && npx vite",
|
|
14
15
|
"build:spdx": "ts-node scripts/build-spdx.ts",
|
|
15
16
|
"build:report-ui": "cd report-ui && npx vite build",
|
|
16
17
|
"build:report": "npm run build:report-ui && npx ts-node scripts/build-report.ts",
|
|
17
|
-
"build": "npm run build:spdx && npm run build:report && tsc",
|
|
18
|
+
"build": "npm run clean && npm run build:spdx && npm run build:report && tsc",
|
|
18
19
|
"dev": "ts-node src/cli.ts scan",
|
|
19
20
|
"scan": "node dist/cli.js scan",
|
|
20
21
|
"test": "npm run test:unit",
|
|
@@ -30,7 +31,7 @@
|
|
|
30
31
|
"test:release": "npm run build && npm run test:unit && npm run test:fixtures:all && npm run test:docker && npm run test:pack && npm run test:release:ok",
|
|
31
32
|
"prepublishOnly": "npm run build"
|
|
32
33
|
},
|
|
33
|
-
"author": " ",
|
|
34
|
+
"author": "Joseph Maynard",
|
|
34
35
|
"license": "MIT",
|
|
35
36
|
"engines": {
|
|
36
37
|
"node": ">=14.14.0"
|
|
@@ -39,7 +40,7 @@
|
|
|
39
40
|
"type": "git",
|
|
40
41
|
"url": "git+https://github.com/JosephMaynard/dependency-radar.git"
|
|
41
42
|
},
|
|
42
|
-
"homepage": "
|
|
43
|
+
"homepage": "https://www.dependency-radar.com",
|
|
43
44
|
"bugs": {
|
|
44
45
|
"url": "https://github.com/JosephMaynard/dependency-radar/issues"
|
|
45
46
|
},
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.runDepcheck = runDepcheck;
|
|
7
|
-
const path_1 = __importDefault(require("path"));
|
|
8
|
-
const depcheck_1 = __importDefault(require("depcheck"));
|
|
9
|
-
const utils_1 = require("../utils");
|
|
10
|
-
async function runDepcheck(projectPath, tempDir) {
|
|
11
|
-
const targetFile = path_1.default.join(tempDir, 'depcheck.json');
|
|
12
|
-
try {
|
|
13
|
-
const result = await (0, depcheck_1.default)(projectPath, {
|
|
14
|
-
ignoreDirs: ['.dependency-radar', 'dist', 'build', 'coverage', 'node_modules']
|
|
15
|
-
});
|
|
16
|
-
await (0, utils_1.writeJsonFile)(targetFile, result);
|
|
17
|
-
return { ok: true, data: result, file: targetFile };
|
|
18
|
-
}
|
|
19
|
-
catch (err) {
|
|
20
|
-
await (0, utils_1.writeJsonFile)(targetFile, { error: String(err) });
|
|
21
|
-
return { ok: false, error: `depcheck failed: ${String(err)}`, file: targetFile };
|
|
22
|
-
}
|
|
23
|
-
}
|