@westbayberry/dg 2.3.0 → 2.3.2

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.
@@ -68,8 +68,18 @@ const PIP_VALUE_FLAGS = new Set([
68
68
  "--extra-index-url", "-f", "--find-links", "-t", "--target", "--platform",
69
69
  "--python-version", "--implementation", "--abi", "--prefix", "--root", "--no-binary",
70
70
  "--only-binary", "--progress-bar",
71
+ // Global value-flags that can precede the `install` subcommand; without them
72
+ // the flag's value is mistaken for the verb and the real package is missed.
73
+ "--log", "--cache-dir", "--proxy", "--timeout", "--retries", "--cert", "--client-cert",
74
+ "--trusted-host", "--log-file", "--python",
75
+ ]);
76
+ const NPM_VALUE_FLAGS = new Set([
77
+ "--registry", "--prefix", "-C", "--workspace", "-w", "--tag", "--otp",
78
+ // Global value-flags that can precede the subcommand.
79
+ "--loglevel", "--cache", "--userconfig", "--globalconfig", "--node-options", "--script-shell",
80
+ "--before", "--omit", "--include", "--https-proxy", "--proxy", "--cafile", "--cert", "--key",
81
+ "--user-agent", "--fetch-timeout", "--fetch-retries", "--maxsockets", "--network-timeout",
71
82
  ]);
72
- const NPM_VALUE_FLAGS = new Set(["--registry", "--prefix", "-C", "--workspace", "-w", "--tag", "--otp"]);
73
83
  function analyzeEcosystem(eco) {
74
84
  if (eco === "javascript")
75
85
  return "npm";
@@ -78,14 +88,123 @@ function analyzeEcosystem(eco) {
78
88
  return null;
79
89
  }
80
90
  function splitSegments(line) {
81
- // Split on shell control operators, but not on a `&` that is part of a
82
- // redirection (`2>&1`, `>&2`, `&>file`) where the `&` is glued to a `>` —
83
- // shearing there turns a redirection into a phantom package token.
84
- return line.split(/&&|\|\||;|\||[\n\r]|(?<!>)&(?!>)/).map((s) => s.trim()).filter(Boolean);
91
+ // Split on shell control operators (`&&`, `||`, `;`, `|`, newline, and a
92
+ // backgrounding `&`), but only OUTSIDE single/double quotes and backslash
93
+ // escapes the shell treats a quoted `;`/`&&`/`|` as literal data, so a
94
+ // separator inside a curl header, a search query, or a JSON body must not
95
+ // shear the argument into a phantom command segment (a quoted `npm install`
96
+ // would otherwise be screened as a real install). A `$(…)`/backtick command
97
+ // substitution inside quotes is still reached: collectSegments extracts its
98
+ // body separately. A bare `&` glued to a `>` (`2>&1`, `&>file`) is part of a
99
+ // redirection, not a separator.
100
+ const segments = [];
101
+ let buf = "";
102
+ let quote = null;
103
+ let ansiC = false;
104
+ const push = () => {
105
+ const t = buf.trim();
106
+ if (t) {
107
+ segments.push(t);
108
+ }
109
+ buf = "";
110
+ };
111
+ for (let i = 0; i < line.length; i += 1) {
112
+ const ch = line[i] ?? "";
113
+ if (ansiC) {
114
+ // ANSI-C `$'…'`: a backslash escapes the next char, so `\'` does NOT close
115
+ // the span — track it the way lexSegment/decodeAnsiC and bash do, or the
116
+ // quote state desyncs and a real `;`/`&&` after it is mis-split.
117
+ if (ch === "\\" && i + 1 < line.length) {
118
+ buf += ch + line[i + 1];
119
+ i += 1;
120
+ continue;
121
+ }
122
+ buf += ch;
123
+ if (ch === "'") {
124
+ ansiC = false;
125
+ }
126
+ continue;
127
+ }
128
+ if (quote === "'") {
129
+ buf += ch;
130
+ if (ch === "'") {
131
+ quote = null;
132
+ }
133
+ continue;
134
+ }
135
+ if (quote === '"') {
136
+ if (ch === "\\" && i + 1 < line.length) {
137
+ buf += ch + line[i + 1];
138
+ i += 1;
139
+ continue;
140
+ }
141
+ buf += ch;
142
+ if (ch === '"') {
143
+ quote = null;
144
+ }
145
+ continue;
146
+ }
147
+ if (ch === "$" && line[i + 1] === "'") {
148
+ ansiC = true;
149
+ buf += "$'";
150
+ i += 1;
151
+ continue;
152
+ }
153
+ if (ch === "\\" && i + 1 < line.length) {
154
+ buf += ch + line[i + 1];
155
+ i += 1;
156
+ continue;
157
+ }
158
+ if (ch === "'" || ch === '"') {
159
+ quote = ch;
160
+ buf += ch;
161
+ continue;
162
+ }
163
+ if (ch === "#" && (i === 0 || /\s/.test(line[i - 1] ?? ""))) {
164
+ // A bash comment begins at an unquoted `#` on a word boundary and runs to
165
+ // end of line; the shell never executes that text, so splitting it into a
166
+ // phantom install segment would be a false block.
167
+ push();
168
+ while (i < line.length && line[i] !== "\n" && line[i] !== "\r") {
169
+ i += 1;
170
+ }
171
+ i -= 1;
172
+ continue;
173
+ }
174
+ if (ch === "\n" || ch === "\r" || ch === ";") {
175
+ push();
176
+ continue;
177
+ }
178
+ if (ch === "|") {
179
+ push();
180
+ if (line[i + 1] === "|") {
181
+ i += 1;
182
+ }
183
+ continue;
184
+ }
185
+ if (ch === "&") {
186
+ if (line[i + 1] === "&") {
187
+ push();
188
+ i += 1;
189
+ continue;
190
+ }
191
+ if (line[i - 1] === ">" || line[i + 1] === ">") {
192
+ buf += ch;
193
+ continue;
194
+ }
195
+ push();
196
+ continue;
197
+ }
198
+ buf += ch;
199
+ }
200
+ push();
201
+ return segments;
85
202
  }
86
203
  function substitutionBodies(line) {
87
204
  const bodies = [];
88
- const patterns = [/\$\(([^)]*)\)/g, /`([^`]*)`/g];
205
+ // `$(…)`, backtick, and process substitution `<()` / `>()` all run their
206
+ // body as a command, so an install hidden inside one must be re-screened.
207
+ const patterns = [/\$\(([^)]*)\)/g, /`([^`]*)`/g, /<\(([^)]*)\)/g, />\(([^)]*)\)/g];
89
208
  for (const pattern of patterns) {
90
209
  let m;
91
210
  while ((m = pattern.exec(line)) !== null) {
@@ -332,6 +451,13 @@ const WRAPPERS = new Map([
332
451
  ["xargs", { valueFlags: new Set(["-a", "--arg-file", "-d", "--delimiter", "-E", "-e", "--eof", "-I", "-i", "--replace", "-L", "-l", "--max-lines", "-n", "--max-args", "-P", "--max-procs", "-s", "--max-chars"]) }],
333
452
  ]);
334
453
  const SHELL_EXEC = new Set(["sh", "bash", "zsh", "dash", "ash"]);
454
+ // Control-flow keywords that introduce a command (`if …; then <cmd>`, `for …;
455
+ // do <cmd>`, `! <cmd>`). The segmenter splits on `;`, so the command after one
456
+ // lands at the head of its segment behind the keyword — strip it or a real
457
+ // `do npm install <pkg>` is read as the no-op command `do`.
458
+ const SHELL_KEYWORDS = new Set(["then", "do", "else", "elif", "!"]);
459
+ // A `case … in <pat>) <cmd> ;;` arm label, e.g. `a)`, `*)`, `*.js)`, `a|b)`.
460
+ const CASE_LABEL = /^[A-Za-z0-9_*?.\][|@!+-]+\)$/;
335
461
  // Package managers dg doesn't statically screen (no shim, no classifier). A
336
462
  // recognized install verb for one of these can't be screened by the hook, so it
337
463
  // defers to the runtime gate rather than waving it through silently.
@@ -374,6 +500,22 @@ function commandTokens(segment) {
374
500
  while (tokens.length > 0 && ENV_ASSIGNMENT.test(tokens[0] ?? "")) {
375
501
  tokens = tokens.slice(1);
376
502
  }
503
+ const kw = tokens[0] ?? "";
504
+ if (SHELL_KEYWORDS.has(kw)) {
505
+ tokens = tokens.slice(1);
506
+ continue;
507
+ }
508
+ if (kw === "case") {
509
+ const close = tokens.findIndex((t) => t.endsWith(")"));
510
+ if (close > 0) {
511
+ tokens = tokens.slice(close + 1);
512
+ continue;
513
+ }
514
+ }
515
+ if (CASE_LABEL.test(kw)) {
516
+ tokens = tokens.slice(1);
517
+ continue;
518
+ }
377
519
  const head = commandBasename(tokens[0] ?? "");
378
520
  if (head === "eval" && tokens.length > 1) {
379
521
  return { tokens, ok: lexed.ok, shellScript: tokens.slice(1).join(" "), ...x() };
@@ -7,8 +7,43 @@ const yarnProtected = new Set(["add", "install", "upgrade", "dlx", "create"]);
7
7
  const pipProtected = new Set(["install", "download", "wheel"]);
8
8
  const pipxProtected = new Set(["install", "upgrade", "inject", "run"]);
9
9
  const uvProtected = new Set(["add", "sync"]);
10
- const cargoProtected = new Set(["add", "install", "fetch", "update", "build", "test", "check", "run"]);
10
+ // Only the dependency-set-changing verbs. `build`/`test`/`run`/`check` compile
11
+ // local code (the npm `run`/`build` analog, which is passthrough) and crates are
12
+ // not analyzable yet, so gating them just interrupts every Rust build with no
13
+ // screening value.
14
+ const cargoProtected = new Set(["add", "install", "fetch", "update"]);
11
15
  const passthrough = new Set(["help", "--help", "-h", "version", "--version", "-v", "list", "ls", "show", "freeze"]);
16
+ // Recognized read-only / local / script subcommands across the managers. Used
17
+ // only to locate the real subcommand when a global value-flag puts a non-verb
18
+ // token first (`npm --prefix /tmp install x` → action `/tmp`, missing the
19
+ // install). A flag value (a path, URL, or log level) is never a recognized
20
+ // verb, so the scan steps over it; a script-runner verb (`run`, `view`) is
21
+ // matched before any later install-looking token, so `npm run install` is not
22
+ // mis-screened. Must contain no install verb (those live in the protected sets).
23
+ const READ_ONLY_VERBS = new Set([
24
+ "run", "run-script", "start", "stop", "restart", "test", "build", "watch", "serve", "lint", "format",
25
+ "ls", "list", "ll", "la", "view", "show", "info", "outdated", "why", "fund", "audit", "doctor", "ping",
26
+ "config", "get", "cache", "whoami", "org", "team", "access", "token", "profile", "search", "dist-tag",
27
+ "docs", "repo", "home", "bin", "root", "prefix", "completion", "help", "version", "explain", "star", "unstar",
28
+ "deprecate", "sbom", "query", "explore", "pack", "publish", "link", "unlink", "prune", "rebuild",
29
+ "logout", "login", "adduser", "owner", "set", "init", "licenses",
30
+ "freeze", "check", "uninstall", "debug", "hash", "inspect", "index", "tree", "lock", "venv", "export", "python", "self",
31
+ ]);
32
+ function findAction(args, protectedCommands) {
33
+ let firstPositional;
34
+ for (const arg of args) {
35
+ if (arg.startsWith("-")) {
36
+ continue;
37
+ }
38
+ if (firstPositional === undefined) {
39
+ firstPositional = arg;
40
+ }
41
+ if (protectedCommands.has(arg) || READ_ONLY_VERBS.has(arg)) {
42
+ return arg;
43
+ }
44
+ }
45
+ return firstPositional ?? "";
46
+ }
12
47
  // pip3, pip3.12, python3, python3.11 are the canonical install commands on
13
48
  // macOS/Homebrew and many Linux distros (see BINARY_FALLBACKS in
14
49
  // resolve-real-binary.ts). The classifier keys on the base name, so a
@@ -61,8 +96,8 @@ export function classifyPackageManagerInvocation(rawManager, args) {
61
96
  return classifyByCommand(manager, args, "cargo", cargoProtected, "Cargo command can fetch crates", "rust");
62
97
  }
63
98
  function classifyByCommand(manager, args, realBinaryName, protectedCommands, protectedReason, ecosystem) {
64
- const action = firstCommand(args);
65
- if (protectedCommands.has(action) || containsFetchSpec(args) || initFetchesPackage(action, args, protectedCommands)) {
99
+ const action = findAction(args, protectedCommands);
100
+ if (protectedCommands.has(action) || initFetchesPackage(action, args, protectedCommands)) {
66
101
  return {
67
102
  manager,
68
103
  ecosystem,
@@ -96,7 +131,7 @@ function classifyUv(args) {
96
131
  // fetch-and-execute path that the bare `run` verb otherwise waves through.
97
132
  return protectedClassification("uv", args, "uv", "uv run --with fetches and runs a package");
98
133
  }
99
- if (uvProtected.has(action) || containsFetchSpec(args)) {
134
+ if (uvProtected.has(action)) {
100
135
  return protectedClassification("uv", args, "uv", "uv install/fetch command");
101
136
  }
102
137
  return {
@@ -148,6 +183,3 @@ function initFetchesPackage(action, args, protectedCommands) {
148
183
  export function uvRunFetchesPackage(args) {
149
184
  return args.some((a) => a === "--with" || a === "--with-editable" || a === "--with-requirements" || a.startsWith("--with="));
150
185
  }
151
- function containsFetchSpec(args) {
152
- return args.some((arg) => /^https?:\/\//.test(arg) || /^git\+https?:\/\//.test(arg) || /\.t(ar\.)?gz(?:$|[#?])/.test(arg));
153
- }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@westbayberry/dg",
3
- "version": "2.3.0",
3
+ "version": "2.3.2",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@westbayberry/dg",
9
- "version": "2.3.0",
9
+ "version": "2.3.2",
10
10
  "license": "Apache-2.0",
11
11
  "dependencies": {
12
12
  "chalk": "4.1.2",
package/package.json CHANGED
@@ -1,7 +1,23 @@
1
1
  {
2
2
  "name": "@westbayberry/dg",
3
- "version": "2.3.0",
3
+ "version": "2.3.2",
4
4
  "description": "Dependency Guardian supply-chain firewall CLI",
5
+ "keywords": [
6
+ "supply-chain-security",
7
+ "supply-chain",
8
+ "npm-security",
9
+ "pypi-security",
10
+ "malware-scanner",
11
+ "malware-detection",
12
+ "dependency-scanner",
13
+ "npm-audit-alternative",
14
+ "install-time-security",
15
+ "typosquatting",
16
+ "postinstall",
17
+ "ai-agent-security",
18
+ "claude-code",
19
+ "security"
20
+ ],
5
21
  "repository": {
6
22
  "type": "git",
7
23
  "url": "git+https://github.com/WestBayBerry/DG_CLI.git"
@@ -9,7 +25,7 @@
9
25
  "bugs": {
10
26
  "url": "https://github.com/WestBayBerry/DG_CLI/issues"
11
27
  },
12
- "homepage": "https://github.com/WestBayBerry/DG_CLI#readme",
28
+ "homepage": "https://westbayberry.com",
13
29
  "type": "module",
14
30
  "bin": {
15
31
  "dg": "./dist/bin/dg.js"