@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/CHANGELOG.md +22 -1
- package/README.md +9 -3
- package/README.ru.md +11 -5
- package/README.zh.md +5 -4
- package/lib/bands.js +125 -0
- package/lib/breaker.js +97 -0
- package/lib/engine.js +108 -0
- package/lib/index.js +6 -0
- package/lib/inspect.js +59 -423
- package/lib/messages.js +140 -0
- package/lib/paths.js +25 -0
- package/lib/tokenizer.js +380 -0
- package/package.json +1 -1
package/lib/inspect.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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.
|
|
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
|
-
}
|