@goodandready/dsh-approval-gate 0.1.4 → 0.1.6

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/lib/inspect.js CHANGED
@@ -1,4 +1,10 @@
1
- import { MESSAGES } from "./messages.js";
1
+ import { DYNAMIC_MARKER, HEREDOC_OPS, REDIR_OPS, SPLIT_OPS, tokenize, readCommandSubstitution, readBacktick, readParameterExpansion } from "./tokenizer.js";
2
+ import { expandHome } from "./paths.js";
3
+ import { askMessage, denyMessage, sessionTag } from "./messages.js";
4
+
5
+ export { DYNAMIC_MARKER, tokenize, splitSubcommands } from "./tokenizer.js";
6
+ export { askMessage, denyMessage, sessionTag } from "./messages.js";
7
+ export { expandHome } from "./paths.js";
2
8
 
3
9
  // Pure command/path inspector for dsh-approval-gate.
4
10
  // No Cordis imports: unit tests run this without the harness.
@@ -13,10 +19,6 @@ export const DEFAULT_FILE_WRITE_TOOLS = Object.freeze([
13
19
  "apply_patch",
14
20
  ]);
15
21
 
16
- const SPLIT_OPS = new Set(["|", "|&", "||", "&&", ";", "&"]);
17
- const REDIR_OPS = new Set([">", ">>", ">|", "<>", "2>", "2>>", "&>", "&>>"]);
18
- const HEREDOC_OPS = new Set(["<<", "<<-"]);
19
- const DYNAMIC_MARKER = "__shell_dynamic__";
20
22
  const PREFIXES = new Set(["then", "do"]);
21
23
  const SHELLS = new Set(["bash", "sh", "dash", "zsh", "ksh", "ash"]);
22
24
  const INTERPRETERS = new Set(["python", "python2", "python3", "node", "nodejs", "perl", "ruby", "php"]);
@@ -36,335 +38,23 @@ export function basename(path) {
36
38
  return parts[parts.length - 1] || "";
37
39
  }
38
40
 
39
- export function isProtectedPath(path) {
41
+ export function isProtectedPath(path, env = process.env) {
40
42
  if (typeof path !== "string" || path.trim() === "") return false;
41
- const base = basename(path);
43
+ const expanded = expandHome(path, env);
44
+ const normalized = expanded.replace(/\\/g, "/");
45
+ if (normalized.includes("/etc/sudoers.d/")) return true;
46
+ const base = basename(normalized);
42
47
  if (base === ".env" || base.startsWith(".env.")) return true;
43
48
  if (/^credentials\.ya?ml$/i.test(base)) return true;
44
49
  if (/^settings\.ya?ml$/i.test(base)) return true;
45
50
  if (base === "cordis.patch.yml") return true;
46
51
  if (/\.(pem|key|crt|p12|pfx)$/i.test(base)) return true;
47
52
  if (/^(id_rsa|id_ed25519|id_ecdsa)$/.test(base)) return true;
53
+ if (/^(shadow|gshadow|shadow-|gshadow-|sudoers|authorized_keys2?)$/i.test(base)) return true;
48
54
  if (/(?:^|[._-])(?:secret|token|credential|passwd|api[_-]?key)(?:$|[._-])/i.test(base)) return true;
49
55
  return false;
50
56
  }
51
57
 
52
- function fail(reason, snippet) {
53
- return { ok: false, reason: reason, snippet: snippet };
54
- }
55
-
56
- function opAt(s, i) {
57
- const rest = s.slice(i);
58
- if (rest.startsWith("<(") || rest.startsWith(">(")) return null;
59
- const candidates = ["2>&", "2>>", "&>>", "<<<", "<<-", "2>", ">>", ">|", "|&", "&&", "||", "&>", ">&", "<&", "<>", "<<", ">", "<", "|", ";", "&"];
60
- for (const op of candidates) {
61
- if (rest.startsWith(op)) return op;
62
- }
63
- return null;
64
- }
65
-
66
- function readCommandSubstitution(src, start) {
67
- let depth = 1;
68
- let quote = "";
69
- let escaped = false;
70
- for (let i = start + 2; i < src.length; i += 1) {
71
- const c = src[i];
72
- if (escaped) { escaped = false; continue; }
73
- if (c === "\\") { escaped = true; continue; }
74
- if (quote) {
75
- if (c === quote) quote = "";
76
- continue;
77
- }
78
- if (c === "'" || c === '"') { quote = c; continue; }
79
- if (c === String.fromCharCode(96)) {
80
- const end = readBacktick(src, i);
81
- if (!end) return null;
82
- i = end.end;
83
- continue;
84
- }
85
- if (src.startsWith("$(", i)) { depth += 1; i += 1; continue; }
86
- if (c === "(") { depth += 1; continue; }
87
- if (c === ")") {
88
- depth -= 1;
89
- if (depth === 0) return { script: src.slice(start + 2, i), end: i + 1 };
90
- }
91
- }
92
- return null;
93
- }
94
-
95
- function readBacktick(src, start) {
96
- let escaped = false;
97
- for (let i = start + 1; i < src.length; i += 1) {
98
- if (escaped) { escaped = false; continue; }
99
- if (src[i] === "\\") { escaped = true; continue; }
100
- if (src[i] === String.fromCharCode(96)) return { script: src.slice(start + 1, i), end: i + 1 };
101
- }
102
- return null;
103
- }
104
-
105
- function readParameterExpansion(src, start) {
106
- if (src[start + 1] !== "{") {
107
- let i = start + 1;
108
- if (/[A-Za-z_]/.test(src[i] || "")) {
109
- i += 1;
110
- while (i < src.length && /[A-Za-z0-9_]/.test(src[i])) i += 1;
111
- } else if (i < src.length) i += 1;
112
- return { end: i };
113
- }
114
- let depth = 1;
115
- let quote = "";
116
- for (let i = start + 2; i < src.length; i += 1) {
117
- const c = src[i];
118
- if (c === "\\") { i += 1; continue; }
119
- if (quote) {
120
- if (c === quote) quote = "";
121
- continue;
122
- }
123
- if (c === "'" || c === '"') { quote = c; continue; }
124
- if (c === String.fromCharCode(96)) {
125
- const nested = readBacktick(src, i);
126
- if (!nested) return null;
127
- i = nested.end - 1;
128
- continue;
129
- }
130
- if (src.startsWith("$" + "(", i)) {
131
- const nested = readCommandSubstitution(src, i);
132
- if (!nested) return null;
133
- i = nested.end - 1;
134
- continue;
135
- }
136
- if (src.startsWith("$" + "{", i)) {
137
- const nested = readParameterExpansion(src, i);
138
- if (!nested) return null;
139
- i = nested.end - 1;
140
- continue;
141
- }
142
- if (c === "{") depth += 1;
143
- if (c === "}") {
144
- depth -= 1;
145
- if (depth === 0) return { end: i + 1, body: src.slice(start + 2, i) };
146
- }
147
- }
148
- return null;
149
- }
150
-
151
- function lexFail(src, index, reason, tokens) {
152
- return { ok: false, reason: reason, snippet: src.slice(Math.max(0, index - 32), index + 96), tokens: tokens };
153
- }
154
-
155
- function readHeredocDelimiter(src, index) {
156
- let i = index;
157
- while (i < src.length && (src[i] === " " || src[i] === "\t")) i += 1;
158
- if (i >= src.length || src[i] === "\n" || src[i] === "\r") return null;
159
- let delimiter = "";
160
- let quote = "";
161
- let started = false;
162
- let quoted = false;
163
- while (i < src.length && !/\s/.test(src[i]) && !opAt(src, i)) {
164
- const c = src[i];
165
- if (c === "'" || c === '"') {
166
- if (!quote) { quote = c; quoted = true; started = true; i += 1; continue; }
167
- if (quote === c) { quote = ""; i += 1; continue; }
168
- }
169
- if (c === "\\") {
170
- if (i + 1 >= src.length) return null;
171
- quoted = true;
172
- delimiter += src[i + 1];
173
- started = true;
174
- i += 2;
175
- continue;
176
- }
177
- delimiter += c;
178
- started = true;
179
- i += 1;
180
- }
181
- if (quote || !started || !delimiter) return null;
182
- return { delimiter: delimiter, end: i, quoted: quoted };
183
- }
184
-
185
- export function tokenize(src) {
186
- if (typeof src !== "string") return { ok: false, reason: "not-a-string", snippet: "", tokens: [] };
187
- const tokens = [];
188
- const pendingHeredocs = [];
189
- let i = 0;
190
- const n = src.length;
191
-
192
- function flushWord(value, started, dynamic, substitutions) {
193
- if (started) tokens.push({ kind: "word", value: value, dynamic: dynamic, substitutions: substitutions });
194
- }
195
-
196
- while (i < n) {
197
- if (src.startsWith("\\\n", i)) { i += 2; continue; }
198
- if (src[i] === "\r" || src[i] === "\n") {
199
- if (src[i] === "\r" && src[i + 1] === "\n") i += 2;
200
- else i += 1;
201
- if (pendingHeredocs.length) {
202
- for (const heredoc of pendingHeredocs) {
203
- let body = "";
204
- let found = false;
205
- while (i <= n) {
206
- const lineEnd = src.indexOf("\n", i);
207
- const hasNewline = lineEnd !== -1;
208
- let line = src.slice(i, hasNewline ? lineEnd : n);
209
- if (line.endsWith("\r")) line = line.slice(0, -1);
210
- const compare = heredoc.stripTabs ? line.replace(/^\t+/, "") : line;
211
- if (compare === heredoc.delimiter) {
212
- i = hasNewline ? lineEnd + 1 : n;
213
- found = true;
214
- break;
215
- }
216
- body += (heredoc.stripTabs ? line.replace(/^\t+/, "") : line) + (hasNewline ? "\n" : "");
217
- if (!hasNewline) { i = n; break; }
218
- i = lineEnd + 1;
219
- }
220
- if (!found) return lexFail(src, i, "unterminated-heredoc", tokens);
221
- tokens.push({ kind: "heredoc", body: body, quoted: heredoc.quoted });
222
- }
223
- pendingHeredocs.length = 0;
224
- tokens.push({ kind: "op", value: ";" });
225
- } else {
226
- tokens.push({ kind: "op", value: ";" });
227
- }
228
- continue;
229
- }
230
- if (/\s/.test(src[i])) { i += 1; continue; }
231
- if (src[i] === "#" && (tokens.length === 0 || tokens[tokens.length - 1].kind === "op")) {
232
- while (i < n && src[i] !== "\n") i += 1;
233
- continue;
234
- }
235
-
236
- const op = opAt(src, i);
237
- if (op) {
238
- tokens.push({ kind: "op", value: op });
239
- i += op.length;
240
- if (HEREDOC_OPS.has(op)) {
241
- const parsed = readHeredocDelimiter(src, i);
242
- if (!parsed) return lexFail(src, i, "invalid-heredoc-delimiter", tokens);
243
- tokens.push({ kind: "heredoc-delimiter", value: parsed.delimiter });
244
- pendingHeredocs.push({ delimiter: parsed.delimiter, stripTabs: op === "<<-", quoted: parsed.quoted });
245
- i = parsed.end;
246
- }
247
- continue;
248
- }
249
-
250
- let word = "";
251
- let started = false;
252
- let dynamic = false;
253
- const substitutions = [];
254
- while (i < n && !/\s/.test(src[i]) && !opAt(src, i)) {
255
- const c = src[i];
256
- if (src.startsWith("<(", i) || src.startsWith(">(", i)) {
257
- const sub = readCommandSubstitution(src, i);
258
- if (!sub) return lexFail(src, i, "unclosed-process-substitution", tokens);
259
- substitutions.push({ kind: "process-substitution", script: sub.script });
260
- word += DYNAMIC_MARKER;
261
- dynamic = true;
262
- started = true;
263
- i = sub.end;
264
- continue;
265
- }
266
- if (c === "(" || c === ")") return lexFail(src, i, "unsupported-shell-grouping", tokens);
267
- if (c === "\\") {
268
- if (i + 1 >= n) return lexFail(src, i, "dangling-backslash", tokens);
269
- word += src[i + 1];
270
- started = true;
271
- i += 2;
272
- continue;
273
- }
274
- if (c === "'") {
275
- const j = src.indexOf("'", i + 1);
276
- if (j < 0) return lexFail(src, i, "unclosed-single-quote", tokens);
277
- word += src.slice(i + 1, j);
278
- started = true;
279
- i = j + 1;
280
- continue;
281
- }
282
- if (c === '"') {
283
- started = true;
284
- i += 1;
285
- let closed = false;
286
- while (i < n) {
287
- if (src[i] === '"') { i += 1; closed = true; break; }
288
- if (src[i] === "\\") {
289
- if (i + 1 >= n) return lexFail(src, i, "dangling-backslash", tokens);
290
- word += src[i + 1];
291
- i += 2;
292
- continue;
293
- }
294
- if (src.startsWith("$(", i)) {
295
- const sub = readCommandSubstitution(src, i);
296
- if (!sub) return lexFail(src, i, "unclosed-command-substitution", tokens);
297
- substitutions.push({ kind: "command-substitution", script: sub.script });
298
- word += DYNAMIC_MARKER;
299
- dynamic = true;
300
- i = sub.end;
301
- continue;
302
- }
303
- if (src[i] === String.fromCharCode(96)) {
304
- const sub = readBacktick(src, i);
305
- if (!sub) return lexFail(src, i, "unclosed-backtick-substitution", tokens);
306
- substitutions.push({ kind: "backtick-substitution", script: sub.script });
307
- word += DYNAMIC_MARKER;
308
- dynamic = true;
309
- i = sub.end;
310
- continue;
311
- }
312
- if (src[i] === "$") {
313
- const expansion = readParameterExpansion(src, i);
314
- if (!expansion) return lexFail(src, i, "unclosed-parameter-expansion", tokens);
315
- word += DYNAMIC_MARKER;
316
- dynamic = true;
317
- if (expansion.body !== undefined) substitutions.push({ kind: "parameter-expansion", script: expansion.body });
318
- i = expansion.end;
319
- continue;
320
- }
321
- word += src[i];
322
- i += 1;
323
- }
324
- if (!closed) return lexFail(src, i, "unclosed-double-quote", tokens);
325
- continue;
326
- }
327
- if (src.startsWith("$(", i)) {
328
- const sub = readCommandSubstitution(src, i);
329
- if (!sub) return lexFail(src, i, "unclosed-command-substitution", tokens);
330
- substitutions.push({ kind: "command-substitution", script: sub.script });
331
- word += DYNAMIC_MARKER;
332
- dynamic = true;
333
- started = true;
334
- i = sub.end;
335
- continue;
336
- }
337
- if (c === String.fromCharCode(96)) {
338
- const sub = readBacktick(src, i);
339
- if (!sub) return lexFail(src, i, "unclosed-backtick-substitution", tokens);
340
- substitutions.push({ kind: "backtick-substitution", script: sub.script });
341
- word += DYNAMIC_MARKER;
342
- dynamic = true;
343
- started = true;
344
- i = sub.end;
345
- continue;
346
- }
347
- if (c === "$") {
348
- const expansion = readParameterExpansion(src, i);
349
- if (!expansion) return lexFail(src, i, "unclosed-parameter-expansion", tokens);
350
- word += DYNAMIC_MARKER;
351
- dynamic = true;
352
- started = true;
353
- if (expansion.body !== undefined) substitutions.push({ kind: "parameter-expansion", script: expansion.body });
354
- i = expansion.end;
355
- continue;
356
- }
357
- if (c === "*" || c === "?") dynamic = true;
358
- word += c;
359
- started = true;
360
- i += 1;
361
- }
362
- flushWord(word, started, dynamic, substitutions);
363
- }
364
- if (pendingHeredocs.length) return lexFail(src, n, "unterminated-heredoc", tokens);
365
- return { ok: true, tokens: tokens };
366
- }
367
-
368
58
  function wordsOf(seg) {
369
59
  return seg.filter(function (t) { return t.kind === "word"; }).map(function (t) { return t.value; });
370
60
  }
@@ -510,7 +200,7 @@ function inspectArgv(argv, depth, hasHeredoc) {
510
200
 
511
201
  if (cmd === "eval") return deny("eval", argv.join(" "));
512
202
  if (KILL_CMDS.has(cmd)) return deny("process-signal", argv.join(" "));
513
- if (cmd === "rm" && (hasLetterFlag(argv, "r") || rest.indexOf("--recursive") !== -1)) {
203
+ if (cmd === "rm" && (hasLetterFlag(argv, "r") || hasLetterFlag(argv, "R") || rest.indexOf("--recursive") !== -1)) {
514
204
  return deny("recursive-rm", argv.join(" "));
515
205
  }
516
206
  if (cmd === "systemctl") {
@@ -543,17 +233,24 @@ function inspectArgv(argv, depth, hasHeredoc) {
543
233
  if (a.startsWith("of=") && (isDevicePath(a.slice(3)) || isProtectedPath(a.slice(3)))) return deny("device-write", a);
544
234
  }
545
235
  }
546
- if (cmd === "tee" || cmd === "install" || cmd === "cp" || cmd === "mv" || cmd === "truncate") {
236
+ if (cmd === "tee" || cmd === "install" || cmd === "cp" || cmd === "mv" || cmd === "truncate" || cmd === "chmod" || cmd === "chown") {
547
237
  for (const a of rest) {
548
238
  if (a[0] !== "-" && isProtectedPath(a)) return deny("secret-write", argv.join(" "));
549
239
  }
240
+ if ((cmd === "chmod" || cmd === "chown") && (hasLetterFlag(argv, "R") || hasLetterFlag(argv, "r") || rest.indexOf("--recursive") !== -1)) {
241
+ for (const a of rest) {
242
+ if (a === "/" || a === "/etc" || a === "/var" || a === "/usr" || a === "/root" || a === "/boot") {
243
+ return deny("device-write", argv.join(" "));
244
+ }
245
+ }
246
+ }
550
247
  }
551
- if (cmd === "sed" && (hasLetterFlag(argv, "i") || rest.indexOf("--in-place") !== -1)) {
248
+ if (cmd === "sed" && (hasLetterFlag(argv, "i") || rest.some(function (a) { return a === "--in-place" || a.startsWith("--in-place="); }))) {
552
249
  for (const a of rest) {
553
250
  if (a[0] !== "-" && isProtectedPath(a)) return deny("secret-write", argv.join(" "));
554
251
  }
555
252
  }
556
- if (SHELLS.has(cmd)) {
253
+ if (SHELLS.has(cmd) || cmd === "source" || cmd === ".") {
557
254
  const script = scriptAfterDashC(rest);
558
255
  if (typeof script === "string") {
559
256
  if (depth > 4) return deny("nested-shell", script);
@@ -583,7 +280,7 @@ function hasDangerousPipeline(tokens) {
583
280
  for (const token of tokens) {
584
281
  if (token.kind === "op" && SPLIT_OPS.has(token.value)) {
585
282
  const cmd = inspectSegment();
586
- if (inPipeline && SHELLS.has(cmd) && pipelineHasDownloader) return true;
283
+ if (inPipeline && (SHELLS.has(cmd) || INTERPRETERS.has(cmd)) && pipelineHasDownloader) return true;
587
284
  const continues = token.value === "|" || token.value === "|&";
588
285
  if (continues) {
589
286
  pipelineHasDownloader = pipelineHasDownloader || cmd === "curl" || cmd === "wget";
@@ -598,7 +295,33 @@ function hasDangerousPipeline(tokens) {
598
295
  segment.push(token);
599
296
  }
600
297
  const cmd = inspectSegment();
601
- return inPipeline && pipelineHasDownloader && SHELLS.has(cmd);
298
+ return inPipeline && pipelineHasDownloader && (SHELLS.has(cmd) || INTERPRETERS.has(cmd));
299
+ }
300
+
301
+ function hasDestructiveSqlPipeline(tokens) {
302
+ let pipelineHasDestructiveSql = false;
303
+ let inPipeline = false;
304
+ let segment = [];
305
+ for (const token of tokens) {
306
+ if (token.kind === "op" && SPLIT_OPS.has(token.value)) {
307
+ const cmd = commandNameFromSegment(segment);
308
+ if (inPipeline && DB_CLIENTS.has(cmd) && pipelineHasDestructiveSql) return true;
309
+ const continues = token.value === "|" || token.value === "|&";
310
+ if (continues) {
311
+ const segText = wordsOf(segment).join(" ");
312
+ pipelineHasDestructiveSql = pipelineHasDestructiveSql || DESTRUCTIVE_SQL.test(segText);
313
+ inPipeline = true;
314
+ } else {
315
+ pipelineHasDestructiveSql = false;
316
+ inPipeline = false;
317
+ }
318
+ segment = [];
319
+ continue;
320
+ }
321
+ segment.push(token);
322
+ }
323
+ const cmd = commandNameFromSegment(segment);
324
+ return inPipeline && pipelineHasDestructiveSql && DB_CLIENTS.has(cmd);
602
325
  }
603
326
 
604
327
  function executableTokenOf(seg) {
@@ -727,6 +450,7 @@ export function inspectBashCommand(command, depth) {
727
450
  if (knownDanger) return knownDanger;
728
451
  }
729
452
  if (hasDangerousPipeline(tokens)) return deny("curl-pipe", command);
453
+ if (hasDestructiveSqlPipeline(tokens)) return deny("destructive-sql", command);
730
454
  const pipelineHeredocHit = inspectPipelineHeredocs(tokens, depth);
731
455
  if (pipelineHeredocHit.deny || pipelineHeredocHit.ask) return pipelineHeredocHit;
732
456
 
@@ -780,6 +504,12 @@ export function inspectBashCommand(command, depth) {
780
504
  const nested = inspectInterpreterPayload(commandName, heredoc.body);
781
505
  if (nested.deny || nested.ask) return nested;
782
506
  }
507
+ } else if (DB_CLIENTS.has(commandName)) {
508
+ for (const heredoc of heredocs) {
509
+ if (DESTRUCTIVE_SQL.test(heredoc.body)) {
510
+ return deny("destructive-sql", heredoc.body);
511
+ }
512
+ }
783
513
  }
784
514
  }
785
515
 
@@ -822,97 +552,3 @@ export function inspectExecution(execution, config) {
822
552
  return allow();
823
553
  }
824
554
 
825
- export function sessionTag(execution) {
826
- const agent = execution && execution.agent;
827
- if (typeof agent === "string") return " (session " + agent + ")";
828
- if (agent && agent.session) return " (session " + String(agent.session) + ")";
829
- return "";
830
- }
831
-
832
- function safeSnippet(value) {
833
- return String(value || "")
834
- .replace(/[\r\n\t\u0000-\u001f]+/g, " ")
835
- .replace(/(authorization\s*:\s*(?:bearer|token)\s+)\S+/gi, "$1[redacted]")
836
- .replace(/((?:password|passwd|secret|token|api[_-]?key)\s*[:=]\s*)[^\s,;]+/gi, "$1[redacted]")
837
- .slice(0, 120);
838
- }
839
-
840
- function translated(translate, key, logger) {
841
- if (typeof translate === "function") {
842
- try {
843
- const value = translate(key);
844
- if (value && value !== key) return value;
845
- } catch {
846
- if (logger && typeof logger.warn === "function") {
847
- logger.warn("[dsh-approval-gate] localization lookup failed; using English fallback");
848
- }
849
- }
850
- }
851
- return MESSAGES.en[key] || key;
852
- }
853
-
854
- const RULE_KEYS = Object.freeze({
855
- "recursive-rm": "ruleRecursiveRm",
856
- "process-signal": "ruleProcessSignal",
857
- "service-control": "ruleServiceControl",
858
- "git-clean": "ruleGitClean",
859
- "git-reset-hard": "ruleGitResetHard",
860
- "find-delete": "ruleFindDelete",
861
- "destructive-sql": "ruleDestructiveSql",
862
- "secret-write": "ruleSecretWrite",
863
- "device-write": "ruleDeviceWrite",
864
- "mkfs-device": "ruleMkfsDevice",
865
- "curl-pipe": "ruleCurlPipe",
866
- "eval": "ruleEval",
867
- "nested-shell": "ruleNestedShell",
868
- "interpreter-payload": "ruleInterpreterPayload",
869
- });
870
-
871
- const ASK_KEYS = Object.freeze({
872
- unparseable: "askUnparseable",
873
- "unknown-format": "askUnknownFormat",
874
- "unknown-write-target": "askUnknownWriteTarget",
875
- "dynamic-command": "askDynamicCommand",
876
- "dynamic-arguments": "askDynamicArguments",
877
- "dynamic-redirect": "askDynamicRedirect",
878
- "script-file": "askScriptFile",
879
- "shell-stdin": "askShellStdin",
880
- "interpreter-payload": "askInterpreterPayload",
881
- "credential-in-argv": "askCredentialInArgv",
882
- });
883
-
884
- const DETAIL_KEYS = Object.freeze({
885
- "process-substitution": "detailProcessSubstitution",
886
- "unclosed-process-substitution": "detailUnclosedProcessSubstitution",
887
- "unclosed-command-substitution": "detailUnclosedCommandSubstitution",
888
- "unclosed-backtick-substitution": "detailUnclosedBacktick",
889
- "unclosed-parameter-expansion": "detailUnclosedParameterExpansion",
890
- "unclosed-single-quote": "detailUnclosedSingleQuote",
891
- "unclosed-double-quote": "detailUnclosedDoubleQuote",
892
- "dangling-backslash": "detailDanglingEscape",
893
- "invalid-heredoc-delimiter": "detailInvalidHeredoc",
894
- "unterminated-heredoc": "detailUnterminatedHeredoc",
895
- "unsupported-shell-grouping": "detailUnsupportedGrouping",
896
- });
897
-
898
- export function denyMessage(hit, who, translate, logger) {
899
- const key = RULE_KEYS[hit && hit.reason] || "ruleUnknown";
900
- const snippet = safeSnippet(hit && hit.snippet);
901
- const parts = [translated(translate, "blockedPrefix", logger), translated(translate, key, logger)];
902
- if (snippet) parts.push(translated(translate, "detectedLabel", logger) + ": " + snippet);
903
- if (who) parts.push(who.trim());
904
- return parts.join(" — ") + ".";
905
- }
906
-
907
- export function askMessage(hit, who, translate, logger) {
908
- const key = ASK_KEYS[hit && hit.reason] || "askUnknown";
909
- const snippet = safeSnippet(hit && hit.snippet);
910
- const parts = [translated(translate, "approvalRequired", logger), translated(translate, key, logger)];
911
- if (hit && hit.reason === "unparseable") {
912
- const detailKey = DETAIL_KEYS[hit.detail] || "detailUnsupported";
913
- parts.push(translated(translate, "parseReasonLabel", logger) + ": " + translated(translate, detailKey, logger));
914
- }
915
- if (snippet) parts.push(translated(translate, "commandLabel", logger) + ": " + snippet);
916
- if (who) parts.push(who.trim());
917
- return parts.join(" — ") + ".";
918
- }