@westbayberry/dg 1.0.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.
Files changed (3) hide show
  1. package/README.md +90 -0
  2. package/dist/index.js +804 -0
  3. package/package.json +32 -0
package/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # @westbayberry/dg
2
+
3
+ Supply chain security scanner for npm dependencies. Detects malicious packages, typosquatting, dependency confusion, and 20+ attack patterns before they reach production.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g @westbayberry/dg
9
+ ```
10
+
11
+ Or run without installing:
12
+
13
+ ```bash
14
+ npx @westbayberry/dg scan
15
+ ```
16
+
17
+ ## Quick Start
18
+
19
+ 1. Get your API key at [westbayberry.com/dashboard](https://westbayberry.com/dashboard)
20
+ 2. Run a scan:
21
+
22
+ ```bash
23
+ export DG_API_KEY=dg_live_your_key_here
24
+ dg scan
25
+ ```
26
+
27
+ That's it. The CLI auto-detects changed packages by diffing your lockfile against the base branch.
28
+
29
+ ## Usage
30
+
31
+ ```
32
+ dg scan [options]
33
+ ```
34
+
35
+ ### Options
36
+
37
+ | Flag | Env Var | Default | Description |
38
+ |------|---------|---------|-------------|
39
+ | `--api-key <key>` | `DG_API_KEY` | *required* | Your API key |
40
+ | `--mode <mode>` | `DG_MODE` | `warn` | `block` / `warn` / `off` |
41
+ | `--block-threshold <n>` | | `70` | Score threshold for blocking |
42
+ | `--warn-threshold <n>` | | `60` | Score threshold for warnings |
43
+ | `--max-packages <n>` | | `200` | Max packages per scan |
44
+ | `--allowlist <pkgs>` | `DG_ALLOWLIST` | | Comma-separated packages to skip |
45
+ | `--json` | | | Output JSON for CI parsing |
46
+ | `--scan-all` | | | Scan all packages, not just changed |
47
+ | `--base-lockfile <path>` | | | Explicit base lockfile for diff |
48
+ | `--api-url <url>` | `DG_API_URL` | `https://api.westbayberry.com` | API endpoint |
49
+
50
+ ### Exit Codes
51
+
52
+ | Code | Meaning | CI Action |
53
+ |------|---------|-----------|
54
+ | `0` | Pass | Continue |
55
+ | `1` | Warning | Advisory — review recommended |
56
+ | `2` | Block | Fail the pipeline |
57
+
58
+ ## CI Examples
59
+
60
+ ### GitHub Actions
61
+
62
+ ```yaml
63
+ - name: Scan dependencies
64
+ run: npx @westbayberry/dg scan --mode block
65
+ env:
66
+ DG_API_KEY: ${{ secrets.DG_API_KEY }}
67
+ ```
68
+
69
+ ### GitLab CI
70
+
71
+ ```yaml
72
+ dependency-scan:
73
+ script:
74
+ - npx @westbayberry/dg scan --mode block
75
+ variables:
76
+ DG_API_KEY: $DG_API_KEY
77
+ ```
78
+
79
+ ### Any CI
80
+
81
+ ```bash
82
+ export DG_API_KEY="$DG_API_KEY"
83
+ npx @westbayberry/dg scan --mode block --json
84
+ ```
85
+
86
+ ## Links
87
+
88
+ - [Dashboard & API Keys](https://westbayberry.com/dashboard)
89
+ - [Documentation](https://westbayberry.com/docs)
90
+ - [Pricing](https://westbayberry.com/pricing)
package/dist/index.js ADDED
@@ -0,0 +1,804 @@
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
+ async function callAnalyzeAPI(packages, config) {
23
+ const url = `${config.apiUrl}/v1/analyze`;
24
+ const payload = {
25
+ packages: packages.map((p) => ({
26
+ name: p.name,
27
+ version: p.version,
28
+ previousVersion: p.previousVersion,
29
+ isNew: p.isNew,
30
+ })),
31
+ config: {
32
+ blockThreshold: config.blockThreshold,
33
+ warnThreshold: config.warnThreshold,
34
+ },
35
+ };
36
+ const controller = new AbortController();
37
+ const timeoutId = setTimeout(() => controller.abort(), 120000);
38
+ let response;
39
+ try {
40
+ response = await fetch(url, {
41
+ method: "POST",
42
+ headers: {
43
+ "Content-Type": "application/json",
44
+ Authorization: `Bearer ${config.apiKey}`,
45
+ "User-Agent": "dependency-guardian-cli/1.0.0",
46
+ },
47
+ body: JSON.stringify(payload),
48
+ signal: controller.signal,
49
+ });
50
+ }
51
+ catch (error) {
52
+ clearTimeout(timeoutId);
53
+ if (error instanceof Error && error.name === "AbortError") {
54
+ throw new APIError("Request timed out after 120s. Try scanning fewer packages.", 408, "");
55
+ }
56
+ throw error;
57
+ }
58
+ clearTimeout(timeoutId);
59
+ if (response.status === 401) {
60
+ throw new APIError("Invalid API key. Check your --api-key or DG_API_KEY value.\n" +
61
+ "Get your key at https://westbayberry.com/dashboard", 401, "");
62
+ }
63
+ if (response.status === 429) {
64
+ throw new APIError("Rate limit exceeded. Upgrade your plan at https://westbayberry.com/pricing", 429, "");
65
+ }
66
+ if (!response.ok) {
67
+ const body = await response.text();
68
+ throw new APIError(`API returned ${response.status}: ${body}`, response.status, body);
69
+ }
70
+ return (await response.json());
71
+ }
72
+
73
+
74
+ /***/ }),
75
+
76
+ /***/ 988:
77
+ /***/ ((__unused_webpack_module, exports) => {
78
+
79
+
80
+ /**
81
+ * Zero-dependency ANSI color helpers.
82
+ * Disabled when stdout is not a TTY or NO_COLOR is set.
83
+ */
84
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
85
+ exports.cyan = exports.yellow = exports.green = exports.red = exports.dim = exports.bold = void 0;
86
+ const enabled = process.stdout.isTTY === true && !process.env.NO_COLOR;
87
+ const wrap = (code, reset) => enabled ? (s) => `\x1b[${code}m${s}\x1b[${reset}m` : (s) => s;
88
+ exports.bold = wrap("1", "22");
89
+ exports.dim = wrap("2", "22");
90
+ exports.red = wrap("31", "39");
91
+ exports.green = wrap("32", "39");
92
+ exports.yellow = wrap("33", "39");
93
+ exports.cyan = wrap("36", "39");
94
+
95
+
96
+ /***/ }),
97
+
98
+ /***/ 973:
99
+ /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
100
+
101
+
102
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
103
+ exports.USAGE = void 0;
104
+ exports.parseConfig = parseConfig;
105
+ exports.getVersion = getVersion;
106
+ const node_util_1 = __nccwpck_require__(975);
107
+ const node_fs_1 = __nccwpck_require__(24);
108
+ const node_path_1 = __nccwpck_require__(760);
109
+ const USAGE = `
110
+ Dependency Guardian — Supply chain security scanner
111
+
112
+ Usage:
113
+ dependency-guardian scan [options]
114
+ dg scan [options]
115
+
116
+ Commands:
117
+ scan Scan dependencies for security risks (default)
118
+
119
+ Options:
120
+ --api-key <key> API key (or set DG_API_KEY env var)
121
+ --api-url <url> API base URL (default: https://api.westbayberry.com)
122
+ --mode <mode> block | warn | off (default: warn)
123
+ --block-threshold <n> Score threshold for blocking (default: 70)
124
+ --warn-threshold <n> Score threshold for warnings (default: 60)
125
+ --max-packages <n> Max packages per scan (default: 200)
126
+ --allowlist <pkgs> Comma-separated package names to skip
127
+ --json Output JSON for CI parsing
128
+ --scan-all Scan all packages, not just changed
129
+ --base-lockfile <path> Path to base lockfile for explicit diff
130
+ --help Show this help message
131
+ --version Show version number
132
+
133
+ Environment Variables:
134
+ DG_API_KEY API key
135
+ DG_API_URL API base URL
136
+ DG_MODE Mode (block/warn/off)
137
+ DG_ALLOWLIST Comma-separated allowlist
138
+
139
+ Exit Codes:
140
+ 0 pass — No risks detected
141
+ 1 warn — Risks detected (advisory)
142
+ 2 block — High-risk packages detected
143
+
144
+ Examples:
145
+ DG_API_KEY=dg_live_xxx dg scan
146
+ dg scan --api-key dg_live_xxx --json
147
+ dg scan --scan-all --mode block
148
+ dg scan --base-lockfile ./main-lockfile.json
149
+ `.trimStart();
150
+ exports.USAGE = USAGE;
151
+ function getVersion() {
152
+ try {
153
+ const pkg = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(__dirname, "..", "package.json"), "utf-8"));
154
+ return pkg.version ?? "1.0.0";
155
+ }
156
+ catch {
157
+ return "1.0.0";
158
+ }
159
+ }
160
+ function parseConfig(argv) {
161
+ const { values, positionals } = (0, node_util_1.parseArgs)({
162
+ args: argv.slice(2),
163
+ options: {
164
+ "api-key": { type: "string" },
165
+ "api-url": { type: "string" },
166
+ mode: { type: "string" },
167
+ "block-threshold": { type: "string" },
168
+ "warn-threshold": { type: "string" },
169
+ "max-packages": { type: "string" },
170
+ allowlist: { type: "string" },
171
+ json: { type: "boolean", default: false },
172
+ "scan-all": { type: "boolean", default: false },
173
+ "base-lockfile": { type: "string" },
174
+ help: { type: "boolean", default: false },
175
+ version: { type: "boolean", default: false },
176
+ },
177
+ allowPositionals: true,
178
+ strict: false,
179
+ });
180
+ if (values.help) {
181
+ process.stdout.write(USAGE);
182
+ process.exit(0);
183
+ }
184
+ if (values.version) {
185
+ process.stdout.write(`dependency-guardian v${getVersion()}\n`);
186
+ process.exit(0);
187
+ }
188
+ const command = positionals[0] ?? "scan";
189
+ const apiKey = values["api-key"] ??
190
+ process.env.DG_API_KEY ??
191
+ "";
192
+ if (!apiKey) {
193
+ process.stderr.write("Error: API key required. Set --api-key or DG_API_KEY environment variable.\n" +
194
+ "Get your key at https://westbayberry.com/dashboard\n");
195
+ process.exit(1);
196
+ }
197
+ const modeRaw = values.mode ??
198
+ process.env.DG_MODE ??
199
+ "warn";
200
+ if (!["block", "warn", "off"].includes(modeRaw)) {
201
+ process.stderr.write(`Error: Invalid mode "${modeRaw}". Must be block, warn, or off.\n`);
202
+ process.exit(1);
203
+ }
204
+ const allowlistRaw = values.allowlist ??
205
+ process.env.DG_ALLOWLIST ??
206
+ "";
207
+ return {
208
+ apiKey,
209
+ apiUrl: values["api-url"] ??
210
+ process.env.DG_API_URL ??
211
+ "https://api.westbayberry.com",
212
+ mode: modeRaw,
213
+ blockThreshold: Number(values["block-threshold"] ?? "70"),
214
+ warnThreshold: Number(values["warn-threshold"] ?? "60"),
215
+ maxPackages: Number(values["max-packages"] ?? "200"),
216
+ allowlist: allowlistRaw
217
+ .split(",")
218
+ .map((s) => s.trim())
219
+ .filter(Boolean),
220
+ json: values.json,
221
+ scanAll: values["scan-all"],
222
+ baseLockfile: values["base-lockfile"] ?? null,
223
+ command,
224
+ };
225
+ }
226
+
227
+
228
+ /***/ }),
229
+
230
+ /***/ 746:
231
+ /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
232
+
233
+
234
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
235
+ exports.discoverChanges = discoverChanges;
236
+ const node_child_process_1 = __nccwpck_require__(421);
237
+ const node_fs_1 = __nccwpck_require__(24);
238
+ const node_path_1 = __nccwpck_require__(760);
239
+ const parse_package_lock_1 = __nccwpck_require__(88);
240
+ const diff_1 = __nccwpck_require__(229);
241
+ const parse_package_json_1 = __nccwpck_require__(417);
242
+ /**
243
+ * Discover changed (or all) packages by reading the local lockfile
244
+ * and comparing against a base.
245
+ */
246
+ function discoverChanges(cwd, config) {
247
+ const lockfilePath = findLockfile(cwd);
248
+ if (!lockfilePath) {
249
+ throw new Error("No package-lock.json found. Run from your project root or use --base-lockfile.");
250
+ }
251
+ const headContent = (0, node_fs_1.readFileSync)(lockfilePath, "utf-8");
252
+ const headParsed = (0, parse_package_lock_1.parseLockfile)(headContent);
253
+ const directDeps = getDirectDeps(cwd);
254
+ // 1. --scan-all: treat everything as new
255
+ if (config.scanAll) {
256
+ const packages = [];
257
+ for (const [name, entry] of headParsed.packages) {
258
+ if (packages.length >= config.maxPackages)
259
+ break;
260
+ packages.push({
261
+ name,
262
+ version: entry.version,
263
+ previousVersion: null,
264
+ isNew: true,
265
+ });
266
+ }
267
+ return { packages, method: "scan-all", skipped: [] };
268
+ }
269
+ // 2. --base-lockfile: explicit diff
270
+ if (config.baseLockfile) {
271
+ if (!(0, node_fs_1.existsSync)(config.baseLockfile)) {
272
+ throw new Error(`Base lockfile not found: ${config.baseLockfile}`);
273
+ }
274
+ const baseContent = (0, node_fs_1.readFileSync)(config.baseLockfile, "utf-8");
275
+ const baseParsed = (0, parse_package_lock_1.parseLockfile)(baseContent);
276
+ const diff = (0, diff_1.diffLockfiles)(baseParsed, headParsed, config.maxPackages, directDeps);
277
+ return {
278
+ packages: diff.changes.map(toPackageInput),
279
+ method: "base-lockfile",
280
+ skipped: diff.skipped,
281
+ };
282
+ }
283
+ // 3. Git auto-diff
284
+ const baseContent = getGitBaseLockfile(cwd);
285
+ if (baseContent !== null) {
286
+ const baseParsed = (0, parse_package_lock_1.parseLockfile)(baseContent);
287
+ const diff = (0, diff_1.diffLockfiles)(baseParsed, headParsed, config.maxPackages, directDeps);
288
+ return {
289
+ packages: diff.changes.map(toPackageInput),
290
+ method: "git-diff",
291
+ skipped: diff.skipped,
292
+ };
293
+ }
294
+ // 4. Fallback: try package.json diff, else scan all
295
+ const pkgJsonPath = (0, node_path_1.join)(cwd, "package.json");
296
+ if ((0, node_fs_1.existsSync)(pkgJsonPath)) {
297
+ const headPkgJson = (0, node_fs_1.readFileSync)(pkgJsonPath, "utf-8");
298
+ const basePkgJson = getGitBaseFile(cwd, "package.json");
299
+ if (basePkgJson !== null) {
300
+ const diff = (0, parse_package_json_1.diffPackageJsons)(basePkgJson, headPkgJson, config.maxPackages);
301
+ return {
302
+ packages: diff.changes.map(toPackageInput),
303
+ method: "fallback",
304
+ skipped: [],
305
+ };
306
+ }
307
+ }
308
+ // Ultimate fallback: scan everything
309
+ const packages = [];
310
+ for (const [name, entry] of headParsed.packages) {
311
+ if (packages.length >= config.maxPackages)
312
+ break;
313
+ packages.push({
314
+ name,
315
+ version: entry.version,
316
+ previousVersion: null,
317
+ isNew: true,
318
+ });
319
+ }
320
+ return { packages, method: "fallback", skipped: [] };
321
+ }
322
+ function findLockfile(cwd) {
323
+ const candidates = ["package-lock.json", "npm-shrinkwrap.json"];
324
+ for (const name of candidates) {
325
+ const p = (0, node_path_1.join)(cwd, name);
326
+ if ((0, node_fs_1.existsSync)(p))
327
+ return p;
328
+ }
329
+ return null;
330
+ }
331
+ function getDirectDeps(cwd) {
332
+ try {
333
+ const content = (0, node_fs_1.readFileSync)((0, node_path_1.join)(cwd, "package.json"), "utf-8");
334
+ const pkg = JSON.parse(content);
335
+ return new Set([
336
+ ...Object.keys(pkg.dependencies ?? {}),
337
+ ...Object.keys(pkg.devDependencies ?? {}),
338
+ ]);
339
+ }
340
+ catch {
341
+ return new Set();
342
+ }
343
+ }
344
+ function getGitBaseLockfile(cwd) {
345
+ try {
346
+ const mergeBase = (0, node_child_process_1.execSync)("git merge-base HEAD main", {
347
+ cwd,
348
+ encoding: "utf-8",
349
+ stdio: ["pipe", "pipe", "pipe"],
350
+ }).trim();
351
+ if (!mergeBase)
352
+ return null;
353
+ return (0, node_child_process_1.execSync)(`git show ${mergeBase}:package-lock.json`, {
354
+ cwd,
355
+ encoding: "utf-8",
356
+ stdio: ["pipe", "pipe", "pipe"],
357
+ });
358
+ }
359
+ catch {
360
+ // Not a git repo, no main branch, or file doesn't exist at base
361
+ return null;
362
+ }
363
+ }
364
+ function getGitBaseFile(cwd, filename) {
365
+ try {
366
+ const mergeBase = (0, node_child_process_1.execSync)("git merge-base HEAD main", {
367
+ cwd,
368
+ encoding: "utf-8",
369
+ stdio: ["pipe", "pipe", "pipe"],
370
+ }).trim();
371
+ if (!mergeBase)
372
+ return null;
373
+ return (0, node_child_process_1.execSync)(`git show ${mergeBase}:${filename}`, {
374
+ cwd,
375
+ encoding: "utf-8",
376
+ stdio: ["pipe", "pipe", "pipe"],
377
+ });
378
+ }
379
+ catch {
380
+ return null;
381
+ }
382
+ }
383
+ function toPackageInput(change) {
384
+ return {
385
+ name: change.name,
386
+ version: change.newVersion,
387
+ previousVersion: change.oldVersion,
388
+ isNew: change.oldVersion === null,
389
+ };
390
+ }
391
+
392
+
393
+ /***/ }),
394
+
395
+ /***/ 202:
396
+ /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
397
+
398
+
399
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
400
+ exports.renderResult = renderResult;
401
+ const color_1 = __nccwpck_require__(988);
402
+ const SEVERITY_LABELS = {
403
+ 5: "CRITICAL",
404
+ 4: "HIGH",
405
+ 3: "MEDIUM",
406
+ 2: "LOW",
407
+ 1: "INFO",
408
+ };
409
+ function severityColor(sev) {
410
+ if (sev >= 5)
411
+ return (s) => (0, color_1.bold)((0, color_1.red)(s));
412
+ if (sev >= 4)
413
+ return color_1.red;
414
+ if (sev >= 3)
415
+ return color_1.yellow;
416
+ if (sev >= 2)
417
+ return color_1.cyan;
418
+ return color_1.dim;
419
+ }
420
+ function actionColor(action) {
421
+ if (action === "block")
422
+ return color_1.red;
423
+ if (action === "warn")
424
+ return color_1.yellow;
425
+ return color_1.green;
426
+ }
427
+ function pad(s, len) {
428
+ return s + " ".repeat(Math.max(0, len - s.length));
429
+ }
430
+ function rpad(s, len) {
431
+ return " ".repeat(Math.max(0, len - s.length)) + s;
432
+ }
433
+ function renderResult(result, config) {
434
+ if (config.json) {
435
+ return JSON.stringify(result, null, 2);
436
+ }
437
+ const lines = [];
438
+ const actionStr = result.action.toUpperCase();
439
+ const colorAction = actionColor(result.action);
440
+ // Header
441
+ lines.push("");
442
+ lines.push(` ${(0, color_1.bold)("Dependency Guardian")}`);
443
+ lines.push(` Score: ${(0, color_1.bold)(String(result.score))}${" ".repeat(Math.max(1, 50 - String(result.score).length))}${colorAction(actionStr)}`);
444
+ lines.push("");
445
+ // Summary
446
+ const flagged = result.packages.filter((p) => p.score > 0).length;
447
+ const total = result.packages.length;
448
+ lines.push(` ${total} package${total !== 1 ? "s" : ""} scanned, ${flagged} flagged`);
449
+ lines.push("");
450
+ if (total === 0) {
451
+ lines.push(" No packages to scan.");
452
+ lines.push("");
453
+ return lines.join("\n");
454
+ }
455
+ // Package table
456
+ const COL_NAME = 22;
457
+ const COL_VER = 22;
458
+ const COL_SCORE = 7;
459
+ 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")}`);
460
+ 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))}`);
461
+ // Sort: flagged first, then by score desc
462
+ const sorted = [...result.packages].sort((a, b) => b.score - a.score);
463
+ for (const pkg of sorted) {
464
+ const versionStr = pkg.version;
465
+ const safe = result.safeVersions[pkg.name];
466
+ const verDisplay = safe ? `${(0, color_1.dim)(safe + " \u2192 ")}${versionStr}` : versionStr;
467
+ const pkgAction = pkg.score >= config.blockThreshold
468
+ ? "BLOCK"
469
+ : pkg.score >= config.warnThreshold
470
+ ? "WARN"
471
+ : "pass";
472
+ const pkgColor = actionColor(pkg.score >= config.blockThreshold
473
+ ? "block"
474
+ : pkg.score >= config.warnThreshold
475
+ ? "warn"
476
+ : "pass");
477
+ lines.push(` ${pad(truncate(pkg.name, COL_NAME - 2), COL_NAME)}${pad(verDisplay, COL_VER)}${rpad(String(pkg.score), COL_SCORE - 2)} ${pkgColor(pkgAction)}`);
478
+ }
479
+ lines.push("");
480
+ // Detailed findings for flagged packages
481
+ const flaggedPkgs = sorted.filter((p) => p.score > 0);
482
+ for (const pkg of flaggedPkgs) {
483
+ lines.push(renderPackageDetail(pkg, result.safeVersions[pkg.name]));
484
+ }
485
+ // Duration
486
+ if (result.durationMs) {
487
+ lines.push(` ${(0, color_1.dim)(`Completed in ${(result.durationMs / 1000).toFixed(1)}s`)}`);
488
+ lines.push("");
489
+ }
490
+ return lines.join("\n");
491
+ }
492
+ function renderPackageDetail(pkg, safeVersion) {
493
+ const lines = [];
494
+ const header = `${pkg.name}@${pkg.version} (score: ${pkg.score})`;
495
+ const rule = "\u2500".repeat(Math.max(0, 50 - header.length));
496
+ lines.push(` ${(0, color_1.dim)("\u2500\u2500")} ${(0, color_1.bold)(header)} ${(0, color_1.dim)(rule)}`);
497
+ lines.push("");
498
+ for (const finding of pkg.findings) {
499
+ // Skip low-severity informational findings in detail view
500
+ if (finding.severity <= 1 && !finding.critical)
501
+ continue;
502
+ const sevLabel = SEVERITY_LABELS[finding.severity] ?? "INFO";
503
+ const colorFn = severityColor(finding.severity);
504
+ lines.push(` ${colorFn(pad(sevLabel, 10))}${finding.id} \u2014 ${finding.title} (sev ${finding.severity}, conf ${finding.confidence.toFixed(2)})`);
505
+ // Show up to 3 evidence lines
506
+ const evidenceLimit = 3;
507
+ for (let i = 0; i < Math.min(finding.evidence.length, evidenceLimit); i++) {
508
+ lines.push(` ${" ".repeat(10)}${(0, color_1.dim)(truncate(finding.evidence[i], 80))}`);
509
+ }
510
+ if (finding.evidence.length > evidenceLimit) {
511
+ lines.push(` ${" ".repeat(10)}${(0, color_1.dim)(`... and ${finding.evidence.length - evidenceLimit} more`)}`);
512
+ }
513
+ lines.push("");
514
+ }
515
+ if (safeVersion) {
516
+ lines.push(` ${(0, color_1.green)(`Safe version: ${pkg.name}@${safeVersion}`)}`);
517
+ lines.push("");
518
+ }
519
+ return lines.join("\n");
520
+ }
521
+ function truncate(s, max) {
522
+ if (s.length <= max)
523
+ return s;
524
+ return s.slice(0, max - 1) + "\u2026";
525
+ }
526
+
527
+
528
+ /***/ }),
529
+
530
+ /***/ 229:
531
+ /***/ ((__unused_webpack_module, exports) => {
532
+
533
+
534
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
535
+ exports.diffLockfiles = diffLockfiles;
536
+ function diffLockfiles(base, head, maxPackages, directDeps) {
537
+ const allChanges = [];
538
+ for (const [name, headEntry] of head.packages) {
539
+ const baseEntry = base?.packages.get(name);
540
+ if (!baseEntry) {
541
+ allChanges.push({
542
+ name,
543
+ oldVersion: null,
544
+ newVersion: headEntry.version,
545
+ isDirect: directDeps?.has(name) ?? false,
546
+ isDev: headEntry.dev ?? false,
547
+ });
548
+ }
549
+ else if (baseEntry.version !== headEntry.version) {
550
+ allChanges.push({
551
+ name,
552
+ oldVersion: baseEntry.version,
553
+ newVersion: headEntry.version,
554
+ isDirect: directDeps?.has(name) ?? false,
555
+ isDev: headEntry.dev ?? false,
556
+ });
557
+ }
558
+ }
559
+ allChanges.sort((a, b) => {
560
+ if (a.isDirect !== b.isDirect)
561
+ return a.isDirect ? -1 : 1;
562
+ return a.name.localeCompare(b.name);
563
+ });
564
+ if (allChanges.length <= maxPackages) {
565
+ return { changes: allChanges, skipped: [] };
566
+ }
567
+ const changes = allChanges.slice(0, maxPackages);
568
+ const skipped = allChanges.slice(maxPackages).map((c) => c.name);
569
+ return { changes, skipped };
570
+ }
571
+
572
+
573
+ /***/ }),
574
+
575
+ /***/ 417:
576
+ /***/ ((__unused_webpack_module, exports) => {
577
+
578
+
579
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
580
+ exports.diffPackageJsons = diffPackageJsons;
581
+ function diffPackageJsons(baseContent, headContent, maxPackages) {
582
+ const head = JSON.parse(headContent);
583
+ const headDeps = {
584
+ ...head.dependencies,
585
+ ...head.devDependencies,
586
+ };
587
+ let baseDeps = {};
588
+ if (baseContent) {
589
+ const base = JSON.parse(baseContent);
590
+ baseDeps = { ...base.dependencies, ...base.devDependencies };
591
+ }
592
+ const changes = [];
593
+ for (const [name, version] of Object.entries(headDeps)) {
594
+ if (changes.length >= maxPackages)
595
+ break;
596
+ const baseVersion = baseDeps[name];
597
+ const isDev = !!(head.devDependencies && head.devDependencies[name]);
598
+ if (!baseVersion) {
599
+ changes.push({
600
+ name,
601
+ oldVersion: null,
602
+ newVersion: version,
603
+ isDirect: true,
604
+ isDev,
605
+ });
606
+ }
607
+ else if (baseVersion !== version) {
608
+ changes.push({
609
+ name,
610
+ oldVersion: baseVersion,
611
+ newVersion: version,
612
+ isDirect: true,
613
+ isDev,
614
+ });
615
+ }
616
+ }
617
+ return { changes };
618
+ }
619
+
620
+
621
+ /***/ }),
622
+
623
+ /***/ 88:
624
+ /***/ ((__unused_webpack_module, exports) => {
625
+
626
+
627
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
628
+ exports.parseLockfile = parseLockfile;
629
+ function parseLockfile(content) {
630
+ const json = JSON.parse(content);
631
+ const lockfileVersion = json.lockfileVersion ?? 1;
632
+ const packages = new Map();
633
+ if (lockfileVersion >= 2 && json.packages) {
634
+ for (const [path, entry] of Object.entries(json.packages)) {
635
+ if (path === "")
636
+ continue;
637
+ const name = extractPackageName(path);
638
+ if (name) {
639
+ const e = entry;
640
+ packages.set(name, {
641
+ version: e.version ?? "",
642
+ resolved: e.resolved,
643
+ integrity: e.integrity,
644
+ dev: e.dev,
645
+ });
646
+ }
647
+ }
648
+ }
649
+ else if (json.dependencies) {
650
+ parseLegacyDeps(json.dependencies, packages);
651
+ }
652
+ return { lockfileVersion, packages };
653
+ }
654
+ function extractPackageName(nodePath) {
655
+ const prefix = "node_modules/";
656
+ const lastIdx = nodePath.lastIndexOf(prefix);
657
+ if (lastIdx === -1)
658
+ return null;
659
+ const name = nodePath.slice(lastIdx + prefix.length);
660
+ return name || null;
661
+ }
662
+ function parseLegacyDeps(deps, packages, parentPrefix = "") {
663
+ for (const [name, value] of Object.entries(deps)) {
664
+ const fullName = parentPrefix ? `${parentPrefix}/${name}` : name;
665
+ const entry = value;
666
+ packages.set(fullName, {
667
+ version: entry.version ?? "",
668
+ resolved: entry.resolved,
669
+ integrity: entry.integrity,
670
+ dev: entry.dev,
671
+ });
672
+ if (entry.dependencies) {
673
+ parseLegacyDeps(entry.dependencies, packages);
674
+ }
675
+ }
676
+ }
677
+
678
+
679
+ /***/ }),
680
+
681
+ /***/ 421:
682
+ /***/ ((module) => {
683
+
684
+ module.exports = require("node:child_process");
685
+
686
+ /***/ }),
687
+
688
+ /***/ 24:
689
+ /***/ ((module) => {
690
+
691
+ module.exports = require("node:fs");
692
+
693
+ /***/ }),
694
+
695
+ /***/ 760:
696
+ /***/ ((module) => {
697
+
698
+ module.exports = require("node:path");
699
+
700
+ /***/ }),
701
+
702
+ /***/ 975:
703
+ /***/ ((module) => {
704
+
705
+ module.exports = require("node:util");
706
+
707
+ /***/ })
708
+
709
+ /******/ });
710
+ /************************************************************************/
711
+ /******/ // The module cache
712
+ /******/ var __webpack_module_cache__ = {};
713
+ /******/
714
+ /******/ // The require function
715
+ /******/ function __nccwpck_require__(moduleId) {
716
+ /******/ // Check if module is in cache
717
+ /******/ var cachedModule = __webpack_module_cache__[moduleId];
718
+ /******/ if (cachedModule !== undefined) {
719
+ /******/ return cachedModule.exports;
720
+ /******/ }
721
+ /******/ // Create a new module (and put it into the cache)
722
+ /******/ var module = __webpack_module_cache__[moduleId] = {
723
+ /******/ // no module.id needed
724
+ /******/ // no module.loaded needed
725
+ /******/ exports: {}
726
+ /******/ };
727
+ /******/
728
+ /******/ // Execute the module function
729
+ /******/ var threw = true;
730
+ /******/ try {
731
+ /******/ __webpack_modules__[moduleId](module, module.exports, __nccwpck_require__);
732
+ /******/ threw = false;
733
+ /******/ } finally {
734
+ /******/ if(threw) delete __webpack_module_cache__[moduleId];
735
+ /******/ }
736
+ /******/
737
+ /******/ // Return the exports of the module
738
+ /******/ return module.exports;
739
+ /******/ }
740
+ /******/
741
+ /************************************************************************/
742
+ /******/ /* webpack/runtime/compat */
743
+ /******/
744
+ /******/ if (typeof __nccwpck_require__ !== 'undefined') __nccwpck_require__.ab = __dirname + "/";
745
+ /******/
746
+ /************************************************************************/
747
+ var __webpack_exports__ = {};
748
+ // This entry need to be wrapped in an IIFE because it uses a non-standard name for the exports (exports).
749
+ (() => {
750
+ var exports = __webpack_exports__;
751
+
752
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
753
+ const config_1 = __nccwpck_require__(973);
754
+ const lockfile_1 = __nccwpck_require__(746);
755
+ const api_1 = __nccwpck_require__(879);
756
+ const output_1 = __nccwpck_require__(202);
757
+ const color_1 = __nccwpck_require__(988);
758
+ async function main() {
759
+ const config = (0, config_1.parseConfig)(process.argv);
760
+ if (config.mode === "off") {
761
+ process.stderr.write((0, color_1.dim)(" Dependency Guardian: mode is off — skipping.\n"));
762
+ process.exit(0);
763
+ }
764
+ // Discover packages
765
+ process.stderr.write((0, color_1.dim)(" Discovering package changes...\n"));
766
+ const discovery = (0, lockfile_1.discoverChanges)(process.cwd(), config);
767
+ if (discovery.packages.length === 0) {
768
+ process.stderr.write((0, color_1.dim)(" No package changes detected.\n"));
769
+ process.exit(0);
770
+ }
771
+ // Filter allowlist
772
+ const packages = discovery.packages.filter((p) => !config.allowlist.includes(p.name));
773
+ if (packages.length === 0) {
774
+ process.stderr.write((0, color_1.dim)(" All changed packages are allowlisted.\n"));
775
+ process.exit(0);
776
+ }
777
+ process.stderr.write((0, color_1.dim)(` Scanning ${packages.length} package${packages.length !== 1 ? "s" : ""} (${discovery.method})...\n`));
778
+ if (discovery.skipped.length > 0) {
779
+ process.stderr.write((0, color_1.yellow)(` Warning: ${discovery.skipped.length} package(s) skipped (max-packages=${config.maxPackages})\n`));
780
+ }
781
+ // Call Detection API
782
+ const result = await (0, api_1.callAnalyzeAPI)(packages, config);
783
+ // Render output
784
+ const output = (0, output_1.renderResult)(result, config);
785
+ process.stdout.write(output + "\n");
786
+ // Exit code
787
+ if (result.action === "block" && config.mode === "block") {
788
+ process.exit(2);
789
+ }
790
+ else if (result.action === "block" || result.action === "warn") {
791
+ process.exit(1);
792
+ }
793
+ process.exit(0);
794
+ }
795
+ main().catch((err) => {
796
+ process.stderr.write(`\n ${(0, color_1.bold)((0, color_1.red)("Error:"))} ${err.message}\n\n`);
797
+ process.exit(1);
798
+ });
799
+
800
+ })();
801
+
802
+ module.exports = __webpack_exports__;
803
+ /******/ })()
804
+ ;
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@westbayberry/dg",
3
+ "version": "1.0.0",
4
+ "description": "Supply chain security scanner — scan npm dependencies in any CI or terminal",
5
+ "bin": {
6
+ "dependency-guardian": "./dist/index.js",
7
+ "dg": "./dist/index.js"
8
+ },
9
+ "files": ["dist"],
10
+ "license": "UNLICENSED",
11
+ "author": "WestBayBerry",
12
+ "homepage": "https://westbayberry.com",
13
+ "keywords": ["security", "npm", "supply-chain", "scanner", "cli", "dependencies"],
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "scripts": {
18
+ "build": "npx ncc build src/bin.ts -o dist",
19
+ "dev": "npx tsx src/bin.ts",
20
+ "test": "vitest run",
21
+ "prepublishOnly": "npm run build && npm test"
22
+ },
23
+ "engines": {
24
+ "node": ">=18"
25
+ },
26
+ "devDependencies": {
27
+ "@types/node": "^20.0.0",
28
+ "@vercel/ncc": "^0.38.4",
29
+ "typescript": "^5.9.3",
30
+ "vitest": "^3.0.0"
31
+ }
32
+ }