@westbayberry/dg 1.0.3 → 1.0.4
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/dist/index.mjs +4704 -4296
- package/package.json +1 -1
- package/dist/index.js +0 -1246
package/dist/index.js
DELETED
|
@@ -1,1246 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/******/ (() => { // webpackBootstrap
|
|
3
|
-
/******/ "use strict";
|
|
4
|
-
/******/ var __webpack_modules__ = ({
|
|
5
|
-
|
|
6
|
-
/***/ 879:
|
|
7
|
-
/***/ ((__unused_webpack_module, exports) => {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
11
|
-
exports.APIError = void 0;
|
|
12
|
-
exports.callAnalyzeAPI = callAnalyzeAPI;
|
|
13
|
-
class APIError extends Error {
|
|
14
|
-
constructor(message, statusCode, body) {
|
|
15
|
-
super(message);
|
|
16
|
-
this.statusCode = statusCode;
|
|
17
|
-
this.body = body;
|
|
18
|
-
this.name = "APIError";
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
exports.APIError = APIError;
|
|
22
|
-
const BATCH_SIZE = 15;
|
|
23
|
-
const MAX_RETRIES = 2;
|
|
24
|
-
const RETRY_DELAY_MS = 5000;
|
|
25
|
-
async function callAnalyzeAPI(packages, config, onProgress) {
|
|
26
|
-
if (packages.length <= BATCH_SIZE) {
|
|
27
|
-
return callAnalyzeBatch(packages, config);
|
|
28
|
-
}
|
|
29
|
-
const batches = [];
|
|
30
|
-
for (let i = 0; i < packages.length; i += BATCH_SIZE) {
|
|
31
|
-
batches.push(packages.slice(i, i + BATCH_SIZE));
|
|
32
|
-
}
|
|
33
|
-
// Process batches sequentially — parallel batches overload the server on cold scans
|
|
34
|
-
const results = [];
|
|
35
|
-
let completed = 0;
|
|
36
|
-
for (const batch of batches) {
|
|
37
|
-
const result = await callBatchWithRetry(batch, config);
|
|
38
|
-
completed += batch.length;
|
|
39
|
-
if (onProgress) {
|
|
40
|
-
onProgress(completed, packages.length);
|
|
41
|
-
}
|
|
42
|
-
results.push(result);
|
|
43
|
-
}
|
|
44
|
-
return mergeResponses(results, config);
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Retry a batch on 504 (gateway timeout). The server continues processing
|
|
48
|
-
* during a timeout, so retries find previously-analyzed packages cached
|
|
49
|
-
* in Redis — fewer cold packages means faster response.
|
|
50
|
-
*/
|
|
51
|
-
async function callBatchWithRetry(packages, config) {
|
|
52
|
-
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
53
|
-
try {
|
|
54
|
-
return await callAnalyzeBatch(packages, config);
|
|
55
|
-
}
|
|
56
|
-
catch (error) {
|
|
57
|
-
const is504 = error instanceof APIError && error.statusCode === 504;
|
|
58
|
-
const isTimeout = error instanceof APIError && error.statusCode === 408;
|
|
59
|
-
if ((is504 || isTimeout) && attempt < MAX_RETRIES) {
|
|
60
|
-
// Server is still working — wait, then retry.
|
|
61
|
-
// Packages that finished before the timeout are now cached.
|
|
62
|
-
const delay = RETRY_DELAY_MS * (attempt + 1);
|
|
63
|
-
process.stderr.write(` Batch timed out, retrying in ${delay / 1000}s (attempt ${attempt + 2}/${MAX_RETRIES + 1})...\n`);
|
|
64
|
-
await new Promise((r) => setTimeout(r, delay));
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
67
|
-
throw error;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
// Unreachable, but TypeScript needs it
|
|
71
|
-
throw new Error("Exhausted retries");
|
|
72
|
-
}
|
|
73
|
-
function mergeResponses(results, config) {
|
|
74
|
-
const allPackages = results.flatMap((r) => r.packages);
|
|
75
|
-
const maxScore = Math.max(0, ...allPackages.map((p) => p.score));
|
|
76
|
-
const action = maxScore >= config.blockThreshold
|
|
77
|
-
? "block"
|
|
78
|
-
: maxScore >= config.warnThreshold
|
|
79
|
-
? "warn"
|
|
80
|
-
: "pass";
|
|
81
|
-
const safeVersions = {};
|
|
82
|
-
for (const r of results) {
|
|
83
|
-
Object.assign(safeVersions, r.safeVersions);
|
|
84
|
-
}
|
|
85
|
-
return {
|
|
86
|
-
score: maxScore,
|
|
87
|
-
action: action,
|
|
88
|
-
packages: allPackages,
|
|
89
|
-
safeVersions,
|
|
90
|
-
durationMs: Math.max(0, ...results.map((r) => r.durationMs)),
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
async function callAnalyzeBatch(packages, config) {
|
|
94
|
-
const url = `${config.apiUrl}/v1/analyze`;
|
|
95
|
-
const payload = {
|
|
96
|
-
packages: packages.map((p) => ({
|
|
97
|
-
name: p.name,
|
|
98
|
-
version: p.version,
|
|
99
|
-
previousVersion: p.previousVersion,
|
|
100
|
-
isNew: p.isNew,
|
|
101
|
-
})),
|
|
102
|
-
config: {
|
|
103
|
-
blockThreshold: config.blockThreshold,
|
|
104
|
-
warnThreshold: config.warnThreshold,
|
|
105
|
-
},
|
|
106
|
-
};
|
|
107
|
-
const controller = new AbortController();
|
|
108
|
-
const timeoutId = setTimeout(() => controller.abort(), 120000);
|
|
109
|
-
let response;
|
|
110
|
-
try {
|
|
111
|
-
response = await fetch(url, {
|
|
112
|
-
method: "POST",
|
|
113
|
-
headers: {
|
|
114
|
-
"Content-Type": "application/json",
|
|
115
|
-
Authorization: `Bearer ${config.apiKey}`,
|
|
116
|
-
"User-Agent": "dependency-guardian-cli/1.0.0",
|
|
117
|
-
},
|
|
118
|
-
body: JSON.stringify(payload),
|
|
119
|
-
signal: controller.signal,
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
catch (error) {
|
|
123
|
-
clearTimeout(timeoutId);
|
|
124
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
125
|
-
throw new APIError("Request timed out after 120s. Try scanning fewer packages.", 408, "");
|
|
126
|
-
}
|
|
127
|
-
// Node.js fetch wraps the real error in .cause
|
|
128
|
-
const cause = error instanceof Error && error.cause;
|
|
129
|
-
const detail = cause ? `: ${cause.message || cause}` : "";
|
|
130
|
-
throw new Error(`fetch failed${detail}`);
|
|
131
|
-
}
|
|
132
|
-
clearTimeout(timeoutId);
|
|
133
|
-
if (response.status === 401) {
|
|
134
|
-
throw new APIError("Invalid API key. Check your --api-key or DG_API_KEY value.\n" +
|
|
135
|
-
"Get your key at https://westbayberry.com/dashboard", 401, "");
|
|
136
|
-
}
|
|
137
|
-
if (response.status === 429) {
|
|
138
|
-
throw new APIError("Rate limit exceeded. Upgrade your plan at https://westbayberry.com/pricing", 429, "");
|
|
139
|
-
}
|
|
140
|
-
if (!response.ok) {
|
|
141
|
-
const body = await response.text();
|
|
142
|
-
throw new APIError(`API returned ${response.status}: ${body}`, response.status, body);
|
|
143
|
-
}
|
|
144
|
-
return (await response.json());
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
/***/ }),
|
|
149
|
-
|
|
150
|
-
/***/ 988:
|
|
151
|
-
/***/ ((__unused_webpack_module, exports) => {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Zero-dependency ANSI color helpers.
|
|
156
|
-
* Disabled when stdout is not a TTY or NO_COLOR is set.
|
|
157
|
-
*/
|
|
158
|
-
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
159
|
-
exports.cyan = exports.yellow = exports.green = exports.red = exports.dim = exports.bold = void 0;
|
|
160
|
-
const enabled = process.stdout.isTTY === true && !process.env.NO_COLOR;
|
|
161
|
-
const wrap = (code, reset) => enabled ? (s) => `\x1b[${code}m${s}\x1b[${reset}m` : (s) => s;
|
|
162
|
-
exports.bold = wrap("1", "22");
|
|
163
|
-
exports.dim = wrap("2", "22");
|
|
164
|
-
exports.red = wrap("31", "39");
|
|
165
|
-
exports.green = wrap("32", "39");
|
|
166
|
-
exports.yellow = wrap("33", "39");
|
|
167
|
-
exports.cyan = wrap("36", "39");
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
/***/ }),
|
|
171
|
-
|
|
172
|
-
/***/ 973:
|
|
173
|
-
/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
177
|
-
exports.USAGE = void 0;
|
|
178
|
-
exports.parseConfig = parseConfig;
|
|
179
|
-
exports.getVersion = getVersion;
|
|
180
|
-
const node_util_1 = __nccwpck_require__(975);
|
|
181
|
-
const node_fs_1 = __nccwpck_require__(24);
|
|
182
|
-
const node_path_1 = __nccwpck_require__(760);
|
|
183
|
-
const node_os_1 = __nccwpck_require__(161);
|
|
184
|
-
function loadDgrc() {
|
|
185
|
-
const candidates = [
|
|
186
|
-
(0, node_path_1.join)(process.cwd(), ".dgrc.json"),
|
|
187
|
-
(0, node_path_1.join)((0, node_os_1.homedir)(), ".dgrc.json"),
|
|
188
|
-
];
|
|
189
|
-
for (const filepath of candidates) {
|
|
190
|
-
if ((0, node_fs_1.existsSync)(filepath)) {
|
|
191
|
-
try {
|
|
192
|
-
return JSON.parse((0, node_fs_1.readFileSync)(filepath, "utf-8"));
|
|
193
|
-
}
|
|
194
|
-
catch {
|
|
195
|
-
process.stderr.write(`Warning: Failed to parse ${filepath}, ignoring.\n`);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
return {};
|
|
200
|
-
}
|
|
201
|
-
const USAGE = `
|
|
202
|
-
Dependency Guardian — Supply chain security scanner
|
|
203
|
-
|
|
204
|
-
Usage:
|
|
205
|
-
dependency-guardian scan [options]
|
|
206
|
-
dg scan [options]
|
|
207
|
-
dg npm install <pkg> [npm-flags]
|
|
208
|
-
dg wrap
|
|
209
|
-
|
|
210
|
-
Commands:
|
|
211
|
-
scan Scan dependencies for security risks (default)
|
|
212
|
-
npm Wrap npm commands — scans packages before installing
|
|
213
|
-
wrap Show instructions to alias npm to dg
|
|
214
|
-
|
|
215
|
-
Options:
|
|
216
|
-
--api-key <key> API key (or set DG_API_KEY env var)
|
|
217
|
-
--api-url <url> API base URL (default: https://api.westbayberry.com)
|
|
218
|
-
--mode <mode> block | warn | off (default: warn)
|
|
219
|
-
--block-threshold <n> Score threshold for blocking (default: 70)
|
|
220
|
-
--warn-threshold <n> Score threshold for warnings (default: 60)
|
|
221
|
-
--max-packages <n> Max packages per scan (default: 200)
|
|
222
|
-
--allowlist <pkgs> Comma-separated package names to skip
|
|
223
|
-
--json Output JSON for CI parsing
|
|
224
|
-
--scan-all Scan all packages, not just changed
|
|
225
|
-
--base-lockfile <path> Path to base lockfile for explicit diff
|
|
226
|
-
--debug Show diagnostic output (discovery, batches, timing)
|
|
227
|
-
--no-config Skip loading .dgrc.json config file
|
|
228
|
-
--help Show this help message
|
|
229
|
-
--version Show version number
|
|
230
|
-
|
|
231
|
-
Config File:
|
|
232
|
-
Place a .dgrc.json in your project root or home directory.
|
|
233
|
-
Precedence: CLI flags > env vars > .dgrc.json > defaults
|
|
234
|
-
|
|
235
|
-
Environment Variables:
|
|
236
|
-
DG_API_KEY API key
|
|
237
|
-
DG_API_URL API base URL
|
|
238
|
-
DG_MODE Mode (block/warn/off)
|
|
239
|
-
DG_ALLOWLIST Comma-separated allowlist
|
|
240
|
-
DG_DEBUG Enable debug output (set to 1)
|
|
241
|
-
|
|
242
|
-
Exit Codes:
|
|
243
|
-
0 pass — No risks detected
|
|
244
|
-
1 warn — Risks detected (advisory)
|
|
245
|
-
2 block — High-risk packages detected
|
|
246
|
-
3 error — Internal error (API failure, config error)
|
|
247
|
-
|
|
248
|
-
Examples:
|
|
249
|
-
DG_API_KEY=dg_live_xxx dg scan
|
|
250
|
-
dg scan --api-key dg_live_xxx --json
|
|
251
|
-
dg scan --scan-all --mode block
|
|
252
|
-
dg scan --base-lockfile ./main-lockfile.json
|
|
253
|
-
dg npm install express lodash
|
|
254
|
-
dg npm install @scope/pkg@^2.0.0
|
|
255
|
-
dg npm install risky-pkg --dg-force
|
|
256
|
-
`.trimStart();
|
|
257
|
-
exports.USAGE = USAGE;
|
|
258
|
-
function getVersion() {
|
|
259
|
-
try {
|
|
260
|
-
const pkg = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(__dirname, "..", "package.json"), "utf-8"));
|
|
261
|
-
return pkg.version ?? "1.0.0";
|
|
262
|
-
}
|
|
263
|
-
catch {
|
|
264
|
-
return "1.0.0";
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
function parseConfig(argv) {
|
|
268
|
-
const { values, positionals } = (0, node_util_1.parseArgs)({
|
|
269
|
-
args: argv.slice(2),
|
|
270
|
-
options: {
|
|
271
|
-
"api-key": { type: "string" },
|
|
272
|
-
"api-url": { type: "string" },
|
|
273
|
-
mode: { type: "string" },
|
|
274
|
-
"block-threshold": { type: "string" },
|
|
275
|
-
"warn-threshold": { type: "string" },
|
|
276
|
-
"max-packages": { type: "string" },
|
|
277
|
-
allowlist: { type: "string" },
|
|
278
|
-
json: { type: "boolean", default: false },
|
|
279
|
-
"scan-all": { type: "boolean", default: false },
|
|
280
|
-
"base-lockfile": { type: "string" },
|
|
281
|
-
debug: { type: "boolean", default: false },
|
|
282
|
-
"no-config": { type: "boolean", default: false },
|
|
283
|
-
help: { type: "boolean", default: false },
|
|
284
|
-
version: { type: "boolean", default: false },
|
|
285
|
-
},
|
|
286
|
-
allowPositionals: true,
|
|
287
|
-
strict: false,
|
|
288
|
-
});
|
|
289
|
-
if (values.help) {
|
|
290
|
-
process.stdout.write(USAGE);
|
|
291
|
-
process.exit(0);
|
|
292
|
-
}
|
|
293
|
-
if (values.version) {
|
|
294
|
-
process.stdout.write(`dependency-guardian v${getVersion()}\n`);
|
|
295
|
-
process.exit(0);
|
|
296
|
-
}
|
|
297
|
-
const command = positionals[0] ?? "scan";
|
|
298
|
-
const noConfig = values["no-config"];
|
|
299
|
-
const dgrc = noConfig ? {} : loadDgrc();
|
|
300
|
-
if (values["api-key"]) {
|
|
301
|
-
process.stderr.write("Warning: --api-key is deprecated (visible in process list). Use DG_API_KEY env var instead.\n");
|
|
302
|
-
}
|
|
303
|
-
const apiKey = values["api-key"] ??
|
|
304
|
-
process.env.DG_API_KEY ??
|
|
305
|
-
dgrc.apiKey ??
|
|
306
|
-
"";
|
|
307
|
-
if (!apiKey) {
|
|
308
|
-
process.stderr.write("Error: API key required. Set DG_API_KEY environment variable.\n" +
|
|
309
|
-
"Do NOT pass keys via --api-key (visible in process list).\n" +
|
|
310
|
-
"Get your key at https://westbayberry.com/dashboard\n");
|
|
311
|
-
process.exit(1);
|
|
312
|
-
}
|
|
313
|
-
const modeRaw = values.mode ??
|
|
314
|
-
process.env.DG_MODE ??
|
|
315
|
-
dgrc.mode ??
|
|
316
|
-
"warn";
|
|
317
|
-
if (!["block", "warn", "off"].includes(modeRaw)) {
|
|
318
|
-
process.stderr.write(`Error: Invalid mode "${modeRaw}". Must be block, warn, or off.\n`);
|
|
319
|
-
process.exit(1);
|
|
320
|
-
}
|
|
321
|
-
const allowlistRaw = values.allowlist ??
|
|
322
|
-
process.env.DG_ALLOWLIST ??
|
|
323
|
-
"";
|
|
324
|
-
const blockThreshold = Number(values["block-threshold"] ?? dgrc.blockThreshold ?? "70");
|
|
325
|
-
const warnThreshold = Number(values["warn-threshold"] ?? dgrc.warnThreshold ?? "60");
|
|
326
|
-
const maxPackages = Number(values["max-packages"] ?? dgrc.maxPackages ?? "200");
|
|
327
|
-
const debug = values.debug || process.env.DG_DEBUG === "1";
|
|
328
|
-
if (isNaN(blockThreshold) || blockThreshold < 0 || blockThreshold > 100) {
|
|
329
|
-
process.stderr.write("Error: --block-threshold must be a number between 0 and 100\n");
|
|
330
|
-
process.exit(1);
|
|
331
|
-
}
|
|
332
|
-
if (isNaN(warnThreshold) || warnThreshold < 0 || warnThreshold > 100) {
|
|
333
|
-
process.stderr.write("Error: --warn-threshold must be a number between 0 and 100\n");
|
|
334
|
-
process.exit(1);
|
|
335
|
-
}
|
|
336
|
-
if (isNaN(maxPackages) || maxPackages < 1 || maxPackages > 10000) {
|
|
337
|
-
process.stderr.write("Error: --max-packages must be a number between 1 and 10000\n");
|
|
338
|
-
process.exit(1);
|
|
339
|
-
}
|
|
340
|
-
return {
|
|
341
|
-
apiKey,
|
|
342
|
-
apiUrl: values["api-url"] ??
|
|
343
|
-
process.env.DG_API_URL ??
|
|
344
|
-
dgrc.apiUrl ??
|
|
345
|
-
"https://api.westbayberry.com",
|
|
346
|
-
mode: modeRaw,
|
|
347
|
-
blockThreshold,
|
|
348
|
-
warnThreshold,
|
|
349
|
-
maxPackages,
|
|
350
|
-
allowlist: allowlistRaw
|
|
351
|
-
? allowlistRaw.split(",").map((s) => s.trim()).filter(Boolean)
|
|
352
|
-
: (dgrc.allowlist ?? []),
|
|
353
|
-
json: values.json,
|
|
354
|
-
scanAll: values["scan-all"],
|
|
355
|
-
baseLockfile: values["base-lockfile"] ?? null,
|
|
356
|
-
command,
|
|
357
|
-
debug,
|
|
358
|
-
};
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
/***/ }),
|
|
363
|
-
|
|
364
|
-
/***/ 746:
|
|
365
|
-
/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
369
|
-
exports.discoverChanges = discoverChanges;
|
|
370
|
-
const node_child_process_1 = __nccwpck_require__(421);
|
|
371
|
-
const node_fs_1 = __nccwpck_require__(24);
|
|
372
|
-
const node_path_1 = __nccwpck_require__(760);
|
|
373
|
-
const parse_package_lock_1 = __nccwpck_require__(88);
|
|
374
|
-
const diff_1 = __nccwpck_require__(229);
|
|
375
|
-
const parse_package_json_1 = __nccwpck_require__(417);
|
|
376
|
-
/**
|
|
377
|
-
* Discover changed (or all) packages by reading the local lockfile
|
|
378
|
-
* and comparing against a base.
|
|
379
|
-
*/
|
|
380
|
-
function discoverChanges(cwd, config) {
|
|
381
|
-
const lockfilePath = findLockfile(cwd);
|
|
382
|
-
if (!lockfilePath) {
|
|
383
|
-
throw new Error("No package-lock.json found. Run from your project root or use --base-lockfile.");
|
|
384
|
-
}
|
|
385
|
-
const headContent = (0, node_fs_1.readFileSync)(lockfilePath, "utf-8");
|
|
386
|
-
const headParsed = (0, parse_package_lock_1.parseLockfile)(headContent);
|
|
387
|
-
const directDeps = getDirectDeps(cwd);
|
|
388
|
-
// 1. --scan-all: treat everything as new
|
|
389
|
-
if (config.scanAll) {
|
|
390
|
-
const packages = [];
|
|
391
|
-
for (const [name, entry] of headParsed.packages) {
|
|
392
|
-
if (packages.length >= config.maxPackages)
|
|
393
|
-
break;
|
|
394
|
-
packages.push({
|
|
395
|
-
name,
|
|
396
|
-
version: entry.version,
|
|
397
|
-
previousVersion: null,
|
|
398
|
-
isNew: true,
|
|
399
|
-
});
|
|
400
|
-
}
|
|
401
|
-
return { packages, method: "scan-all", skipped: [] };
|
|
402
|
-
}
|
|
403
|
-
// 2. --base-lockfile: explicit diff
|
|
404
|
-
if (config.baseLockfile) {
|
|
405
|
-
if (!(0, node_fs_1.existsSync)(config.baseLockfile)) {
|
|
406
|
-
throw new Error(`Base lockfile not found: ${config.baseLockfile}`);
|
|
407
|
-
}
|
|
408
|
-
const baseContent = (0, node_fs_1.readFileSync)(config.baseLockfile, "utf-8");
|
|
409
|
-
const baseParsed = (0, parse_package_lock_1.parseLockfile)(baseContent);
|
|
410
|
-
const diff = (0, diff_1.diffLockfiles)(baseParsed, headParsed, config.maxPackages, directDeps);
|
|
411
|
-
return {
|
|
412
|
-
packages: diff.changes.map(toPackageInput),
|
|
413
|
-
method: "base-lockfile",
|
|
414
|
-
skipped: diff.skipped,
|
|
415
|
-
};
|
|
416
|
-
}
|
|
417
|
-
// 3. Git auto-diff
|
|
418
|
-
const baseContent = getGitBaseLockfile(cwd);
|
|
419
|
-
if (baseContent !== null) {
|
|
420
|
-
const baseParsed = (0, parse_package_lock_1.parseLockfile)(baseContent);
|
|
421
|
-
const diff = (0, diff_1.diffLockfiles)(baseParsed, headParsed, config.maxPackages, directDeps);
|
|
422
|
-
return {
|
|
423
|
-
packages: diff.changes.map(toPackageInput),
|
|
424
|
-
method: "git-diff",
|
|
425
|
-
skipped: diff.skipped,
|
|
426
|
-
};
|
|
427
|
-
}
|
|
428
|
-
// 4. Fallback: try package.json diff, resolve ranges via lockfile
|
|
429
|
-
const pkgJsonPath = (0, node_path_1.join)(cwd, "package.json");
|
|
430
|
-
if ((0, node_fs_1.existsSync)(pkgJsonPath)) {
|
|
431
|
-
const headPkgJson = (0, node_fs_1.readFileSync)(pkgJsonPath, "utf-8");
|
|
432
|
-
const basePkgJson = getGitBaseFile(cwd, "package.json");
|
|
433
|
-
if (basePkgJson !== null) {
|
|
434
|
-
const diff = (0, parse_package_json_1.diffPackageJsons)(basePkgJson, headPkgJson, config.maxPackages);
|
|
435
|
-
// Resolve semver ranges (e.g. "^3.0.1") to exact versions from the lockfile
|
|
436
|
-
const resolved = diff.changes.map((change) => {
|
|
437
|
-
const lockEntry = headParsed.packages.get(change.name);
|
|
438
|
-
return {
|
|
439
|
-
...change,
|
|
440
|
-
newVersion: lockEntry?.version ?? change.newVersion,
|
|
441
|
-
};
|
|
442
|
-
});
|
|
443
|
-
return {
|
|
444
|
-
packages: resolved.map(toPackageInput),
|
|
445
|
-
method: "fallback",
|
|
446
|
-
skipped: [],
|
|
447
|
-
};
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
// Ultimate fallback: scan everything
|
|
451
|
-
const packages = [];
|
|
452
|
-
for (const [name, entry] of headParsed.packages) {
|
|
453
|
-
if (packages.length >= config.maxPackages)
|
|
454
|
-
break;
|
|
455
|
-
packages.push({
|
|
456
|
-
name,
|
|
457
|
-
version: entry.version,
|
|
458
|
-
previousVersion: null,
|
|
459
|
-
isNew: true,
|
|
460
|
-
});
|
|
461
|
-
}
|
|
462
|
-
return { packages, method: "fallback", skipped: [] };
|
|
463
|
-
}
|
|
464
|
-
function findLockfile(cwd) {
|
|
465
|
-
const candidates = ["package-lock.json", "npm-shrinkwrap.json"];
|
|
466
|
-
for (const name of candidates) {
|
|
467
|
-
const p = (0, node_path_1.join)(cwd, name);
|
|
468
|
-
if ((0, node_fs_1.existsSync)(p))
|
|
469
|
-
return p;
|
|
470
|
-
}
|
|
471
|
-
return null;
|
|
472
|
-
}
|
|
473
|
-
function getDirectDeps(cwd) {
|
|
474
|
-
try {
|
|
475
|
-
const content = (0, node_fs_1.readFileSync)((0, node_path_1.join)(cwd, "package.json"), "utf-8");
|
|
476
|
-
const pkg = JSON.parse(content);
|
|
477
|
-
return new Set([
|
|
478
|
-
...Object.keys(pkg.dependencies ?? {}),
|
|
479
|
-
...Object.keys(pkg.devDependencies ?? {}),
|
|
480
|
-
]);
|
|
481
|
-
}
|
|
482
|
-
catch {
|
|
483
|
-
return new Set();
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
function getGitBaseLockfile(cwd) {
|
|
487
|
-
try {
|
|
488
|
-
const mergeBase = (0, node_child_process_1.execSync)("git merge-base HEAD main", {
|
|
489
|
-
cwd,
|
|
490
|
-
encoding: "utf-8",
|
|
491
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
492
|
-
}).trim();
|
|
493
|
-
if (!mergeBase)
|
|
494
|
-
return null;
|
|
495
|
-
return (0, node_child_process_1.execSync)(`git show ${mergeBase}:package-lock.json`, {
|
|
496
|
-
cwd,
|
|
497
|
-
encoding: "utf-8",
|
|
498
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
499
|
-
});
|
|
500
|
-
}
|
|
501
|
-
catch {
|
|
502
|
-
// Not a git repo, no main branch, or file doesn't exist at base
|
|
503
|
-
return null;
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
function getGitBaseFile(cwd, filename) {
|
|
507
|
-
try {
|
|
508
|
-
const mergeBase = (0, node_child_process_1.execSync)("git merge-base HEAD main", {
|
|
509
|
-
cwd,
|
|
510
|
-
encoding: "utf-8",
|
|
511
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
512
|
-
}).trim();
|
|
513
|
-
if (!mergeBase)
|
|
514
|
-
return null;
|
|
515
|
-
return (0, node_child_process_1.execSync)(`git show ${mergeBase}:${filename}`, {
|
|
516
|
-
cwd,
|
|
517
|
-
encoding: "utf-8",
|
|
518
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
519
|
-
});
|
|
520
|
-
}
|
|
521
|
-
catch {
|
|
522
|
-
return null;
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
function toPackageInput(change) {
|
|
526
|
-
return {
|
|
527
|
-
name: change.name,
|
|
528
|
-
version: change.newVersion,
|
|
529
|
-
previousVersion: change.oldVersion,
|
|
530
|
-
isNew: change.oldVersion === null,
|
|
531
|
-
};
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
/***/ }),
|
|
536
|
-
|
|
537
|
-
/***/ 124:
|
|
538
|
-
/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
542
|
-
exports.parseNpmArgs = parseNpmArgs;
|
|
543
|
-
exports.parsePackageSpec = parsePackageSpec;
|
|
544
|
-
exports.resolveVersion = resolveVersion;
|
|
545
|
-
exports.resolvePackages = resolvePackages;
|
|
546
|
-
exports.runNpm = runNpm;
|
|
547
|
-
exports.handleNpmCommand = handleNpmCommand;
|
|
548
|
-
exports.handleWrapCommand = handleWrapCommand;
|
|
549
|
-
const node_child_process_1 = __nccwpck_require__(421);
|
|
550
|
-
const color_1 = __nccwpck_require__(988);
|
|
551
|
-
const api_1 = __nccwpck_require__(879);
|
|
552
|
-
const output_1 = __nccwpck_require__(202);
|
|
553
|
-
/** npm commands that install packages and should trigger a scan */
|
|
554
|
-
const INSTALL_COMMANDS = new Set(["install", "i", "add", "update", "up"]);
|
|
555
|
-
/**
|
|
556
|
-
* Parse the argv after `dg npm ...` to extract the npm command and package specifiers.
|
|
557
|
-
*/
|
|
558
|
-
function parseNpmArgs(args) {
|
|
559
|
-
let dgForce = false;
|
|
560
|
-
const filtered = [];
|
|
561
|
-
// Extract dg-specific flags before passing to npm
|
|
562
|
-
for (const arg of args) {
|
|
563
|
-
if (arg === "--dg-force") {
|
|
564
|
-
dgForce = true;
|
|
565
|
-
}
|
|
566
|
-
else {
|
|
567
|
-
filtered.push(arg);
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
const command = filtered[0] ?? "";
|
|
571
|
-
const shouldScan = INSTALL_COMMANDS.has(command);
|
|
572
|
-
// Extract package specifiers: anything that's not a flag and not the command itself
|
|
573
|
-
const packages = [];
|
|
574
|
-
if (shouldScan) {
|
|
575
|
-
for (let i = 1; i < filtered.length; i++) {
|
|
576
|
-
const arg = filtered[i];
|
|
577
|
-
// Skip flags and their values
|
|
578
|
-
if (arg.startsWith("-")) {
|
|
579
|
-
// Flags that take a value: skip next arg too
|
|
580
|
-
if (flagTakesValue(arg)) {
|
|
581
|
-
i++;
|
|
582
|
-
}
|
|
583
|
-
continue;
|
|
584
|
-
}
|
|
585
|
-
packages.push(arg);
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
return {
|
|
589
|
-
command,
|
|
590
|
-
packages,
|
|
591
|
-
rawArgs: filtered,
|
|
592
|
-
dgForce,
|
|
593
|
-
shouldScan,
|
|
594
|
-
};
|
|
595
|
-
}
|
|
596
|
-
/** npm flags that consume the next argument as a value */
|
|
597
|
-
function flagTakesValue(flag) {
|
|
598
|
-
const valueFlagPrefixes = [
|
|
599
|
-
"--save-prefix",
|
|
600
|
-
"--tag",
|
|
601
|
-
"--registry",
|
|
602
|
-
"--cache",
|
|
603
|
-
"--prefix",
|
|
604
|
-
"--fund",
|
|
605
|
-
"--omit",
|
|
606
|
-
"--install-strategy",
|
|
607
|
-
"--workspace",
|
|
608
|
-
];
|
|
609
|
-
// Short flags that take values
|
|
610
|
-
if (flag === "-w")
|
|
611
|
-
return true;
|
|
612
|
-
for (const prefix of valueFlagPrefixes) {
|
|
613
|
-
if (flag === prefix)
|
|
614
|
-
return true;
|
|
615
|
-
}
|
|
616
|
-
// --flag=value style doesn't consume next arg
|
|
617
|
-
return false;
|
|
618
|
-
}
|
|
619
|
-
/**
|
|
620
|
-
* Parse a package specifier like "express", "@scope/pkg@^2.0.0", "pkg@latest"
|
|
621
|
-
* into { name, versionSpec }.
|
|
622
|
-
*/
|
|
623
|
-
function parsePackageSpec(spec) {
|
|
624
|
-
// Scoped: @scope/pkg@version
|
|
625
|
-
if (spec.startsWith("@")) {
|
|
626
|
-
const slashIdx = spec.indexOf("/");
|
|
627
|
-
if (slashIdx === -1) {
|
|
628
|
-
return { name: spec, versionSpec: null };
|
|
629
|
-
}
|
|
630
|
-
const afterSlash = spec.slice(slashIdx + 1);
|
|
631
|
-
const atIdx = afterSlash.indexOf("@");
|
|
632
|
-
if (atIdx === -1) {
|
|
633
|
-
return { name: spec, versionSpec: null };
|
|
634
|
-
}
|
|
635
|
-
return {
|
|
636
|
-
name: spec.slice(0, slashIdx + 1 + atIdx),
|
|
637
|
-
versionSpec: afterSlash.slice(atIdx + 1),
|
|
638
|
-
};
|
|
639
|
-
}
|
|
640
|
-
// Unscoped: pkg@version
|
|
641
|
-
const atIdx = spec.indexOf("@");
|
|
642
|
-
if (atIdx === -1 || atIdx === 0) {
|
|
643
|
-
return { name: spec, versionSpec: null };
|
|
644
|
-
}
|
|
645
|
-
return {
|
|
646
|
-
name: spec.slice(0, atIdx),
|
|
647
|
-
versionSpec: spec.slice(atIdx + 1),
|
|
648
|
-
};
|
|
649
|
-
}
|
|
650
|
-
/**
|
|
651
|
-
* Resolve what version npm would install for a given package specifier.
|
|
652
|
-
* Uses `npm view <spec> version` to get the resolved version.
|
|
653
|
-
*/
|
|
654
|
-
function resolveVersion(spec) {
|
|
655
|
-
try {
|
|
656
|
-
const version = (0, node_child_process_1.execSync)(`npm view "${spec}" version`, {
|
|
657
|
-
encoding: "utf-8",
|
|
658
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
659
|
-
timeout: 15000,
|
|
660
|
-
}).trim();
|
|
661
|
-
return version || null;
|
|
662
|
-
}
|
|
663
|
-
catch {
|
|
664
|
-
return null;
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
/**
|
|
668
|
-
* Build PackageInput[] from package specifiers by resolving versions.
|
|
669
|
-
*/
|
|
670
|
-
function resolvePackages(specs) {
|
|
671
|
-
const resolved = [];
|
|
672
|
-
const failed = [];
|
|
673
|
-
for (const spec of specs) {
|
|
674
|
-
const { name, versionSpec } = parsePackageSpec(spec);
|
|
675
|
-
const querySpec = versionSpec ? `${name}@${versionSpec}` : name;
|
|
676
|
-
const version = resolveVersion(querySpec);
|
|
677
|
-
if (version) {
|
|
678
|
-
resolved.push({
|
|
679
|
-
name,
|
|
680
|
-
version,
|
|
681
|
-
previousVersion: null,
|
|
682
|
-
isNew: true,
|
|
683
|
-
});
|
|
684
|
-
}
|
|
685
|
-
else {
|
|
686
|
-
failed.push(spec);
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
return { resolved, failed };
|
|
690
|
-
}
|
|
691
|
-
/**
|
|
692
|
-
* Run the actual npm command, inheriting stdio.
|
|
693
|
-
* Returns the npm exit code.
|
|
694
|
-
*/
|
|
695
|
-
function runNpm(args) {
|
|
696
|
-
return new Promise((resolve) => {
|
|
697
|
-
const child = (0, node_child_process_1.spawn)("npm", args, {
|
|
698
|
-
stdio: "inherit",
|
|
699
|
-
shell: false,
|
|
700
|
-
});
|
|
701
|
-
child.on("close", (code) => resolve(code ?? 1));
|
|
702
|
-
child.on("error", () => resolve(1));
|
|
703
|
-
});
|
|
704
|
-
}
|
|
705
|
-
/**
|
|
706
|
-
* Main npm wrapper entry point.
|
|
707
|
-
*/
|
|
708
|
-
async function handleNpmCommand(npmArgs, config) {
|
|
709
|
-
const parsed = parseNpmArgs(npmArgs);
|
|
710
|
-
// Non-install commands: pass through directly
|
|
711
|
-
if (!parsed.shouldScan) {
|
|
712
|
-
const code = await runNpm(parsed.rawArgs);
|
|
713
|
-
process.exit(code);
|
|
714
|
-
}
|
|
715
|
-
// No packages specified (e.g. bare `npm install` from package.json)
|
|
716
|
-
if (parsed.packages.length === 0) {
|
|
717
|
-
process.stderr.write((0, color_1.dim)(" Dependency Guardian: no new packages specified, passing through to npm.\n"));
|
|
718
|
-
const code = await runNpm(parsed.rawArgs);
|
|
719
|
-
process.exit(code);
|
|
720
|
-
}
|
|
721
|
-
// Resolve versions
|
|
722
|
-
process.stderr.write((0, color_1.dim)(` Resolving ${parsed.packages.length} package${parsed.packages.length !== 1 ? "s" : ""}...\n`));
|
|
723
|
-
const { resolved, failed } = resolvePackages(parsed.packages);
|
|
724
|
-
if (failed.length > 0) {
|
|
725
|
-
process.stderr.write((0, color_1.yellow)(` Warning: Could not resolve versions for: ${failed.join(", ")}\n`));
|
|
726
|
-
}
|
|
727
|
-
if (resolved.length === 0) {
|
|
728
|
-
process.stderr.write((0, color_1.dim)(" No packages to scan. Passing through to npm.\n"));
|
|
729
|
-
const code = await runNpm(parsed.rawArgs);
|
|
730
|
-
process.exit(code);
|
|
731
|
-
}
|
|
732
|
-
// Filter allowlist
|
|
733
|
-
const toScan = resolved.filter((p) => !config.allowlist.includes(p.name));
|
|
734
|
-
if (toScan.length === 0) {
|
|
735
|
-
process.stderr.write((0, color_1.dim)(" All packages are allowlisted. Passing through to npm.\n"));
|
|
736
|
-
const code = await runNpm(parsed.rawArgs);
|
|
737
|
-
process.exit(code);
|
|
738
|
-
}
|
|
739
|
-
// Scan
|
|
740
|
-
process.stderr.write((0, color_1.dim)(` Scanning ${toScan.length} package${toScan.length !== 1 ? "s" : ""}...\n`));
|
|
741
|
-
let result;
|
|
742
|
-
try {
|
|
743
|
-
const startMs = Date.now();
|
|
744
|
-
result = await (0, api_1.callAnalyzeAPI)(toScan, config);
|
|
745
|
-
const elapsed = Date.now() - startMs;
|
|
746
|
-
if (config.debug) {
|
|
747
|
-
process.stderr.write((0, color_1.dim)(` [debug] API responded in ${elapsed}ms, action=${result.action}, score=${result.score}\n`));
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
catch (error) {
|
|
751
|
-
// API unavailable — warn and proceed
|
|
752
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
753
|
-
process.stderr.write((0, color_1.yellow)(` Warning: Scan failed (${msg}). Proceeding with install.\n`));
|
|
754
|
-
const code = await runNpm(parsed.rawArgs);
|
|
755
|
-
process.exit(code);
|
|
756
|
-
return; // unreachable, but helps TypeScript
|
|
757
|
-
}
|
|
758
|
-
// Handle result
|
|
759
|
-
if (result.action === "pass") {
|
|
760
|
-
process.stderr.write((0, color_1.green)(` ${(0, color_1.bold)("\u2713")} ${toScan.length} package${toScan.length !== 1 ? "s" : ""} scanned \u2014 all clear\n\n`));
|
|
761
|
-
const code = await runNpm(parsed.rawArgs);
|
|
762
|
-
process.exit(code);
|
|
763
|
-
}
|
|
764
|
-
// Render findings
|
|
765
|
-
const output = (0, output_1.renderResult)(result, config);
|
|
766
|
-
process.stdout.write(output + "\n");
|
|
767
|
-
if (result.action === "warn") {
|
|
768
|
-
process.stderr.write((0, color_1.yellow)(" Warnings detected. Proceeding with install.\n\n"));
|
|
769
|
-
const code = await runNpm(parsed.rawArgs);
|
|
770
|
-
process.exit(code);
|
|
771
|
-
}
|
|
772
|
-
// Block
|
|
773
|
-
if (result.action === "block") {
|
|
774
|
-
if (parsed.dgForce) {
|
|
775
|
-
process.stderr.write((0, color_1.yellow)((0, color_1.bold)(" --dg-force: Bypassing block. Install at your own risk.\n\n")));
|
|
776
|
-
const code = await runNpm(parsed.rawArgs);
|
|
777
|
-
process.exit(code);
|
|
778
|
-
}
|
|
779
|
-
process.stderr.write((0, color_1.red)((0, color_1.bold)(" BLOCKED: ")) +
|
|
780
|
-
(0, color_1.red)("High-risk packages detected. Install aborted.\n"));
|
|
781
|
-
process.stderr.write((0, color_1.dim)(" Use --dg-force to bypass this check.\n\n"));
|
|
782
|
-
process.exit(2);
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
const WRAP_USAGE = `
|
|
786
|
-
Set up dg as your npm wrapper:
|
|
787
|
-
|
|
788
|
-
Option 1 — Shell alias (recommended):
|
|
789
|
-
Add to your ~/.zshrc or ~/.bashrc:
|
|
790
|
-
alias npm='dg npm'
|
|
791
|
-
|
|
792
|
-
Option 2 — Per-project .npmrc:
|
|
793
|
-
Not yet supported.
|
|
794
|
-
|
|
795
|
-
Once set up, every \`npm install\` will be scanned automatically.
|
|
796
|
-
`.trimStart();
|
|
797
|
-
function handleWrapCommand() {
|
|
798
|
-
process.stdout.write(WRAP_USAGE);
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
/***/ }),
|
|
803
|
-
|
|
804
|
-
/***/ 202:
|
|
805
|
-
/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
809
|
-
exports.renderResult = renderResult;
|
|
810
|
-
const color_1 = __nccwpck_require__(988);
|
|
811
|
-
const SEVERITY_LABELS = {
|
|
812
|
-
5: "CRITICAL",
|
|
813
|
-
4: "HIGH",
|
|
814
|
-
3: "MEDIUM",
|
|
815
|
-
2: "LOW",
|
|
816
|
-
1: "INFO",
|
|
817
|
-
};
|
|
818
|
-
function severityColor(sev) {
|
|
819
|
-
if (sev >= 5)
|
|
820
|
-
return (s) => (0, color_1.bold)((0, color_1.red)(s));
|
|
821
|
-
if (sev >= 4)
|
|
822
|
-
return color_1.red;
|
|
823
|
-
if (sev >= 3)
|
|
824
|
-
return color_1.yellow;
|
|
825
|
-
if (sev >= 2)
|
|
826
|
-
return color_1.cyan;
|
|
827
|
-
return color_1.dim;
|
|
828
|
-
}
|
|
829
|
-
function actionColor(action) {
|
|
830
|
-
if (action === "block")
|
|
831
|
-
return color_1.red;
|
|
832
|
-
if (action === "warn")
|
|
833
|
-
return color_1.yellow;
|
|
834
|
-
return color_1.green;
|
|
835
|
-
}
|
|
836
|
-
function pad(s, len) {
|
|
837
|
-
return s + " ".repeat(Math.max(0, len - s.length));
|
|
838
|
-
}
|
|
839
|
-
function rpad(s, len) {
|
|
840
|
-
return " ".repeat(Math.max(0, len - s.length)) + s;
|
|
841
|
-
}
|
|
842
|
-
function renderResult(result, config) {
|
|
843
|
-
if (config.json) {
|
|
844
|
-
return JSON.stringify(result, null, 2);
|
|
845
|
-
}
|
|
846
|
-
const lines = [];
|
|
847
|
-
const actionStr = result.action.toUpperCase();
|
|
848
|
-
const colorAction = actionColor(result.action);
|
|
849
|
-
// Header
|
|
850
|
-
lines.push("");
|
|
851
|
-
lines.push(` ${(0, color_1.bold)("Dependency Guardian")}`);
|
|
852
|
-
lines.push(` Score: ${(0, color_1.bold)(String(result.score))}${" ".repeat(Math.max(1, 50 - String(result.score).length))}${colorAction(actionStr)}`);
|
|
853
|
-
lines.push("");
|
|
854
|
-
// Summary
|
|
855
|
-
const flagged = result.packages.filter((p) => p.score > 0).length;
|
|
856
|
-
const total = result.packages.length;
|
|
857
|
-
lines.push(` ${total} package${total !== 1 ? "s" : ""} scanned, ${flagged} flagged`);
|
|
858
|
-
lines.push("");
|
|
859
|
-
if (total === 0) {
|
|
860
|
-
lines.push(" No packages to scan.");
|
|
861
|
-
lines.push("");
|
|
862
|
-
return lines.join("\n");
|
|
863
|
-
}
|
|
864
|
-
// Package table — adapt to terminal width
|
|
865
|
-
const termWidth = process.stdout.columns || 80;
|
|
866
|
-
const COL_NAME = Math.max(16, Math.min(40, Math.floor(termWidth * 0.3)));
|
|
867
|
-
const COL_VER = Math.max(14, Math.min(30, Math.floor(termWidth * 0.25)));
|
|
868
|
-
const COL_SCORE = 7;
|
|
869
|
-
lines.push(` ${(0, color_1.dim)(pad("Package", COL_NAME))}${(0, color_1.dim)(pad("Version", COL_VER))}${(0, color_1.dim)(pad("Score", COL_SCORE))}${(0, color_1.dim)("Action")}`);
|
|
870
|
-
lines.push(` ${(0, color_1.dim)(pad("\u2500".repeat(COL_NAME - 2), COL_NAME))}${(0, color_1.dim)(pad("\u2500".repeat(COL_VER - 2), COL_VER))}${(0, color_1.dim)(pad("\u2500".repeat(COL_SCORE - 2), COL_SCORE))}${(0, color_1.dim)("\u2500".repeat(6))}`);
|
|
871
|
-
// Sort: flagged first, then by score desc
|
|
872
|
-
const sorted = [...result.packages].sort((a, b) => b.score - a.score);
|
|
873
|
-
for (const pkg of sorted) {
|
|
874
|
-
const versionStr = pkg.version;
|
|
875
|
-
const safe = result.safeVersions[pkg.name];
|
|
876
|
-
const verDisplay = safe ? `${(0, color_1.dim)(safe + " \u2192 ")}${versionStr}` : versionStr;
|
|
877
|
-
const pkgAction = pkg.score >= config.blockThreshold
|
|
878
|
-
? "BLOCK"
|
|
879
|
-
: pkg.score >= config.warnThreshold
|
|
880
|
-
? "WARN"
|
|
881
|
-
: "pass";
|
|
882
|
-
const pkgColor = actionColor(pkg.score >= config.blockThreshold
|
|
883
|
-
? "block"
|
|
884
|
-
: pkg.score >= config.warnThreshold
|
|
885
|
-
? "warn"
|
|
886
|
-
: "pass");
|
|
887
|
-
lines.push(` ${pad(truncate(pkg.name, COL_NAME - 2), COL_NAME)}${pad(verDisplay, COL_VER)}${rpad(String(pkg.score), COL_SCORE - 2)} ${pkgColor(pkgAction)}`);
|
|
888
|
-
}
|
|
889
|
-
lines.push("");
|
|
890
|
-
// Detailed findings for flagged packages
|
|
891
|
-
const flaggedPkgs = sorted.filter((p) => p.score > 0);
|
|
892
|
-
for (const pkg of flaggedPkgs) {
|
|
893
|
-
lines.push(renderPackageDetail(pkg, result.safeVersions[pkg.name]));
|
|
894
|
-
}
|
|
895
|
-
// Duration
|
|
896
|
-
if (result.durationMs) {
|
|
897
|
-
lines.push(` ${(0, color_1.dim)(`Completed in ${(result.durationMs / 1000).toFixed(1)}s`)}`);
|
|
898
|
-
lines.push("");
|
|
899
|
-
}
|
|
900
|
-
return lines.join("\n");
|
|
901
|
-
}
|
|
902
|
-
function renderPackageDetail(pkg, safeVersion) {
|
|
903
|
-
const lines = [];
|
|
904
|
-
const header = `${pkg.name}@${pkg.version} (score: ${pkg.score})`;
|
|
905
|
-
const rule = "\u2500".repeat(Math.max(0, 50 - header.length));
|
|
906
|
-
lines.push(` ${(0, color_1.dim)("\u2500\u2500")} ${(0, color_1.bold)(header)} ${(0, color_1.dim)(rule)}`);
|
|
907
|
-
lines.push("");
|
|
908
|
-
for (const finding of pkg.findings) {
|
|
909
|
-
// Skip low-severity informational findings in detail view
|
|
910
|
-
if (finding.severity <= 1 && !finding.critical)
|
|
911
|
-
continue;
|
|
912
|
-
const sevLabel = SEVERITY_LABELS[finding.severity] ?? "INFO";
|
|
913
|
-
const colorFn = severityColor(finding.severity);
|
|
914
|
-
lines.push(` ${colorFn(pad(sevLabel, 10))}${finding.id} \u2014 ${finding.title} (sev ${finding.severity}, conf ${finding.confidence.toFixed(2)})`);
|
|
915
|
-
// Show up to 3 evidence lines
|
|
916
|
-
const evidenceLimit = 3;
|
|
917
|
-
for (let i = 0; i < Math.min(finding.evidence.length, evidenceLimit); i++) {
|
|
918
|
-
lines.push(` ${" ".repeat(10)}${(0, color_1.dim)(truncate(finding.evidence[i], 80))}`);
|
|
919
|
-
}
|
|
920
|
-
if (finding.evidence.length > evidenceLimit) {
|
|
921
|
-
lines.push(` ${" ".repeat(10)}${(0, color_1.dim)(`... and ${finding.evidence.length - evidenceLimit} more`)}`);
|
|
922
|
-
}
|
|
923
|
-
lines.push("");
|
|
924
|
-
}
|
|
925
|
-
if (safeVersion) {
|
|
926
|
-
lines.push(` ${(0, color_1.green)(`Safe version: ${pkg.name}@${safeVersion}`)}`);
|
|
927
|
-
lines.push("");
|
|
928
|
-
}
|
|
929
|
-
return lines.join("\n");
|
|
930
|
-
}
|
|
931
|
-
function truncate(s, max) {
|
|
932
|
-
if (s.length <= max)
|
|
933
|
-
return s;
|
|
934
|
-
return s.slice(0, max - 1) + "\u2026";
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
/***/ }),
|
|
939
|
-
|
|
940
|
-
/***/ 229:
|
|
941
|
-
/***/ ((__unused_webpack_module, exports) => {
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
945
|
-
exports.diffLockfiles = diffLockfiles;
|
|
946
|
-
function diffLockfiles(base, head, maxPackages, directDeps) {
|
|
947
|
-
const allChanges = [];
|
|
948
|
-
for (const [name, headEntry] of head.packages) {
|
|
949
|
-
const baseEntry = base?.packages.get(name);
|
|
950
|
-
if (!baseEntry) {
|
|
951
|
-
allChanges.push({
|
|
952
|
-
name,
|
|
953
|
-
oldVersion: null,
|
|
954
|
-
newVersion: headEntry.version,
|
|
955
|
-
isDirect: directDeps?.has(name) ?? false,
|
|
956
|
-
isDev: headEntry.dev ?? false,
|
|
957
|
-
});
|
|
958
|
-
}
|
|
959
|
-
else if (baseEntry.version !== headEntry.version) {
|
|
960
|
-
allChanges.push({
|
|
961
|
-
name,
|
|
962
|
-
oldVersion: baseEntry.version,
|
|
963
|
-
newVersion: headEntry.version,
|
|
964
|
-
isDirect: directDeps?.has(name) ?? false,
|
|
965
|
-
isDev: headEntry.dev ?? false,
|
|
966
|
-
});
|
|
967
|
-
}
|
|
968
|
-
}
|
|
969
|
-
allChanges.sort((a, b) => {
|
|
970
|
-
if (a.isDirect !== b.isDirect)
|
|
971
|
-
return a.isDirect ? -1 : 1;
|
|
972
|
-
return a.name.localeCompare(b.name);
|
|
973
|
-
});
|
|
974
|
-
if (allChanges.length <= maxPackages) {
|
|
975
|
-
return { changes: allChanges, skipped: [] };
|
|
976
|
-
}
|
|
977
|
-
const changes = allChanges.slice(0, maxPackages);
|
|
978
|
-
const skipped = allChanges.slice(maxPackages).map((c) => c.name);
|
|
979
|
-
return { changes, skipped };
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
/***/ }),
|
|
984
|
-
|
|
985
|
-
/***/ 417:
|
|
986
|
-
/***/ ((__unused_webpack_module, exports) => {
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
990
|
-
exports.diffPackageJsons = diffPackageJsons;
|
|
991
|
-
function diffPackageJsons(baseContent, headContent, maxPackages) {
|
|
992
|
-
const head = JSON.parse(headContent);
|
|
993
|
-
const headDeps = {
|
|
994
|
-
...head.dependencies,
|
|
995
|
-
...head.devDependencies,
|
|
996
|
-
};
|
|
997
|
-
let baseDeps = {};
|
|
998
|
-
if (baseContent) {
|
|
999
|
-
const base = JSON.parse(baseContent);
|
|
1000
|
-
baseDeps = { ...base.dependencies, ...base.devDependencies };
|
|
1001
|
-
}
|
|
1002
|
-
const changes = [];
|
|
1003
|
-
for (const [name, version] of Object.entries(headDeps)) {
|
|
1004
|
-
if (changes.length >= maxPackages)
|
|
1005
|
-
break;
|
|
1006
|
-
const baseVersion = baseDeps[name];
|
|
1007
|
-
const isDev = !!(head.devDependencies && head.devDependencies[name]);
|
|
1008
|
-
if (!baseVersion) {
|
|
1009
|
-
changes.push({
|
|
1010
|
-
name,
|
|
1011
|
-
oldVersion: null,
|
|
1012
|
-
newVersion: version,
|
|
1013
|
-
isDirect: true,
|
|
1014
|
-
isDev,
|
|
1015
|
-
});
|
|
1016
|
-
}
|
|
1017
|
-
else if (baseVersion !== version) {
|
|
1018
|
-
changes.push({
|
|
1019
|
-
name,
|
|
1020
|
-
oldVersion: baseVersion,
|
|
1021
|
-
newVersion: version,
|
|
1022
|
-
isDirect: true,
|
|
1023
|
-
isDev,
|
|
1024
|
-
});
|
|
1025
|
-
}
|
|
1026
|
-
}
|
|
1027
|
-
return { changes };
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
/***/ }),
|
|
1032
|
-
|
|
1033
|
-
/***/ 88:
|
|
1034
|
-
/***/ ((__unused_webpack_module, exports) => {
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
1038
|
-
exports.parseLockfile = parseLockfile;
|
|
1039
|
-
function parseLockfile(content) {
|
|
1040
|
-
const json = JSON.parse(content);
|
|
1041
|
-
const lockfileVersion = json.lockfileVersion ?? 1;
|
|
1042
|
-
const packages = new Map();
|
|
1043
|
-
if (lockfileVersion >= 2 && json.packages) {
|
|
1044
|
-
for (const [path, entry] of Object.entries(json.packages)) {
|
|
1045
|
-
if (path === "")
|
|
1046
|
-
continue;
|
|
1047
|
-
const name = extractPackageName(path);
|
|
1048
|
-
if (name && !packages.has(name)) {
|
|
1049
|
-
const e = entry;
|
|
1050
|
-
packages.set(name, {
|
|
1051
|
-
version: e.version ?? "",
|
|
1052
|
-
resolved: e.resolved,
|
|
1053
|
-
integrity: e.integrity,
|
|
1054
|
-
dev: e.dev,
|
|
1055
|
-
});
|
|
1056
|
-
}
|
|
1057
|
-
}
|
|
1058
|
-
}
|
|
1059
|
-
else if (json.dependencies) {
|
|
1060
|
-
parseLegacyDeps(json.dependencies, packages);
|
|
1061
|
-
}
|
|
1062
|
-
return { lockfileVersion, packages };
|
|
1063
|
-
}
|
|
1064
|
-
function extractPackageName(nodePath) {
|
|
1065
|
-
const prefix = "node_modules/";
|
|
1066
|
-
const lastIdx = nodePath.lastIndexOf(prefix);
|
|
1067
|
-
if (lastIdx === -1)
|
|
1068
|
-
return null;
|
|
1069
|
-
const name = nodePath.slice(lastIdx + prefix.length);
|
|
1070
|
-
return name || null;
|
|
1071
|
-
}
|
|
1072
|
-
function parseLegacyDeps(deps, packages, parentPrefix = "") {
|
|
1073
|
-
for (const [name, value] of Object.entries(deps)) {
|
|
1074
|
-
const fullName = parentPrefix ? `${parentPrefix}/${name}` : name;
|
|
1075
|
-
const entry = value;
|
|
1076
|
-
packages.set(fullName, {
|
|
1077
|
-
version: entry.version ?? "",
|
|
1078
|
-
resolved: entry.resolved,
|
|
1079
|
-
integrity: entry.integrity,
|
|
1080
|
-
dev: entry.dev,
|
|
1081
|
-
});
|
|
1082
|
-
if (entry.dependencies) {
|
|
1083
|
-
parseLegacyDeps(entry.dependencies, packages);
|
|
1084
|
-
}
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
/***/ }),
|
|
1090
|
-
|
|
1091
|
-
/***/ 421:
|
|
1092
|
-
/***/ ((module) => {
|
|
1093
|
-
|
|
1094
|
-
module.exports = require("node:child_process");
|
|
1095
|
-
|
|
1096
|
-
/***/ }),
|
|
1097
|
-
|
|
1098
|
-
/***/ 24:
|
|
1099
|
-
/***/ ((module) => {
|
|
1100
|
-
|
|
1101
|
-
module.exports = require("node:fs");
|
|
1102
|
-
|
|
1103
|
-
/***/ }),
|
|
1104
|
-
|
|
1105
|
-
/***/ 161:
|
|
1106
|
-
/***/ ((module) => {
|
|
1107
|
-
|
|
1108
|
-
module.exports = require("node:os");
|
|
1109
|
-
|
|
1110
|
-
/***/ }),
|
|
1111
|
-
|
|
1112
|
-
/***/ 760:
|
|
1113
|
-
/***/ ((module) => {
|
|
1114
|
-
|
|
1115
|
-
module.exports = require("node:path");
|
|
1116
|
-
|
|
1117
|
-
/***/ }),
|
|
1118
|
-
|
|
1119
|
-
/***/ 975:
|
|
1120
|
-
/***/ ((module) => {
|
|
1121
|
-
|
|
1122
|
-
module.exports = require("node:util");
|
|
1123
|
-
|
|
1124
|
-
/***/ })
|
|
1125
|
-
|
|
1126
|
-
/******/ });
|
|
1127
|
-
/************************************************************************/
|
|
1128
|
-
/******/ // The module cache
|
|
1129
|
-
/******/ var __webpack_module_cache__ = {};
|
|
1130
|
-
/******/
|
|
1131
|
-
/******/ // The require function
|
|
1132
|
-
/******/ function __nccwpck_require__(moduleId) {
|
|
1133
|
-
/******/ // Check if module is in cache
|
|
1134
|
-
/******/ var cachedModule = __webpack_module_cache__[moduleId];
|
|
1135
|
-
/******/ if (cachedModule !== undefined) {
|
|
1136
|
-
/******/ return cachedModule.exports;
|
|
1137
|
-
/******/ }
|
|
1138
|
-
/******/ // Create a new module (and put it into the cache)
|
|
1139
|
-
/******/ var module = __webpack_module_cache__[moduleId] = {
|
|
1140
|
-
/******/ // no module.id needed
|
|
1141
|
-
/******/ // no module.loaded needed
|
|
1142
|
-
/******/ exports: {}
|
|
1143
|
-
/******/ };
|
|
1144
|
-
/******/
|
|
1145
|
-
/******/ // Execute the module function
|
|
1146
|
-
/******/ var threw = true;
|
|
1147
|
-
/******/ try {
|
|
1148
|
-
/******/ __webpack_modules__[moduleId](module, module.exports, __nccwpck_require__);
|
|
1149
|
-
/******/ threw = false;
|
|
1150
|
-
/******/ } finally {
|
|
1151
|
-
/******/ if(threw) delete __webpack_module_cache__[moduleId];
|
|
1152
|
-
/******/ }
|
|
1153
|
-
/******/
|
|
1154
|
-
/******/ // Return the exports of the module
|
|
1155
|
-
/******/ return module.exports;
|
|
1156
|
-
/******/ }
|
|
1157
|
-
/******/
|
|
1158
|
-
/************************************************************************/
|
|
1159
|
-
/******/ /* webpack/runtime/compat */
|
|
1160
|
-
/******/
|
|
1161
|
-
/******/ if (typeof __nccwpck_require__ !== 'undefined') __nccwpck_require__.ab = __dirname + "/";
|
|
1162
|
-
/******/
|
|
1163
|
-
/************************************************************************/
|
|
1164
|
-
var __webpack_exports__ = {};
|
|
1165
|
-
// This entry need to be wrapped in an IIFE because it uses a non-standard name for the exports (exports).
|
|
1166
|
-
(() => {
|
|
1167
|
-
var exports = __webpack_exports__;
|
|
1168
|
-
|
|
1169
|
-
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
1170
|
-
const config_1 = __nccwpck_require__(973);
|
|
1171
|
-
const lockfile_1 = __nccwpck_require__(746);
|
|
1172
|
-
const api_1 = __nccwpck_require__(879);
|
|
1173
|
-
const output_1 = __nccwpck_require__(202);
|
|
1174
|
-
const color_1 = __nccwpck_require__(988);
|
|
1175
|
-
const npm_wrapper_1 = __nccwpck_require__(124);
|
|
1176
|
-
async function main() {
|
|
1177
|
-
// Check for `dg npm ...` or `dg wrap` before full config parsing
|
|
1178
|
-
const rawCommand = process.argv[2];
|
|
1179
|
-
if (rawCommand === "npm") {
|
|
1180
|
-
// Parse config but don't require lockfile discovery
|
|
1181
|
-
const config = (0, config_1.parseConfig)(process.argv);
|
|
1182
|
-
await (0, npm_wrapper_1.handleNpmCommand)(process.argv.slice(3), config);
|
|
1183
|
-
return;
|
|
1184
|
-
}
|
|
1185
|
-
if (rawCommand === "wrap") {
|
|
1186
|
-
(0, npm_wrapper_1.handleWrapCommand)();
|
|
1187
|
-
return;
|
|
1188
|
-
}
|
|
1189
|
-
const config = (0, config_1.parseConfig)(process.argv);
|
|
1190
|
-
const dbg = (msg) => {
|
|
1191
|
-
if (config.debug)
|
|
1192
|
-
process.stderr.write((0, color_1.dim)(` [debug] ${msg}\n`));
|
|
1193
|
-
};
|
|
1194
|
-
if (config.mode === "off") {
|
|
1195
|
-
process.stderr.write((0, color_1.dim)(" Dependency Guardian: mode is off — skipping.\n"));
|
|
1196
|
-
process.exit(0);
|
|
1197
|
-
}
|
|
1198
|
-
dbg(`mode=${config.mode} block=${config.blockThreshold} warn=${config.warnThreshold} max=${config.maxPackages}`);
|
|
1199
|
-
dbg(`api=${config.apiUrl}`);
|
|
1200
|
-
// Discover packages
|
|
1201
|
-
process.stderr.write((0, color_1.dim)(" Discovering package changes...\n"));
|
|
1202
|
-
const discovery = (0, lockfile_1.discoverChanges)(process.cwd(), config);
|
|
1203
|
-
dbg(`discovery method: ${discovery.method}`);
|
|
1204
|
-
if (discovery.packages.length === 0) {
|
|
1205
|
-
process.stderr.write((0, color_1.dim)(" No package changes detected.\n"));
|
|
1206
|
-
process.exit(0);
|
|
1207
|
-
}
|
|
1208
|
-
// Filter allowlist
|
|
1209
|
-
const packages = discovery.packages.filter((p) => !config.allowlist.includes(p.name));
|
|
1210
|
-
if (packages.length === 0) {
|
|
1211
|
-
process.stderr.write((0, color_1.dim)(" All changed packages are allowlisted.\n"));
|
|
1212
|
-
process.exit(0);
|
|
1213
|
-
}
|
|
1214
|
-
process.stderr.write((0, color_1.dim)(` Scanning ${packages.length} package${packages.length !== 1 ? "s" : ""} (${discovery.method})...\n`));
|
|
1215
|
-
if (discovery.skipped.length > 0) {
|
|
1216
|
-
process.stderr.write((0, color_1.yellow)(` Warning: ${discovery.skipped.length} package(s) skipped (max-packages=${config.maxPackages})\n`));
|
|
1217
|
-
}
|
|
1218
|
-
dbg(`packages to scan: ${packages.map(p => `${p.name}@${p.version}`).slice(0, 10).join(", ")}${packages.length > 10 ? ` (+${packages.length - 10} more)` : ""}`);
|
|
1219
|
-
// Call Detection API
|
|
1220
|
-
const startMs = Date.now();
|
|
1221
|
-
const result = await (0, api_1.callAnalyzeAPI)(packages, config, (done, total) => {
|
|
1222
|
-
process.stderr.write((0, color_1.dim)(` Analyzed ${done}/${total}...\n`));
|
|
1223
|
-
});
|
|
1224
|
-
dbg(`API responded in ${Date.now() - startMs}ms, action=${result.action}, score=${result.score}`);
|
|
1225
|
-
// Render output
|
|
1226
|
-
const output = (0, output_1.renderResult)(result, config);
|
|
1227
|
-
process.stdout.write(output + "\n");
|
|
1228
|
-
// Exit code
|
|
1229
|
-
if (result.action === "block" && config.mode === "block") {
|
|
1230
|
-
process.exit(2);
|
|
1231
|
-
}
|
|
1232
|
-
else if (result.action === "block" || result.action === "warn") {
|
|
1233
|
-
process.exit(1);
|
|
1234
|
-
}
|
|
1235
|
-
process.exit(0);
|
|
1236
|
-
}
|
|
1237
|
-
main().catch((err) => {
|
|
1238
|
-
process.stderr.write(`\n ${(0, color_1.bold)((0, color_1.red)("Error:"))} ${err.message}\n\n`);
|
|
1239
|
-
process.exit(3); // Exit 3 = internal error (distinct from exit 1 = warnings)
|
|
1240
|
-
});
|
|
1241
|
-
|
|
1242
|
-
})();
|
|
1243
|
-
|
|
1244
|
-
module.exports = __webpack_exports__;
|
|
1245
|
-
/******/ })()
|
|
1246
|
-
;
|