@westbayberry/dg 1.0.2 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -57,7 +57,7 @@ dg scan [options]
57
57
 
58
58
  ## CI Examples
59
59
 
60
- ### GitHub Actions
60
+ ### GitHub Actions CI
61
61
 
62
62
  ```yaml
63
63
  - name: Scan dependencies
package/dist/index.js CHANGED
@@ -180,15 +180,37 @@ exports.getVersion = getVersion;
180
180
  const node_util_1 = __nccwpck_require__(975);
181
181
  const node_fs_1 = __nccwpck_require__(24);
182
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
+ }
183
201
  const USAGE = `
184
202
  Dependency Guardian — Supply chain security scanner
185
203
 
186
204
  Usage:
187
205
  dependency-guardian scan [options]
188
206
  dg scan [options]
207
+ dg npm install <pkg> [npm-flags]
208
+ dg wrap
189
209
 
190
210
  Commands:
191
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
192
214
 
193
215
  Options:
194
216
  --api-key <key> API key (or set DG_API_KEY env var)
@@ -201,25 +223,36 @@ const USAGE = `
201
223
  --json Output JSON for CI parsing
202
224
  --scan-all Scan all packages, not just changed
203
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
204
228
  --help Show this help message
205
229
  --version Show version number
206
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
+
207
235
  Environment Variables:
208
236
  DG_API_KEY API key
209
237
  DG_API_URL API base URL
210
238
  DG_MODE Mode (block/warn/off)
211
239
  DG_ALLOWLIST Comma-separated allowlist
240
+ DG_DEBUG Enable debug output (set to 1)
212
241
 
213
242
  Exit Codes:
214
243
  0 pass — No risks detected
215
244
  1 warn — Risks detected (advisory)
216
245
  2 block — High-risk packages detected
246
+ 3 error — Internal error (API failure, config error)
217
247
 
218
248
  Examples:
219
249
  DG_API_KEY=dg_live_xxx dg scan
220
250
  dg scan --api-key dg_live_xxx --json
221
251
  dg scan --scan-all --mode block
222
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
223
256
  `.trimStart();
224
257
  exports.USAGE = USAGE;
225
258
  function getVersion() {
@@ -245,6 +278,8 @@ function parseConfig(argv) {
245
278
  json: { type: "boolean", default: false },
246
279
  "scan-all": { type: "boolean", default: false },
247
280
  "base-lockfile": { type: "string" },
281
+ debug: { type: "boolean", default: false },
282
+ "no-config": { type: "boolean", default: false },
248
283
  help: { type: "boolean", default: false },
249
284
  version: { type: "boolean", default: false },
250
285
  },
@@ -260,11 +295,14 @@ function parseConfig(argv) {
260
295
  process.exit(0);
261
296
  }
262
297
  const command = positionals[0] ?? "scan";
298
+ const noConfig = values["no-config"];
299
+ const dgrc = noConfig ? {} : loadDgrc();
263
300
  if (values["api-key"]) {
264
301
  process.stderr.write("Warning: --api-key is deprecated (visible in process list). Use DG_API_KEY env var instead.\n");
265
302
  }
266
303
  const apiKey = values["api-key"] ??
267
304
  process.env.DG_API_KEY ??
305
+ dgrc.apiKey ??
268
306
  "";
269
307
  if (!apiKey) {
270
308
  process.stderr.write("Error: API key required. Set DG_API_KEY environment variable.\n" +
@@ -274,6 +312,7 @@ function parseConfig(argv) {
274
312
  }
275
313
  const modeRaw = values.mode ??
276
314
  process.env.DG_MODE ??
315
+ dgrc.mode ??
277
316
  "warn";
278
317
  if (!["block", "warn", "off"].includes(modeRaw)) {
279
318
  process.stderr.write(`Error: Invalid mode "${modeRaw}". Must be block, warn, or off.\n`);
@@ -282,23 +321,40 @@ function parseConfig(argv) {
282
321
  const allowlistRaw = values.allowlist ??
283
322
  process.env.DG_ALLOWLIST ??
284
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
+ }
285
340
  return {
286
341
  apiKey,
287
342
  apiUrl: values["api-url"] ??
288
343
  process.env.DG_API_URL ??
344
+ dgrc.apiUrl ??
289
345
  "https://api.westbayberry.com",
290
346
  mode: modeRaw,
291
- blockThreshold: Number(values["block-threshold"] ?? "70"),
292
- warnThreshold: Number(values["warn-threshold"] ?? "60"),
293
- maxPackages: Number(values["max-packages"] ?? "200"),
347
+ blockThreshold,
348
+ warnThreshold,
349
+ maxPackages,
294
350
  allowlist: allowlistRaw
295
- .split(",")
296
- .map((s) => s.trim())
297
- .filter(Boolean),
351
+ ? allowlistRaw.split(",").map((s) => s.trim()).filter(Boolean)
352
+ : (dgrc.allowlist ?? []),
298
353
  json: values.json,
299
354
  scanAll: values["scan-all"],
300
355
  baseLockfile: values["base-lockfile"] ?? null,
301
356
  command,
357
+ debug,
302
358
  };
303
359
  }
304
360
 
@@ -476,6 +532,273 @@ function toPackageInput(change) {
476
532
  }
477
533
 
478
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
+
479
802
  /***/ }),
480
803
 
481
804
  /***/ 202:
@@ -538,9 +861,10 @@ function renderResult(result, config) {
538
861
  lines.push("");
539
862
  return lines.join("\n");
540
863
  }
541
- // Package table
542
- const COL_NAME = 22;
543
- const COL_VER = 22;
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)));
544
868
  const COL_SCORE = 7;
545
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")}`);
546
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))}`);
@@ -721,7 +1045,7 @@ function parseLockfile(content) {
721
1045
  if (path === "")
722
1046
  continue;
723
1047
  const name = extractPackageName(path);
724
- if (name) {
1048
+ if (name && !packages.has(name)) {
725
1049
  const e = entry;
726
1050
  packages.set(name, {
727
1051
  version: e.version ?? "",
@@ -778,6 +1102,13 @@ module.exports = require("node:fs");
778
1102
 
779
1103
  /***/ }),
780
1104
 
1105
+ /***/ 161:
1106
+ /***/ ((module) => {
1107
+
1108
+ module.exports = require("node:os");
1109
+
1110
+ /***/ }),
1111
+
781
1112
  /***/ 760:
782
1113
  /***/ ((module) => {
783
1114
 
@@ -841,15 +1172,35 @@ const lockfile_1 = __nccwpck_require__(746);
841
1172
  const api_1 = __nccwpck_require__(879);
842
1173
  const output_1 = __nccwpck_require__(202);
843
1174
  const color_1 = __nccwpck_require__(988);
1175
+ const npm_wrapper_1 = __nccwpck_require__(124);
844
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
+ }
845
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
+ };
846
1194
  if (config.mode === "off") {
847
1195
  process.stderr.write((0, color_1.dim)(" Dependency Guardian: mode is off — skipping.\n"));
848
1196
  process.exit(0);
849
1197
  }
1198
+ dbg(`mode=${config.mode} block=${config.blockThreshold} warn=${config.warnThreshold} max=${config.maxPackages}`);
1199
+ dbg(`api=${config.apiUrl}`);
850
1200
  // Discover packages
851
1201
  process.stderr.write((0, color_1.dim)(" Discovering package changes...\n"));
852
1202
  const discovery = (0, lockfile_1.discoverChanges)(process.cwd(), config);
1203
+ dbg(`discovery method: ${discovery.method}`);
853
1204
  if (discovery.packages.length === 0) {
854
1205
  process.stderr.write((0, color_1.dim)(" No package changes detected.\n"));
855
1206
  process.exit(0);
@@ -864,10 +1215,13 @@ async function main() {
864
1215
  if (discovery.skipped.length > 0) {
865
1216
  process.stderr.write((0, color_1.yellow)(` Warning: ${discovery.skipped.length} package(s) skipped (max-packages=${config.maxPackages})\n`));
866
1217
  }
1218
+ dbg(`packages to scan: ${packages.map(p => `${p.name}@${p.version}`).slice(0, 10).join(", ")}${packages.length > 10 ? ` (+${packages.length - 10} more)` : ""}`);
867
1219
  // Call Detection API
1220
+ const startMs = Date.now();
868
1221
  const result = await (0, api_1.callAnalyzeAPI)(packages, config, (done, total) => {
869
1222
  process.stderr.write((0, color_1.dim)(` Analyzed ${done}/${total}...\n`));
870
1223
  });
1224
+ dbg(`API responded in ${Date.now() - startMs}ms, action=${result.action}, score=${result.score}`);
871
1225
  // Render output
872
1226
  const output = (0, output_1.renderResult)(result, config);
873
1227
  process.stdout.write(output + "\n");
@@ -882,7 +1236,7 @@ async function main() {
882
1236
  }
883
1237
  main().catch((err) => {
884
1238
  process.stderr.write(`\n ${(0, color_1.bold)((0, color_1.red)("Error:"))} ${err.message}\n\n`);
885
- process.exit(1);
1239
+ process.exit(3); // Exit 3 = internal error (distinct from exit 1 = warnings)
886
1240
  });
887
1241
 
888
1242
  })();