javi-forge 1.38.2 → 1.38.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.
|
@@ -177,6 +177,97 @@ function lex(command, powershell = false) {
|
|
|
177
177
|
if (!commands.at(-1).length) commands.pop();
|
|
178
178
|
return { commands, separators };
|
|
179
179
|
}
|
|
180
|
+
function hasUnquotedHereDocOperator(line) {
|
|
181
|
+
let quote = "", escaped = false;
|
|
182
|
+
for (let index = 0; index < line.length; index++) {
|
|
183
|
+
const char = line[index];
|
|
184
|
+
if (escaped) { escaped = false; continue; }
|
|
185
|
+
if (quote) {
|
|
186
|
+
if (char === "\\" && quote === '"') escaped = true;
|
|
187
|
+
else if (char === quote) quote = "";
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (char === "\\") { escaped = true; continue; }
|
|
191
|
+
if (char === "'" || char === '"') { quote = char; continue; }
|
|
192
|
+
if (char === "#" && (index === 0 || /\s/.test(line[index - 1]))) break;
|
|
193
|
+
if (char === "<" && line[index + 1] === "<" && line[index + 2] !== "<") return true;
|
|
194
|
+
}
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
const SHELL_HEADER_WHITESPACE = new Set([" ", "\t"]);
|
|
198
|
+
const STATIC_FILE_TARGET_CHARS = new Set("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._/-");
|
|
199
|
+
const HEREDOC_DELIMITER_START_CHARS = new Set("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_");
|
|
200
|
+
const HEREDOC_DELIMITER_CHARS = new Set("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_");
|
|
201
|
+
function isShellHeaderWhitespace(char) {
|
|
202
|
+
return SHELL_HEADER_WHITESPACE.has(char);
|
|
203
|
+
}
|
|
204
|
+
function skipShellWhitespace(line, index) {
|
|
205
|
+
while (isShellHeaderWhitespace(line[index])) index++;
|
|
206
|
+
return index;
|
|
207
|
+
}
|
|
208
|
+
function parseStaticFileTarget(line, index) {
|
|
209
|
+
const start = index;
|
|
210
|
+
while (index < line.length && !isShellHeaderWhitespace(line[index])) {
|
|
211
|
+
if (!STATIC_FILE_TARGET_CHARS.has(line[index])) return null;
|
|
212
|
+
index++;
|
|
213
|
+
}
|
|
214
|
+
return index === start ? null : { target: line.slice(start, index), index };
|
|
215
|
+
}
|
|
216
|
+
function parseQuotedHereDocDelimiter(line, index) {
|
|
217
|
+
const quote = line[index];
|
|
218
|
+
if (quote !== "'" && quote !== '"') return null;
|
|
219
|
+
const start = ++index;
|
|
220
|
+
while (index < line.length && line[index] !== quote) {
|
|
221
|
+
const char = line[index];
|
|
222
|
+
if (!(index === start ? HEREDOC_DELIMITER_START_CHARS : HEREDOC_DELIMITER_CHARS).has(char)) return null;
|
|
223
|
+
index++;
|
|
224
|
+
}
|
|
225
|
+
return index === start || line[index] !== quote ? null : { delimiter: line.slice(start, index), index: index + 1 };
|
|
226
|
+
}
|
|
227
|
+
function parseInertCatHereDocHeader(line) {
|
|
228
|
+
let index = skipShellWhitespace(line, 0);
|
|
229
|
+
if (line.slice(index, index + 3) !== "cat" || !(isShellHeaderWhitespace(line[index + 3]) || line[index + 3] === ">")) return null;
|
|
230
|
+
index = skipShellWhitespace(line, index + 3);
|
|
231
|
+
let target, delimiter;
|
|
232
|
+
if (line[index] === ">") {
|
|
233
|
+
const parsedTarget = parseStaticFileTarget(line, skipShellWhitespace(line, index + 1));
|
|
234
|
+
if (!parsedTarget) return null;
|
|
235
|
+
target = parsedTarget.target;
|
|
236
|
+
index = skipShellWhitespace(line, parsedTarget.index);
|
|
237
|
+
if (line[index] !== "<" || line[index + 1] !== "<" || line[index + 2] === "-") return null;
|
|
238
|
+
const parsedDelimiter = parseQuotedHereDocDelimiter(line, skipShellWhitespace(line, index + 2));
|
|
239
|
+
if (!parsedDelimiter) return null;
|
|
240
|
+
delimiter = parsedDelimiter.delimiter;
|
|
241
|
+
index = parsedDelimiter.index;
|
|
242
|
+
} else if (line[index] === "<" && line[index + 1] === "<" && line[index + 2] !== "-") {
|
|
243
|
+
const parsedDelimiter = parseQuotedHereDocDelimiter(line, skipShellWhitespace(line, index + 2));
|
|
244
|
+
if (!parsedDelimiter) return null;
|
|
245
|
+
delimiter = parsedDelimiter.delimiter;
|
|
246
|
+
index = skipShellWhitespace(line, parsedDelimiter.index);
|
|
247
|
+
if (line[index] !== ">") return null;
|
|
248
|
+
const parsedTarget = parseStaticFileTarget(line, skipShellWhitespace(line, index + 1));
|
|
249
|
+
if (!parsedTarget) return null;
|
|
250
|
+
target = parsedTarget.target;
|
|
251
|
+
index = parsedTarget.index;
|
|
252
|
+
} else return null;
|
|
253
|
+
return skipShellWhitespace(line, index) === line.length ? { target, delimiter } : null;
|
|
254
|
+
}
|
|
255
|
+
function opaqueInertCatHereDoc(command, cwd, config, projectRoot) {
|
|
256
|
+
// A continuation changes the lexical boundary which identifies a heredoc
|
|
257
|
+
// header. This bounded parser cannot prove it is body-only, so fail closed.
|
|
258
|
+
if (command.includes("\\" + "\n") || command.includes("\\" + "\r\n")) return null;
|
|
259
|
+
const lines = command.split(/\r?\n/);
|
|
260
|
+
if (lines.at(-1) === "") lines.pop();
|
|
261
|
+
const parsed = parseInertCatHereDocHeader(lines[0] ?? "");
|
|
262
|
+
if (parsed && lines.length >= 2) {
|
|
263
|
+
const terminator = lines.findIndex((line, index) => index > 0 && line === parsed.delimiter);
|
|
264
|
+
if (terminator === lines.length - 1) {
|
|
265
|
+
const target = lexicalizePolicyPath(parsed.target, { base: cwd, projectRoot });
|
|
266
|
+
return evaluateFile("Write", target, config, projectRoot).allowed ? lines[0] : null;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return lines.some(hasUnquotedHereDocOperator) ? null : command;
|
|
270
|
+
}
|
|
180
271
|
// The pre-redesign boolean helpers (parseEnvSplit/hasChmodRecursive/hasBase64Decode
|
|
181
272
|
// and their isLongPrefix/splitEnvString internals) were replaced by the semantic
|
|
182
273
|
// state machines below; only ENV_ESCAPES survives, shared with splitEnvSemantics.
|
|
@@ -703,6 +794,9 @@ function bashSubstitutions(command) {
|
|
|
703
794
|
}
|
|
704
795
|
function evaluateBash(command, cwd, config = AGENT_CONFIGS.claude, projectRoot = PROJECT_ROOT, depth = 0) {
|
|
705
796
|
if (depth > 4) return { allowed: false, ruleId: "shell.obfuscated-interpreter" };
|
|
797
|
+
const headerOnlyCommand = opaqueInertCatHereDoc(command, cwd, config, projectRoot);
|
|
798
|
+
if (headerOnlyCommand === null) return { allowed: false, ruleId: "shell.obfuscated-interpreter" };
|
|
799
|
+
command = headerOnlyCommand;
|
|
706
800
|
try { for (const body of bashSubstitutions(command)) { const nested = evaluateBash(body, cwd, config, projectRoot, depth + 1); if (!nested.allowed) return nested; } } catch { return { allowed: false, ruleId: "shell.obfuscated-interpreter" }; }
|
|
707
801
|
if (/^\s*:\s*\(\s*\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:\s*$/.test(command)) return { allowed: false, ruleId: "shell.destructive-root" };
|
|
708
802
|
let parsed;
|
|
@@ -4,14 +4,15 @@
|
|
|
4
4
|
"name": "javi-forge-skillguard-pre-tool-use.mjs",
|
|
5
5
|
"version": 1,
|
|
6
6
|
"policyVersion": 2,
|
|
7
|
-
"sha256": "
|
|
7
|
+
"sha256": "6edfbb31ce0551b27e38ae1ffd1daf9cc4bea54f2c86687c5124132af5b8c0af",
|
|
8
8
|
"historical": [
|
|
9
9
|
"78be7e6613c012280b7ad17886462ba166b63ebd031e34565d757b3a0796d7cc",
|
|
10
10
|
"5dc2a5c31131f4ac7d8657c78b950de52776aad6eaefe78ea0d764a9963c4425",
|
|
11
11
|
"0c9aa8fa26b389f4892782f83104f0792c2b71ebca39c18c02706e2185e22b40",
|
|
12
12
|
"3581862f0567cce75a58b693c9ade80d39ee7d58add11537a34a8461c47c1ed4",
|
|
13
13
|
"54a270f28b068450b79547a88ec6f2d4854514392fd5f38ed1d6174ea093d7aa",
|
|
14
|
-
"9a565cec31d9e091e3fb9420b86685f824733bc1ebe479f086b2b955aba6ef3e"
|
|
14
|
+
"9a565cec31d9e091e3fb9420b86685f824733bc1ebe479f086b2b955aba6ef3e",
|
|
15
|
+
"59fc4224975ad64cfc85bab50ec60d9bd4948070e9d41f42ab43e6d8231c19a1"
|
|
15
16
|
]
|
|
16
17
|
},
|
|
17
18
|
"settingsEntries": {
|