vigiles 14.13.4 → 14.13.5

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.
@@ -75,6 +75,7 @@ export interface TrifectaFinding {
75
75
  /** A ready-to-show, actionable message. */
76
76
  readonly message: string;
77
77
  }
78
+ export declare function bashGrantIsUnbounded(raw: string): boolean;
78
79
  /**
79
80
  * Classify each tool in a declared contract into the trifecta legs it supplies.
80
81
  * A single tool may land in MULTIPLE legs (`Bash` → A+C, `WebFetch` → B+C). An
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.bashGrantIsUnbounded = bashGrantIsUnbounded;
3
4
  exports.classifyTrifectaLegs = classifyTrifectaLegs;
4
5
  exports.lethalTrifectaIssues = lethalTrifectaIssues;
5
6
  // ---------------------------------------------------------------------------
@@ -71,6 +72,100 @@ const EXFIL_MCP_TOOLS = [
71
72
  function baseTool(raw) {
72
73
  return raw.split("(")[0].trim();
73
74
  }
75
+ /** The restriction inside `Tool(...)`, or null when the grant is unrestricted. */
76
+ function restriction(raw) {
77
+ const open = raw.indexOf("(");
78
+ if (open === -1)
79
+ return null;
80
+ const close = raw.lastIndexOf(")");
81
+ if (close <= open)
82
+ return null;
83
+ return raw.slice(open + 1, close).trim();
84
+ }
85
+ /**
86
+ * Does a `Bash(...)` grant still supply the shell's legs — reading a secret and
87
+ * curling it out — or has it been narrowed to a command that cannot?
88
+ *
89
+ * 🔴 WHY THIS EXISTS. Narrowing the grant is the remedy this tool RECOMMENDS: drop a
90
+ * leg, allow at most two. Before this, `Bash(node ./scripts/log.mjs:*)` was read as
91
+ * plain `Bash` — the whole shell, in both leg A and leg C — so an author who took the
92
+ * advice saw their score not move, and the reasonable next conclusion is that
93
+ * narrowing is pointless. A diagnosis blind to its own prescription teaches the wrong
94
+ * lesson. Observed 2026-08-07 on a repo that narrowed nine skills to two named ledger
95
+ * commands and stayed at "17 units can read data, reach the web, and run commands".
96
+ *
97
+ * HIGH-PRECISION, deliberately. The grant loses the legs ONLY when the pattern pins a
98
+ * program AND at least one concrete argument, e.g. `node ./x.mjs:*`. These stay full
99
+ * shell, because each still runs whatever the caller likes:
100
+ *
101
+ * Bash no restriction at all
102
+ * Bash(*) Bash(:*) the wildcard forms
103
+ * Bash(node:*) program pinned, arguments free — `node -e "..."` is a shell
104
+ * Bash(sh ...) Bash(bash ...) Bash(eval ...) the shell itself, however pinned
105
+ * Bash(curl ...) Bash(scp ...) a pinned program whose whole job IS exfiltration
106
+ *
107
+ * When in doubt it returns true (keeps the legs): a false "you are exposed" costs the
108
+ * author an argument, a false "you are safe" costs them the finding.
109
+ */
110
+ const SHELL_LIKE = new Set([
111
+ "sh",
112
+ "bash",
113
+ "zsh",
114
+ "dash",
115
+ "ksh",
116
+ "fish",
117
+ "eval",
118
+ "exec",
119
+ "env",
120
+ "xargs",
121
+ "sudo",
122
+ "doas",
123
+ "nohup",
124
+ "setsid",
125
+ "script",
126
+ "ssh",
127
+ "docker",
128
+ "podman",
129
+ "make",
130
+ ]);
131
+ const EXFIL_PROGRAMS = new Set([
132
+ "curl",
133
+ "wget",
134
+ "scp",
135
+ "sftp",
136
+ "rsync",
137
+ "nc",
138
+ "ncat",
139
+ "netcat",
140
+ "telnet",
141
+ "ftp",
142
+ "git",
143
+ "gh",
144
+ "aws",
145
+ "gcloud",
146
+ "az",
147
+ "kubectl",
148
+ "npm",
149
+ "npx",
150
+ "pip",
151
+ ]);
152
+ function bashGrantIsUnbounded(raw) {
153
+ const r = restriction(raw);
154
+ if (r === null)
155
+ return true; // bare `Bash`
156
+ const pattern = r.replace(/^["']|["']$/g, "").trim();
157
+ if (pattern === "" || pattern === "*" || pattern === ":*")
158
+ return true;
159
+ // `Bash(node ./x.mjs:*)` — the trailing `:*` is Claude Code's prefix marker, not an
160
+ // argument. Strip it before deciding whether any concrete argument was pinned.
161
+ const words = pattern.replace(/:\*$/, "").trim().split(/\s+/).filter(Boolean);
162
+ if (words.length < 2)
163
+ return true; // program pinned, arguments free
164
+ const program = (words[0].split("/").pop() ?? words[0]).toLowerCase();
165
+ if (SHELL_LIKE.has(program) || EXFIL_PROGRAMS.has(program))
166
+ return true;
167
+ return false;
168
+ }
74
169
  /** Returns true for the wildcard sentinels that mean "inherits-all". */
75
170
  function isWildcard(tool) {
76
171
  return tool === "" || tool === "*";
@@ -118,10 +213,14 @@ function classifyTrifectaLegs(tools, dialect) {
118
213
  const base = baseTool(raw);
119
214
  if (isWildcard(base))
120
215
  continue; // handled by the issues fn, not a named leg
121
- // Dual-role shell: leg A AND leg C.
216
+ // Dual-role shell: leg A AND leg C — unless the grant has been narrowed to a
217
+ // command that is neither a shell nor an exfiltration tool, which is the remedy
218
+ // this checker itself recommends. See {@link bashGrantIsUnbounded}.
122
219
  if (LEG_BASH_DUAL.has(base)) {
123
- priv.add(base);
124
- exfil.add(base);
220
+ if (bashGrantIsUnbounded(raw)) {
221
+ priv.add(base);
222
+ exfil.add(base);
223
+ }
125
224
  continue;
126
225
  }
127
226
  if (PRIVATE_BUILTINS.has(base))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vigiles",
3
- "version": "14.13.4",
3
+ "version": "14.13.5",
4
4
  "description": "Lint & test the harness your AI agent runs on — verify the references in your CLAUDE.md / AGENTS.md and test that your hooks and skills actually work.",
5
5
  "keywords": [
6
6
  "claude-code",