@node9/policy-engine 2.22.1 → 2.23.1
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/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +295 -26
- package/dist/index.mjs +295 -26
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -1351,7 +1351,7 @@ declare const LONG_OUTPUT_THRESHOLD_BYTES: number;
|
|
|
1351
1351
|
* and fails CI when the hash drifts without a version bump — forgetting
|
|
1352
1352
|
* is loud, not silent.
|
|
1353
1353
|
*/
|
|
1354
|
-
declare const CANONICAL_EXTRACTOR_VERSION = "canonical-
|
|
1354
|
+
declare const CANONICAL_EXTRACTOR_VERSION = "canonical-v18";
|
|
1355
1355
|
/**
|
|
1356
1356
|
* SHA-256 prefix of the detector-source files
|
|
1357
1357
|
* (canonical.ts + pii.ts + destructive-regex.ts).
|
|
@@ -1362,7 +1362,7 @@ declare const CANONICAL_EXTRACTOR_VERSION = "canonical-v17";
|
|
|
1362
1362
|
* files changed, this hash must change too, and you must consciously
|
|
1363
1363
|
* decide whether to bump CANONICAL_EXTRACTOR_VERSION."
|
|
1364
1364
|
*/
|
|
1365
|
-
declare const CANONICAL_EXTRACTOR_HASH = "
|
|
1365
|
+
declare const CANONICAL_EXTRACTOR_HASH = "5b2e76537f368d99";
|
|
1366
1366
|
declare function extractCanonicalFindings(call: ToolCallEntry, ctx: ExtractContext): CanonicalFinding[];
|
|
1367
1367
|
declare function extractSessionLevelFindings(calls: ReadonlyArray<SessionToolCall>, ctx: SessionExtractContext): CanonicalFinding[];
|
|
1368
1368
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -1351,7 +1351,7 @@ declare const LONG_OUTPUT_THRESHOLD_BYTES: number;
|
|
|
1351
1351
|
* and fails CI when the hash drifts without a version bump — forgetting
|
|
1352
1352
|
* is loud, not silent.
|
|
1353
1353
|
*/
|
|
1354
|
-
declare const CANONICAL_EXTRACTOR_VERSION = "canonical-
|
|
1354
|
+
declare const CANONICAL_EXTRACTOR_VERSION = "canonical-v18";
|
|
1355
1355
|
/**
|
|
1356
1356
|
* SHA-256 prefix of the detector-source files
|
|
1357
1357
|
* (canonical.ts + pii.ts + destructive-regex.ts).
|
|
@@ -1362,7 +1362,7 @@ declare const CANONICAL_EXTRACTOR_VERSION = "canonical-v17";
|
|
|
1362
1362
|
* files changed, this hash must change too, and you must consciously
|
|
1363
1363
|
* decide whether to bump CANONICAL_EXTRACTOR_VERSION."
|
|
1364
1364
|
*/
|
|
1365
|
-
declare const CANONICAL_EXTRACTOR_HASH = "
|
|
1365
|
+
declare const CANONICAL_EXTRACTOR_HASH = "5b2e76537f368d99";
|
|
1366
1366
|
declare function extractCanonicalFindings(call: ToolCallEntry, ctx: ExtractContext): CanonicalFinding[];
|
|
1367
1367
|
declare function extractSessionLevelFindings(calls: ReadonlyArray<SessionToolCall>, ctx: SessionExtractContext): CanonicalFinding[];
|
|
1368
1368
|
/**
|
package/dist/index.js
CHANGED
|
@@ -1373,6 +1373,7 @@ function normalizeCommandForPolicyImpl(command) {
|
|
|
1373
1373
|
const source = command.slice(s, e);
|
|
1374
1374
|
if (resolved === source) continue;
|
|
1375
1375
|
if (resolved === "" || /\s/.test(resolved)) continue;
|
|
1376
|
+
if (/^[;&|()<>]+$/.test(resolved)) continue;
|
|
1376
1377
|
rewrites.push([s, e, resolved]);
|
|
1377
1378
|
const quoteOnly = source.replace(/['"]/g, "");
|
|
1378
1379
|
if (quoteOnly !== source) quoteOnlyRewrites.push([s, e, quoteOnly]);
|
|
@@ -2392,6 +2393,60 @@ var NET_BINARIES = /* @__PURE__ */ new Set([
|
|
|
2392
2393
|
"rsync"
|
|
2393
2394
|
]);
|
|
2394
2395
|
var VALUE_FLAGS = {
|
|
2396
|
+
// rsync 3.2.7, its own --help: every flag whose operand could be mistaken for
|
|
2397
|
+
// a host. `-e ssh` is the one that matters most (`ssh` is not the destination).
|
|
2398
|
+
rsync: /* @__PURE__ */ new Set([
|
|
2399
|
+
"-e",
|
|
2400
|
+
"--rsh",
|
|
2401
|
+
"-f",
|
|
2402
|
+
"--filter",
|
|
2403
|
+
"-T",
|
|
2404
|
+
"--temp-dir",
|
|
2405
|
+
"-B",
|
|
2406
|
+
"--block-size",
|
|
2407
|
+
"-M",
|
|
2408
|
+
"--remote-option",
|
|
2409
|
+
"--exclude",
|
|
2410
|
+
"--exclude-from",
|
|
2411
|
+
"--include",
|
|
2412
|
+
"--include-from",
|
|
2413
|
+
"--files-from",
|
|
2414
|
+
"--compare-dest",
|
|
2415
|
+
"--copy-dest",
|
|
2416
|
+
"--link-dest",
|
|
2417
|
+
"--partial-dir",
|
|
2418
|
+
"--log-file",
|
|
2419
|
+
"--password-file",
|
|
2420
|
+
"--bwlimit",
|
|
2421
|
+
"--timeout",
|
|
2422
|
+
"--contimeout",
|
|
2423
|
+
"--port",
|
|
2424
|
+
"--sockopts",
|
|
2425
|
+
"--address",
|
|
2426
|
+
"--chmod",
|
|
2427
|
+
"--chown",
|
|
2428
|
+
"--max-size",
|
|
2429
|
+
"--min-size",
|
|
2430
|
+
"--modify-window",
|
|
2431
|
+
"--out-format",
|
|
2432
|
+
"--log-file-format",
|
|
2433
|
+
"--backup-dir",
|
|
2434
|
+
"--suffix",
|
|
2435
|
+
"--iconv",
|
|
2436
|
+
"--max-delete",
|
|
2437
|
+
"--checksum-choice",
|
|
2438
|
+
"--info",
|
|
2439
|
+
"--debug",
|
|
2440
|
+
"--stderr",
|
|
2441
|
+
"--outbuf",
|
|
2442
|
+
"--skip-compress",
|
|
2443
|
+
"--usermap",
|
|
2444
|
+
"--groupmap",
|
|
2445
|
+
"--mkpath",
|
|
2446
|
+
"--write-batch",
|
|
2447
|
+
"--read-batch",
|
|
2448
|
+
"--only-write-batch"
|
|
2449
|
+
]),
|
|
2395
2450
|
curl: /* @__PURE__ */ new Set([
|
|
2396
2451
|
"-d",
|
|
2397
2452
|
"--data",
|
|
@@ -2475,23 +2530,163 @@ var VALUE_FLAGS = {
|
|
|
2475
2530
|
]),
|
|
2476
2531
|
nc: /* @__PURE__ */ new Set(["-p", "-s", "-w", "-X", "-x", "-e", "-g", "-G", "-i", "-O", "-T", "-q", "-m"])
|
|
2477
2532
|
};
|
|
2533
|
+
var HOME_VARIABLES = /* @__PURE__ */ new Set(["HOME", "USERPROFILE"]);
|
|
2534
|
+
var assignmentTable = null;
|
|
2535
|
+
var currentStmtOffset = Number.MAX_SAFE_INTEGER;
|
|
2536
|
+
var PAYLOAD_BUDGET = 256;
|
|
2537
|
+
var payloadBudget = 0;
|
|
2538
|
+
var seenPayloads = null;
|
|
2539
|
+
function payloadKey(payload) {
|
|
2540
|
+
if (!assignmentTable || assignmentTable.size === 0) return payload;
|
|
2541
|
+
const bindings = [];
|
|
2542
|
+
for (const [name, rec] of assignmentTable) {
|
|
2543
|
+
if (rec.value !== null) bindings.push(`${name}=${rec.value}`);
|
|
2544
|
+
}
|
|
2545
|
+
return `${payload}\0${bindings.sort().join("")}`;
|
|
2546
|
+
}
|
|
2547
|
+
function claimPayload(payload) {
|
|
2548
|
+
if (!seenPayloads) return "ok";
|
|
2549
|
+
const key = payloadKey(payload);
|
|
2550
|
+
if (seenPayloads.has(key)) return "seen";
|
|
2551
|
+
if (payloadBudget <= 0) return "exhausted";
|
|
2552
|
+
payloadBudget--;
|
|
2553
|
+
seenPayloads.add(key);
|
|
2554
|
+
return "ok";
|
|
2555
|
+
}
|
|
2556
|
+
var UNANALYSABLE_NESTING = {
|
|
2557
|
+
ruleName: "review-unanalysable-nesting",
|
|
2558
|
+
verdict: "review",
|
|
2559
|
+
reason: "This command nests more wrapped shell payloads than the policy engine will unwrap, so some of what it runs was not read.",
|
|
2560
|
+
path: ""
|
|
2561
|
+
};
|
|
2562
|
+
var ASSIGNMENT_HEADS = /* @__PURE__ */ new Set(["export", "declare", "local", "readonly", "typeset"]);
|
|
2563
|
+
function recordTopLevelAssignments(f) {
|
|
2564
|
+
const stmts = Array.isArray(f?.Stmts) ? f.Stmts : [];
|
|
2565
|
+
for (const stmt of stmts) recordTopLevelStmt(stmt);
|
|
2566
|
+
}
|
|
2567
|
+
function probeBinOp(src) {
|
|
2568
|
+
try {
|
|
2569
|
+
const cmd = syntax.NewParser().Parse(src, "probe")?.Stmts?.[0]?.Cmd;
|
|
2570
|
+
if (cmd && syntax.NodeType(cmd) === "BinaryCmd") return cmd.Op;
|
|
2571
|
+
} catch {
|
|
2572
|
+
}
|
|
2573
|
+
return null;
|
|
2574
|
+
}
|
|
2575
|
+
var AND_OP = probeBinOp("a && b");
|
|
2576
|
+
var OR_OP = probeBinOp("a || b");
|
|
2577
|
+
var AND_OR_OPS = new Set([AND_OP, OR_OP].filter((o) => o !== null));
|
|
2578
|
+
function recordTopLevelStmt(stmt) {
|
|
2579
|
+
if (!stmt || !stmt.Cmd) return false;
|
|
2580
|
+
const t = syntax.NodeType(stmt.Cmd);
|
|
2581
|
+
if (t === "BinaryCmd") {
|
|
2582
|
+
if (!AND_OR_OPS.has(stmt.Cmd.Op)) return false;
|
|
2583
|
+
if (!recordTopLevelStmt(stmt.Cmd.X)) return false;
|
|
2584
|
+
if (stmt.Cmd.Op === AND_OP) return recordTopLevelStmt(stmt.Cmd.Y);
|
|
2585
|
+
return true;
|
|
2586
|
+
}
|
|
2587
|
+
if (t !== "CallExpr" && t !== "DeclClause") return false;
|
|
2588
|
+
let at = 0;
|
|
2589
|
+
try {
|
|
2590
|
+
at = stmt.Pos().Offset();
|
|
2591
|
+
} catch {
|
|
2592
|
+
at = 0;
|
|
2593
|
+
}
|
|
2594
|
+
recordAssignments(stmt.Cmd, at);
|
|
2595
|
+
if (stmt.Negated) return false;
|
|
2596
|
+
if (t === "DeclClause") return ASSIGNMENT_HEADS.has(stmt.Cmd.Variant?.Value ?? "");
|
|
2597
|
+
return (stmt.Cmd.Args || []).length === 0 && (stmt.Cmd.Assigns || []).length > 0;
|
|
2598
|
+
}
|
|
2599
|
+
function recordAssignments(n, at) {
|
|
2600
|
+
if (!assignmentTable) return;
|
|
2601
|
+
const t = syntax.NodeType(n);
|
|
2602
|
+
let assigns = [];
|
|
2603
|
+
if (t === "CallExpr") {
|
|
2604
|
+
if ((n.Args || []).length > 0) return;
|
|
2605
|
+
assigns = n.Assigns || [];
|
|
2606
|
+
} else if (t === "DeclClause") {
|
|
2607
|
+
if (!ASSIGNMENT_HEADS.has(n.Variant?.Value ?? "")) return;
|
|
2608
|
+
assigns = (n.Args || []).filter((a) => syntax.NodeType(a) === "Assign");
|
|
2609
|
+
} else return;
|
|
2610
|
+
for (const a of assigns) {
|
|
2611
|
+
const name = a?.Name?.Value;
|
|
2612
|
+
if (!name || !a.Value || a.Append) continue;
|
|
2613
|
+
assignmentTable.set(name, { value: resolveWordLiteral(a.Value), at });
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
function resolveTrivialSubst(part) {
|
|
2617
|
+
if (syntax.NodeType(part) !== "CmdSubst") return void 0;
|
|
2618
|
+
const stmts = part.Stmts || [];
|
|
2619
|
+
if (stmts.length !== 1) return void 0;
|
|
2620
|
+
const st = stmts[0];
|
|
2621
|
+
if ((st.Redirs || []).length > 0 || st.Negated || st.Background) return void 0;
|
|
2622
|
+
const cmd = st.Cmd;
|
|
2623
|
+
if (!cmd || syntax.NodeType(cmd) !== "CallExpr") return void 0;
|
|
2624
|
+
if ((cmd.Assigns || []).length > 0) return void 0;
|
|
2625
|
+
const words = (cmd.Args || []).map((a) => resolveWordLiteral(a));
|
|
2626
|
+
if (words.length === 0 || words.some((w) => w === null)) return void 0;
|
|
2627
|
+
const head = baseWord(words[0]);
|
|
2628
|
+
const rest = words.slice(1);
|
|
2629
|
+
if (head === "echo") {
|
|
2630
|
+
let i = 0;
|
|
2631
|
+
while (i < rest.length && /^-[neE]+$/.test(rest[i])) i++;
|
|
2632
|
+
return rest.slice(i).join(" ");
|
|
2633
|
+
}
|
|
2634
|
+
if (head === "printf") {
|
|
2635
|
+
if (rest.length !== 2) return void 0;
|
|
2636
|
+
if (!/^%s(\\n)?$/.test(rest[0])) return void 0;
|
|
2637
|
+
return rest[1];
|
|
2638
|
+
}
|
|
2639
|
+
return void 0;
|
|
2640
|
+
}
|
|
2641
|
+
function recordedExpansion(name) {
|
|
2642
|
+
if (!assignmentTable || !name) return void 0;
|
|
2643
|
+
const rec = assignmentTable.get(name);
|
|
2644
|
+
if (rec === void 0 || rec.at >= currentStmtOffset) return void 0;
|
|
2645
|
+
return rec.value;
|
|
2646
|
+
}
|
|
2647
|
+
function isPlainParam(p) {
|
|
2648
|
+
if (syntax.NodeType(p) !== "ParamExp") return false;
|
|
2649
|
+
return !(p.Excl || p.Length || p.Width || p.Index || p.Slice || p.Repl || p.Exp);
|
|
2650
|
+
}
|
|
2651
|
+
function expandPlainParam(p) {
|
|
2652
|
+
if (!assignmentTable) return void 0;
|
|
2653
|
+
if (!isPlainParam(p)) return void 0;
|
|
2654
|
+
const recorded = recordedExpansion(p.Param?.Value);
|
|
2655
|
+
if (recorded !== void 0) return recorded;
|
|
2656
|
+
return HOME_VARIABLES.has(p.Param?.Value) ? "~" : void 0;
|
|
2657
|
+
}
|
|
2478
2658
|
function resolveWordLiteral(w) {
|
|
2479
2659
|
const parts = w?.Parts || [];
|
|
2480
2660
|
let s = "";
|
|
2481
2661
|
for (const p of parts) {
|
|
2482
|
-
const
|
|
2483
|
-
if (
|
|
2484
|
-
|
|
2485
|
-
else if (t === "DblQuoted") {
|
|
2486
|
-
const inner = p.Parts || [];
|
|
2487
|
-
if (!inner.every((ip) => syntax.NodeType(ip) === "Lit")) return null;
|
|
2488
|
-
s += inner.map((ip) => ip.Value ?? "").join("");
|
|
2489
|
-
} else {
|
|
2490
|
-
return null;
|
|
2491
|
-
}
|
|
2662
|
+
const piece = resolvePart(p, false);
|
|
2663
|
+
if (piece === void 0 || piece === null) return null;
|
|
2664
|
+
s += piece;
|
|
2492
2665
|
}
|
|
2493
2666
|
return s;
|
|
2494
2667
|
}
|
|
2668
|
+
function resolvePart(p, inQuotes) {
|
|
2669
|
+
const t = syntax.NodeType(p);
|
|
2670
|
+
if (t === "Lit") {
|
|
2671
|
+
const raw = p.Value ?? "";
|
|
2672
|
+
if (!inQuotes) return raw.replace(/\\(.)/g, "$1");
|
|
2673
|
+
return assignmentTable ? raw.replace(/\\([$`"\\])/g, "$1") : raw;
|
|
2674
|
+
}
|
|
2675
|
+
if (t === "SglQuoted") return p.Value ?? "";
|
|
2676
|
+
if (t === "ParamExp") return expandPlainParam(p);
|
|
2677
|
+
if (t === "CmdSubst") return assignmentTable ? resolveTrivialSubst(p) : void 0;
|
|
2678
|
+
if (t === "DblQuoted" && !inQuotes) {
|
|
2679
|
+
const inner = p.Parts || [];
|
|
2680
|
+
let out = "";
|
|
2681
|
+
for (const ip of inner) {
|
|
2682
|
+
const piece = resolvePart(ip, true);
|
|
2683
|
+
if (piece === void 0 || piece === null) return piece;
|
|
2684
|
+
out += piece;
|
|
2685
|
+
}
|
|
2686
|
+
return out;
|
|
2687
|
+
}
|
|
2688
|
+
return void 0;
|
|
2689
|
+
}
|
|
2495
2690
|
function parseDestHost(token) {
|
|
2496
2691
|
if (!token) return null;
|
|
2497
2692
|
let t = token.trim();
|
|
@@ -2546,6 +2741,7 @@ function destTokensForBinary(binary, args) {
|
|
|
2546
2741
|
case "ssh":
|
|
2547
2742
|
return positionals.slice(0, 1);
|
|
2548
2743
|
case "scp":
|
|
2744
|
+
case "rsync":
|
|
2549
2745
|
return positionals.filter((p) => p.includes(":") || p.includes("@"));
|
|
2550
2746
|
case "nc":
|
|
2551
2747
|
case "ncat":
|
|
@@ -2746,17 +2942,35 @@ function analyzeFsOperationImpl(command, depth = 0) {
|
|
|
2746
2942
|
const f = parseShared(command);
|
|
2747
2943
|
if (f === PARSE_FAIL) return null;
|
|
2748
2944
|
let result = null;
|
|
2945
|
+
const outerTable = assignmentTable;
|
|
2946
|
+
const outerOffset = currentStmtOffset;
|
|
2947
|
+
const outerSeen = seenPayloads;
|
|
2948
|
+
const outerBudget = payloadBudget;
|
|
2949
|
+
if (depth === 0) {
|
|
2950
|
+
seenPayloads = /* @__PURE__ */ new Set();
|
|
2951
|
+
payloadBudget = PAYLOAD_BUDGET;
|
|
2952
|
+
}
|
|
2953
|
+
assignmentTable = new Map(
|
|
2954
|
+
[...outerTable ?? []].map(([k, r]) => [k, { value: r.value, at: -1 }])
|
|
2955
|
+
);
|
|
2956
|
+
currentStmtOffset = Number.MAX_SAFE_INTEGER;
|
|
2749
2957
|
try {
|
|
2958
|
+
recordTopLevelAssignments(f);
|
|
2750
2959
|
syntax.Walk(f, (node) => {
|
|
2751
2960
|
if (!node || result?.verdict === "block") return false;
|
|
2752
2961
|
const n = node;
|
|
2753
2962
|
const nodeType = syntax.NodeType(n);
|
|
2754
2963
|
if (nodeType === "Stmt") {
|
|
2964
|
+
try {
|
|
2965
|
+
currentStmtOffset = n.Pos().Offset();
|
|
2966
|
+
} catch {
|
|
2967
|
+
currentStmtOffset = Number.MAX_SAFE_INTEGER;
|
|
2968
|
+
}
|
|
2755
2969
|
result = stricter(result, jailedRedirectRead(n));
|
|
2756
2970
|
return result?.verdict !== "block";
|
|
2757
2971
|
}
|
|
2758
2972
|
if (nodeType !== "CallExpr") return true;
|
|
2759
|
-
const { name, flags, paths, words
|
|
2973
|
+
const { name, flags, paths, words } = extractLiteralArgs(n);
|
|
2760
2974
|
if (!name) return true;
|
|
2761
2975
|
if (name === "rm") {
|
|
2762
2976
|
const flagStr = flags.join("").toLowerCase();
|
|
@@ -2785,9 +2999,30 @@ function analyzeFsOperationImpl(command, depth = 0) {
|
|
|
2785
2999
|
}
|
|
2786
3000
|
}
|
|
2787
3001
|
}
|
|
2788
|
-
if (depth <
|
|
3002
|
+
if (depth < 24 && name === "find") {
|
|
3003
|
+
for (const action of findActions(words, 0)) {
|
|
3004
|
+
const h = unwrapCommandHead(action);
|
|
3005
|
+
const inner = literalShellPayload(action.slice(h), baseWord(action[h]));
|
|
3006
|
+
if (inner === null) continue;
|
|
3007
|
+
const claim = claimPayload(inner);
|
|
3008
|
+
if (claim === "exhausted") {
|
|
3009
|
+
result = stricter(result, UNANALYSABLE_NESTING);
|
|
3010
|
+
continue;
|
|
3011
|
+
}
|
|
3012
|
+
if (claim === "seen") continue;
|
|
3013
|
+
const v = analyzeFsOperationImpl(inner, depth + 1);
|
|
3014
|
+
result = stricter(result, v);
|
|
3015
|
+
if (result?.verdict === "block") return false;
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
if (depth < 24) {
|
|
2789
3019
|
const payload = literalShellPayload(words, name);
|
|
2790
|
-
|
|
3020
|
+
const claim = payload === null ? "seen" : claimPayload(payload);
|
|
3021
|
+
if (claim === "exhausted") {
|
|
3022
|
+
result = stricter(result, UNANALYSABLE_NESTING);
|
|
3023
|
+
return true;
|
|
3024
|
+
}
|
|
3025
|
+
if (payload !== null && claim === "ok") {
|
|
2791
3026
|
const inner = analyzeFsOperationImpl(payload, depth + 1);
|
|
2792
3027
|
if (inner) {
|
|
2793
3028
|
result = inner;
|
|
@@ -2796,7 +3031,7 @@ function analyzeFsOperationImpl(command, depth = 0) {
|
|
|
2796
3031
|
return true;
|
|
2797
3032
|
}
|
|
2798
3033
|
}
|
|
2799
|
-
const readPaths = FS_READ_TOOLS.has(name) ?
|
|
3034
|
+
const readPaths = FS_READ_TOOLS.has(name) ? readerPaths(words, 0) : wrappedReadPaths(words, name);
|
|
2800
3035
|
if (readPaths) {
|
|
2801
3036
|
for (const p of readPaths) {
|
|
2802
3037
|
result = stricter(result, matchSensitivePath2(p));
|
|
@@ -2811,6 +3046,11 @@ function analyzeFsOperationImpl(command, depth = 0) {
|
|
|
2811
3046
|
return result;
|
|
2812
3047
|
} catch {
|
|
2813
3048
|
return null;
|
|
3049
|
+
} finally {
|
|
3050
|
+
assignmentTable = outerTable;
|
|
3051
|
+
currentStmtOffset = outerOffset;
|
|
3052
|
+
seenPayloads = outerSeen;
|
|
3053
|
+
if (depth === 0) payloadBudget = outerBudget;
|
|
2814
3054
|
}
|
|
2815
3055
|
}
|
|
2816
3056
|
function stricter(a, b) {
|
|
@@ -2868,6 +3108,18 @@ function resolveCopyShape(words, h) {
|
|
|
2868
3108
|
return null;
|
|
2869
3109
|
}
|
|
2870
3110
|
var FIND_OPTIONS = /* @__PURE__ */ new Set(["-H", "-L", "-P"]);
|
|
3111
|
+
function findAction(words, k) {
|
|
3112
|
+
const end = words.findIndex((w, i) => i > k && (w === ";" || w === "+"));
|
|
3113
|
+
return words.slice(k + 1, end < 0 ? words.length : end);
|
|
3114
|
+
}
|
|
3115
|
+
function findActions(words, h) {
|
|
3116
|
+
const out = [];
|
|
3117
|
+
for (let i = h + 1; i < words.length; i++) {
|
|
3118
|
+
const w = words[i];
|
|
3119
|
+
if (w !== null && FIND_EXEC_FLAGS.has(w)) out.push(findAction(words, i));
|
|
3120
|
+
}
|
|
3121
|
+
return out;
|
|
3122
|
+
}
|
|
2871
3123
|
function findStartPoints(words, h) {
|
|
2872
3124
|
const k = words.findIndex((w, i) => i > h && w !== null && FIND_EXEC_FLAGS.has(w));
|
|
2873
3125
|
if (k < 0) return { k, starts: [] };
|
|
@@ -2883,8 +3135,13 @@ function copySourcePaths(words) {
|
|
|
2883
3135
|
if (fi >= 0) {
|
|
2884
3136
|
const { k, starts } = findStartPoints(words, fi);
|
|
2885
3137
|
if (k < 0) return [];
|
|
2886
|
-
const
|
|
2887
|
-
|
|
3138
|
+
const out = [];
|
|
3139
|
+
for (const action of findActions(words, fi)) {
|
|
3140
|
+
const h2 = unwrapCommandHead(action);
|
|
3141
|
+
if (!resolveCopyShape(action, h2)) continue;
|
|
3142
|
+
out.push(...starts, ...copySourcePaths(action));
|
|
3143
|
+
}
|
|
3144
|
+
return out;
|
|
2888
3145
|
}
|
|
2889
3146
|
if (!COPY_VERB_HEADS.has(baseWord(words[h]))) return [];
|
|
2890
3147
|
const r = resolveCopyShape(words, h);
|
|
@@ -3088,6 +3345,15 @@ function flagEffect(token, shape, known) {
|
|
|
3088
3345
|
}
|
|
3089
3346
|
return NONE;
|
|
3090
3347
|
}
|
|
3348
|
+
function readerPaths(words, h) {
|
|
3349
|
+
const head = baseWord(words[h]);
|
|
3350
|
+
const from = h + 1;
|
|
3351
|
+
const flags = words.slice(from).filter((w) => w !== null && w.startsWith("-"));
|
|
3352
|
+
return [
|
|
3353
|
+
...readTargets(head, positionedArgs(words, from), flags, words, from),
|
|
3354
|
+
...flagOperandFiles(head, words, from)
|
|
3355
|
+
];
|
|
3356
|
+
}
|
|
3091
3357
|
function readTargets(verb, args, flags, words = [], from = 1) {
|
|
3092
3358
|
const shape = PATTERN_VERBS[verb];
|
|
3093
3359
|
if (!shape) return args.map((a) => a.value);
|
|
@@ -3169,18 +3435,21 @@ function flagOperandFiles(verb, words, from) {
|
|
|
3169
3435
|
function wrappedReadPaths(words, name) {
|
|
3170
3436
|
if (name === "find") {
|
|
3171
3437
|
const { k, starts } = findStartPoints(words, 0);
|
|
3172
|
-
|
|
3438
|
+
if (k < 0) return null;
|
|
3439
|
+
const paths = [];
|
|
3440
|
+
let reads = false;
|
|
3441
|
+
for (const action of findActions(words, 0)) {
|
|
3442
|
+
const h2 = unwrapCommandHead(action);
|
|
3443
|
+
if (!isReaderWord(action[h2] ?? null)) continue;
|
|
3444
|
+
reads = true;
|
|
3445
|
+
paths.push(...readerPaths(action, h2));
|
|
3446
|
+
}
|
|
3447
|
+
return reads ? [...starts, ...paths] : paths;
|
|
3173
3448
|
}
|
|
3174
3449
|
if (!COMMAND_WRAPPERS.has(name) && !RUNNER_WRAPPERS.has(name)) return null;
|
|
3175
3450
|
const h = unwrapCommandHead(words);
|
|
3176
3451
|
if (h <= 0 || !isReaderWord(words[h] ?? null)) return null;
|
|
3177
|
-
|
|
3178
|
-
const rest = words.slice(h + 1);
|
|
3179
|
-
const restFlags = rest.filter((w) => w !== null && w.startsWith("-"));
|
|
3180
|
-
return [
|
|
3181
|
-
...readTargets(head, positionedArgs(words, h + 1), restFlags, words, h + 1),
|
|
3182
|
-
...flagOperandFiles(head, words, h + 1)
|
|
3183
|
-
];
|
|
3452
|
+
return readerPaths(words, h);
|
|
3184
3453
|
}
|
|
3185
3454
|
function literalShellPayload(words, name) {
|
|
3186
3455
|
const h = COMMAND_WRAPPERS.has(name) || RUNNER_WRAPPERS.has(name) ? unwrapCommandHead(words) : 0;
|
|
@@ -5789,8 +6058,8 @@ function matchCanaryArgs(args, values) {
|
|
|
5789
6058
|
|
|
5790
6059
|
// src/scan/canonical.ts
|
|
5791
6060
|
var LONG_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
|
|
5792
|
-
var CANONICAL_EXTRACTOR_VERSION = "canonical-
|
|
5793
|
-
var CANONICAL_EXTRACTOR_HASH = "
|
|
6061
|
+
var CANONICAL_EXTRACTOR_VERSION = "canonical-v18";
|
|
6062
|
+
var CANONICAL_EXTRACTOR_HASH = "5b2e76537f368d99";
|
|
5794
6063
|
var DEDUPE_PREVIEW_LEN = 120;
|
|
5795
6064
|
function extractCanonicalFindings(call, ctx) {
|
|
5796
6065
|
const out = [];
|
package/dist/index.mjs
CHANGED
|
@@ -1231,6 +1231,7 @@ function normalizeCommandForPolicyImpl(command) {
|
|
|
1231
1231
|
const source = command.slice(s, e);
|
|
1232
1232
|
if (resolved === source) continue;
|
|
1233
1233
|
if (resolved === "" || /\s/.test(resolved)) continue;
|
|
1234
|
+
if (/^[;&|()<>]+$/.test(resolved)) continue;
|
|
1234
1235
|
rewrites.push([s, e, resolved]);
|
|
1235
1236
|
const quoteOnly = source.replace(/['"]/g, "");
|
|
1236
1237
|
if (quoteOnly !== source) quoteOnlyRewrites.push([s, e, quoteOnly]);
|
|
@@ -2250,6 +2251,60 @@ var NET_BINARIES = /* @__PURE__ */ new Set([
|
|
|
2250
2251
|
"rsync"
|
|
2251
2252
|
]);
|
|
2252
2253
|
var VALUE_FLAGS = {
|
|
2254
|
+
// rsync 3.2.7, its own --help: every flag whose operand could be mistaken for
|
|
2255
|
+
// a host. `-e ssh` is the one that matters most (`ssh` is not the destination).
|
|
2256
|
+
rsync: /* @__PURE__ */ new Set([
|
|
2257
|
+
"-e",
|
|
2258
|
+
"--rsh",
|
|
2259
|
+
"-f",
|
|
2260
|
+
"--filter",
|
|
2261
|
+
"-T",
|
|
2262
|
+
"--temp-dir",
|
|
2263
|
+
"-B",
|
|
2264
|
+
"--block-size",
|
|
2265
|
+
"-M",
|
|
2266
|
+
"--remote-option",
|
|
2267
|
+
"--exclude",
|
|
2268
|
+
"--exclude-from",
|
|
2269
|
+
"--include",
|
|
2270
|
+
"--include-from",
|
|
2271
|
+
"--files-from",
|
|
2272
|
+
"--compare-dest",
|
|
2273
|
+
"--copy-dest",
|
|
2274
|
+
"--link-dest",
|
|
2275
|
+
"--partial-dir",
|
|
2276
|
+
"--log-file",
|
|
2277
|
+
"--password-file",
|
|
2278
|
+
"--bwlimit",
|
|
2279
|
+
"--timeout",
|
|
2280
|
+
"--contimeout",
|
|
2281
|
+
"--port",
|
|
2282
|
+
"--sockopts",
|
|
2283
|
+
"--address",
|
|
2284
|
+
"--chmod",
|
|
2285
|
+
"--chown",
|
|
2286
|
+
"--max-size",
|
|
2287
|
+
"--min-size",
|
|
2288
|
+
"--modify-window",
|
|
2289
|
+
"--out-format",
|
|
2290
|
+
"--log-file-format",
|
|
2291
|
+
"--backup-dir",
|
|
2292
|
+
"--suffix",
|
|
2293
|
+
"--iconv",
|
|
2294
|
+
"--max-delete",
|
|
2295
|
+
"--checksum-choice",
|
|
2296
|
+
"--info",
|
|
2297
|
+
"--debug",
|
|
2298
|
+
"--stderr",
|
|
2299
|
+
"--outbuf",
|
|
2300
|
+
"--skip-compress",
|
|
2301
|
+
"--usermap",
|
|
2302
|
+
"--groupmap",
|
|
2303
|
+
"--mkpath",
|
|
2304
|
+
"--write-batch",
|
|
2305
|
+
"--read-batch",
|
|
2306
|
+
"--only-write-batch"
|
|
2307
|
+
]),
|
|
2253
2308
|
curl: /* @__PURE__ */ new Set([
|
|
2254
2309
|
"-d",
|
|
2255
2310
|
"--data",
|
|
@@ -2333,23 +2388,163 @@ var VALUE_FLAGS = {
|
|
|
2333
2388
|
]),
|
|
2334
2389
|
nc: /* @__PURE__ */ new Set(["-p", "-s", "-w", "-X", "-x", "-e", "-g", "-G", "-i", "-O", "-T", "-q", "-m"])
|
|
2335
2390
|
};
|
|
2391
|
+
var HOME_VARIABLES = /* @__PURE__ */ new Set(["HOME", "USERPROFILE"]);
|
|
2392
|
+
var assignmentTable = null;
|
|
2393
|
+
var currentStmtOffset = Number.MAX_SAFE_INTEGER;
|
|
2394
|
+
var PAYLOAD_BUDGET = 256;
|
|
2395
|
+
var payloadBudget = 0;
|
|
2396
|
+
var seenPayloads = null;
|
|
2397
|
+
function payloadKey(payload) {
|
|
2398
|
+
if (!assignmentTable || assignmentTable.size === 0) return payload;
|
|
2399
|
+
const bindings = [];
|
|
2400
|
+
for (const [name, rec] of assignmentTable) {
|
|
2401
|
+
if (rec.value !== null) bindings.push(`${name}=${rec.value}`);
|
|
2402
|
+
}
|
|
2403
|
+
return `${payload}\0${bindings.sort().join("")}`;
|
|
2404
|
+
}
|
|
2405
|
+
function claimPayload(payload) {
|
|
2406
|
+
if (!seenPayloads) return "ok";
|
|
2407
|
+
const key = payloadKey(payload);
|
|
2408
|
+
if (seenPayloads.has(key)) return "seen";
|
|
2409
|
+
if (payloadBudget <= 0) return "exhausted";
|
|
2410
|
+
payloadBudget--;
|
|
2411
|
+
seenPayloads.add(key);
|
|
2412
|
+
return "ok";
|
|
2413
|
+
}
|
|
2414
|
+
var UNANALYSABLE_NESTING = {
|
|
2415
|
+
ruleName: "review-unanalysable-nesting",
|
|
2416
|
+
verdict: "review",
|
|
2417
|
+
reason: "This command nests more wrapped shell payloads than the policy engine will unwrap, so some of what it runs was not read.",
|
|
2418
|
+
path: ""
|
|
2419
|
+
};
|
|
2420
|
+
var ASSIGNMENT_HEADS = /* @__PURE__ */ new Set(["export", "declare", "local", "readonly", "typeset"]);
|
|
2421
|
+
function recordTopLevelAssignments(f) {
|
|
2422
|
+
const stmts = Array.isArray(f?.Stmts) ? f.Stmts : [];
|
|
2423
|
+
for (const stmt of stmts) recordTopLevelStmt(stmt);
|
|
2424
|
+
}
|
|
2425
|
+
function probeBinOp(src) {
|
|
2426
|
+
try {
|
|
2427
|
+
const cmd = syntax.NewParser().Parse(src, "probe")?.Stmts?.[0]?.Cmd;
|
|
2428
|
+
if (cmd && syntax.NodeType(cmd) === "BinaryCmd") return cmd.Op;
|
|
2429
|
+
} catch {
|
|
2430
|
+
}
|
|
2431
|
+
return null;
|
|
2432
|
+
}
|
|
2433
|
+
var AND_OP = probeBinOp("a && b");
|
|
2434
|
+
var OR_OP = probeBinOp("a || b");
|
|
2435
|
+
var AND_OR_OPS = new Set([AND_OP, OR_OP].filter((o) => o !== null));
|
|
2436
|
+
function recordTopLevelStmt(stmt) {
|
|
2437
|
+
if (!stmt || !stmt.Cmd) return false;
|
|
2438
|
+
const t = syntax.NodeType(stmt.Cmd);
|
|
2439
|
+
if (t === "BinaryCmd") {
|
|
2440
|
+
if (!AND_OR_OPS.has(stmt.Cmd.Op)) return false;
|
|
2441
|
+
if (!recordTopLevelStmt(stmt.Cmd.X)) return false;
|
|
2442
|
+
if (stmt.Cmd.Op === AND_OP) return recordTopLevelStmt(stmt.Cmd.Y);
|
|
2443
|
+
return true;
|
|
2444
|
+
}
|
|
2445
|
+
if (t !== "CallExpr" && t !== "DeclClause") return false;
|
|
2446
|
+
let at = 0;
|
|
2447
|
+
try {
|
|
2448
|
+
at = stmt.Pos().Offset();
|
|
2449
|
+
} catch {
|
|
2450
|
+
at = 0;
|
|
2451
|
+
}
|
|
2452
|
+
recordAssignments(stmt.Cmd, at);
|
|
2453
|
+
if (stmt.Negated) return false;
|
|
2454
|
+
if (t === "DeclClause") return ASSIGNMENT_HEADS.has(stmt.Cmd.Variant?.Value ?? "");
|
|
2455
|
+
return (stmt.Cmd.Args || []).length === 0 && (stmt.Cmd.Assigns || []).length > 0;
|
|
2456
|
+
}
|
|
2457
|
+
function recordAssignments(n, at) {
|
|
2458
|
+
if (!assignmentTable) return;
|
|
2459
|
+
const t = syntax.NodeType(n);
|
|
2460
|
+
let assigns = [];
|
|
2461
|
+
if (t === "CallExpr") {
|
|
2462
|
+
if ((n.Args || []).length > 0) return;
|
|
2463
|
+
assigns = n.Assigns || [];
|
|
2464
|
+
} else if (t === "DeclClause") {
|
|
2465
|
+
if (!ASSIGNMENT_HEADS.has(n.Variant?.Value ?? "")) return;
|
|
2466
|
+
assigns = (n.Args || []).filter((a) => syntax.NodeType(a) === "Assign");
|
|
2467
|
+
} else return;
|
|
2468
|
+
for (const a of assigns) {
|
|
2469
|
+
const name = a?.Name?.Value;
|
|
2470
|
+
if (!name || !a.Value || a.Append) continue;
|
|
2471
|
+
assignmentTable.set(name, { value: resolveWordLiteral(a.Value), at });
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
function resolveTrivialSubst(part) {
|
|
2475
|
+
if (syntax.NodeType(part) !== "CmdSubst") return void 0;
|
|
2476
|
+
const stmts = part.Stmts || [];
|
|
2477
|
+
if (stmts.length !== 1) return void 0;
|
|
2478
|
+
const st = stmts[0];
|
|
2479
|
+
if ((st.Redirs || []).length > 0 || st.Negated || st.Background) return void 0;
|
|
2480
|
+
const cmd = st.Cmd;
|
|
2481
|
+
if (!cmd || syntax.NodeType(cmd) !== "CallExpr") return void 0;
|
|
2482
|
+
if ((cmd.Assigns || []).length > 0) return void 0;
|
|
2483
|
+
const words = (cmd.Args || []).map((a) => resolveWordLiteral(a));
|
|
2484
|
+
if (words.length === 0 || words.some((w) => w === null)) return void 0;
|
|
2485
|
+
const head = baseWord(words[0]);
|
|
2486
|
+
const rest = words.slice(1);
|
|
2487
|
+
if (head === "echo") {
|
|
2488
|
+
let i = 0;
|
|
2489
|
+
while (i < rest.length && /^-[neE]+$/.test(rest[i])) i++;
|
|
2490
|
+
return rest.slice(i).join(" ");
|
|
2491
|
+
}
|
|
2492
|
+
if (head === "printf") {
|
|
2493
|
+
if (rest.length !== 2) return void 0;
|
|
2494
|
+
if (!/^%s(\\n)?$/.test(rest[0])) return void 0;
|
|
2495
|
+
return rest[1];
|
|
2496
|
+
}
|
|
2497
|
+
return void 0;
|
|
2498
|
+
}
|
|
2499
|
+
function recordedExpansion(name) {
|
|
2500
|
+
if (!assignmentTable || !name) return void 0;
|
|
2501
|
+
const rec = assignmentTable.get(name);
|
|
2502
|
+
if (rec === void 0 || rec.at >= currentStmtOffset) return void 0;
|
|
2503
|
+
return rec.value;
|
|
2504
|
+
}
|
|
2505
|
+
function isPlainParam(p) {
|
|
2506
|
+
if (syntax.NodeType(p) !== "ParamExp") return false;
|
|
2507
|
+
return !(p.Excl || p.Length || p.Width || p.Index || p.Slice || p.Repl || p.Exp);
|
|
2508
|
+
}
|
|
2509
|
+
function expandPlainParam(p) {
|
|
2510
|
+
if (!assignmentTable) return void 0;
|
|
2511
|
+
if (!isPlainParam(p)) return void 0;
|
|
2512
|
+
const recorded = recordedExpansion(p.Param?.Value);
|
|
2513
|
+
if (recorded !== void 0) return recorded;
|
|
2514
|
+
return HOME_VARIABLES.has(p.Param?.Value) ? "~" : void 0;
|
|
2515
|
+
}
|
|
2336
2516
|
function resolveWordLiteral(w) {
|
|
2337
2517
|
const parts = w?.Parts || [];
|
|
2338
2518
|
let s = "";
|
|
2339
2519
|
for (const p of parts) {
|
|
2340
|
-
const
|
|
2341
|
-
if (
|
|
2342
|
-
|
|
2343
|
-
else if (t === "DblQuoted") {
|
|
2344
|
-
const inner = p.Parts || [];
|
|
2345
|
-
if (!inner.every((ip) => syntax.NodeType(ip) === "Lit")) return null;
|
|
2346
|
-
s += inner.map((ip) => ip.Value ?? "").join("");
|
|
2347
|
-
} else {
|
|
2348
|
-
return null;
|
|
2349
|
-
}
|
|
2520
|
+
const piece = resolvePart(p, false);
|
|
2521
|
+
if (piece === void 0 || piece === null) return null;
|
|
2522
|
+
s += piece;
|
|
2350
2523
|
}
|
|
2351
2524
|
return s;
|
|
2352
2525
|
}
|
|
2526
|
+
function resolvePart(p, inQuotes) {
|
|
2527
|
+
const t = syntax.NodeType(p);
|
|
2528
|
+
if (t === "Lit") {
|
|
2529
|
+
const raw = p.Value ?? "";
|
|
2530
|
+
if (!inQuotes) return raw.replace(/\\(.)/g, "$1");
|
|
2531
|
+
return assignmentTable ? raw.replace(/\\([$`"\\])/g, "$1") : raw;
|
|
2532
|
+
}
|
|
2533
|
+
if (t === "SglQuoted") return p.Value ?? "";
|
|
2534
|
+
if (t === "ParamExp") return expandPlainParam(p);
|
|
2535
|
+
if (t === "CmdSubst") return assignmentTable ? resolveTrivialSubst(p) : void 0;
|
|
2536
|
+
if (t === "DblQuoted" && !inQuotes) {
|
|
2537
|
+
const inner = p.Parts || [];
|
|
2538
|
+
let out = "";
|
|
2539
|
+
for (const ip of inner) {
|
|
2540
|
+
const piece = resolvePart(ip, true);
|
|
2541
|
+
if (piece === void 0 || piece === null) return piece;
|
|
2542
|
+
out += piece;
|
|
2543
|
+
}
|
|
2544
|
+
return out;
|
|
2545
|
+
}
|
|
2546
|
+
return void 0;
|
|
2547
|
+
}
|
|
2353
2548
|
function parseDestHost(token) {
|
|
2354
2549
|
if (!token) return null;
|
|
2355
2550
|
let t = token.trim();
|
|
@@ -2404,6 +2599,7 @@ function destTokensForBinary(binary, args) {
|
|
|
2404
2599
|
case "ssh":
|
|
2405
2600
|
return positionals.slice(0, 1);
|
|
2406
2601
|
case "scp":
|
|
2602
|
+
case "rsync":
|
|
2407
2603
|
return positionals.filter((p) => p.includes(":") || p.includes("@"));
|
|
2408
2604
|
case "nc":
|
|
2409
2605
|
case "ncat":
|
|
@@ -2604,17 +2800,35 @@ function analyzeFsOperationImpl(command, depth = 0) {
|
|
|
2604
2800
|
const f = parseShared(command);
|
|
2605
2801
|
if (f === PARSE_FAIL) return null;
|
|
2606
2802
|
let result = null;
|
|
2803
|
+
const outerTable = assignmentTable;
|
|
2804
|
+
const outerOffset = currentStmtOffset;
|
|
2805
|
+
const outerSeen = seenPayloads;
|
|
2806
|
+
const outerBudget = payloadBudget;
|
|
2807
|
+
if (depth === 0) {
|
|
2808
|
+
seenPayloads = /* @__PURE__ */ new Set();
|
|
2809
|
+
payloadBudget = PAYLOAD_BUDGET;
|
|
2810
|
+
}
|
|
2811
|
+
assignmentTable = new Map(
|
|
2812
|
+
[...outerTable ?? []].map(([k, r]) => [k, { value: r.value, at: -1 }])
|
|
2813
|
+
);
|
|
2814
|
+
currentStmtOffset = Number.MAX_SAFE_INTEGER;
|
|
2607
2815
|
try {
|
|
2816
|
+
recordTopLevelAssignments(f);
|
|
2608
2817
|
syntax.Walk(f, (node) => {
|
|
2609
2818
|
if (!node || result?.verdict === "block") return false;
|
|
2610
2819
|
const n = node;
|
|
2611
2820
|
const nodeType = syntax.NodeType(n);
|
|
2612
2821
|
if (nodeType === "Stmt") {
|
|
2822
|
+
try {
|
|
2823
|
+
currentStmtOffset = n.Pos().Offset();
|
|
2824
|
+
} catch {
|
|
2825
|
+
currentStmtOffset = Number.MAX_SAFE_INTEGER;
|
|
2826
|
+
}
|
|
2613
2827
|
result = stricter(result, jailedRedirectRead(n));
|
|
2614
2828
|
return result?.verdict !== "block";
|
|
2615
2829
|
}
|
|
2616
2830
|
if (nodeType !== "CallExpr") return true;
|
|
2617
|
-
const { name, flags, paths, words
|
|
2831
|
+
const { name, flags, paths, words } = extractLiteralArgs(n);
|
|
2618
2832
|
if (!name) return true;
|
|
2619
2833
|
if (name === "rm") {
|
|
2620
2834
|
const flagStr = flags.join("").toLowerCase();
|
|
@@ -2643,9 +2857,30 @@ function analyzeFsOperationImpl(command, depth = 0) {
|
|
|
2643
2857
|
}
|
|
2644
2858
|
}
|
|
2645
2859
|
}
|
|
2646
|
-
if (depth <
|
|
2860
|
+
if (depth < 24 && name === "find") {
|
|
2861
|
+
for (const action of findActions(words, 0)) {
|
|
2862
|
+
const h = unwrapCommandHead(action);
|
|
2863
|
+
const inner = literalShellPayload(action.slice(h), baseWord(action[h]));
|
|
2864
|
+
if (inner === null) continue;
|
|
2865
|
+
const claim = claimPayload(inner);
|
|
2866
|
+
if (claim === "exhausted") {
|
|
2867
|
+
result = stricter(result, UNANALYSABLE_NESTING);
|
|
2868
|
+
continue;
|
|
2869
|
+
}
|
|
2870
|
+
if (claim === "seen") continue;
|
|
2871
|
+
const v = analyzeFsOperationImpl(inner, depth + 1);
|
|
2872
|
+
result = stricter(result, v);
|
|
2873
|
+
if (result?.verdict === "block") return false;
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
if (depth < 24) {
|
|
2647
2877
|
const payload = literalShellPayload(words, name);
|
|
2648
|
-
|
|
2878
|
+
const claim = payload === null ? "seen" : claimPayload(payload);
|
|
2879
|
+
if (claim === "exhausted") {
|
|
2880
|
+
result = stricter(result, UNANALYSABLE_NESTING);
|
|
2881
|
+
return true;
|
|
2882
|
+
}
|
|
2883
|
+
if (payload !== null && claim === "ok") {
|
|
2649
2884
|
const inner = analyzeFsOperationImpl(payload, depth + 1);
|
|
2650
2885
|
if (inner) {
|
|
2651
2886
|
result = inner;
|
|
@@ -2654,7 +2889,7 @@ function analyzeFsOperationImpl(command, depth = 0) {
|
|
|
2654
2889
|
return true;
|
|
2655
2890
|
}
|
|
2656
2891
|
}
|
|
2657
|
-
const readPaths = FS_READ_TOOLS.has(name) ?
|
|
2892
|
+
const readPaths = FS_READ_TOOLS.has(name) ? readerPaths(words, 0) : wrappedReadPaths(words, name);
|
|
2658
2893
|
if (readPaths) {
|
|
2659
2894
|
for (const p of readPaths) {
|
|
2660
2895
|
result = stricter(result, matchSensitivePath2(p));
|
|
@@ -2669,6 +2904,11 @@ function analyzeFsOperationImpl(command, depth = 0) {
|
|
|
2669
2904
|
return result;
|
|
2670
2905
|
} catch {
|
|
2671
2906
|
return null;
|
|
2907
|
+
} finally {
|
|
2908
|
+
assignmentTable = outerTable;
|
|
2909
|
+
currentStmtOffset = outerOffset;
|
|
2910
|
+
seenPayloads = outerSeen;
|
|
2911
|
+
if (depth === 0) payloadBudget = outerBudget;
|
|
2672
2912
|
}
|
|
2673
2913
|
}
|
|
2674
2914
|
function stricter(a, b) {
|
|
@@ -2726,6 +2966,18 @@ function resolveCopyShape(words, h) {
|
|
|
2726
2966
|
return null;
|
|
2727
2967
|
}
|
|
2728
2968
|
var FIND_OPTIONS = /* @__PURE__ */ new Set(["-H", "-L", "-P"]);
|
|
2969
|
+
function findAction(words, k) {
|
|
2970
|
+
const end = words.findIndex((w, i) => i > k && (w === ";" || w === "+"));
|
|
2971
|
+
return words.slice(k + 1, end < 0 ? words.length : end);
|
|
2972
|
+
}
|
|
2973
|
+
function findActions(words, h) {
|
|
2974
|
+
const out = [];
|
|
2975
|
+
for (let i = h + 1; i < words.length; i++) {
|
|
2976
|
+
const w = words[i];
|
|
2977
|
+
if (w !== null && FIND_EXEC_FLAGS.has(w)) out.push(findAction(words, i));
|
|
2978
|
+
}
|
|
2979
|
+
return out;
|
|
2980
|
+
}
|
|
2729
2981
|
function findStartPoints(words, h) {
|
|
2730
2982
|
const k = words.findIndex((w, i) => i > h && w !== null && FIND_EXEC_FLAGS.has(w));
|
|
2731
2983
|
if (k < 0) return { k, starts: [] };
|
|
@@ -2741,8 +2993,13 @@ function copySourcePaths(words) {
|
|
|
2741
2993
|
if (fi >= 0) {
|
|
2742
2994
|
const { k, starts } = findStartPoints(words, fi);
|
|
2743
2995
|
if (k < 0) return [];
|
|
2744
|
-
const
|
|
2745
|
-
|
|
2996
|
+
const out = [];
|
|
2997
|
+
for (const action of findActions(words, fi)) {
|
|
2998
|
+
const h2 = unwrapCommandHead(action);
|
|
2999
|
+
if (!resolveCopyShape(action, h2)) continue;
|
|
3000
|
+
out.push(...starts, ...copySourcePaths(action));
|
|
3001
|
+
}
|
|
3002
|
+
return out;
|
|
2746
3003
|
}
|
|
2747
3004
|
if (!COPY_VERB_HEADS.has(baseWord(words[h]))) return [];
|
|
2748
3005
|
const r = resolveCopyShape(words, h);
|
|
@@ -2946,6 +3203,15 @@ function flagEffect(token, shape, known) {
|
|
|
2946
3203
|
}
|
|
2947
3204
|
return NONE;
|
|
2948
3205
|
}
|
|
3206
|
+
function readerPaths(words, h) {
|
|
3207
|
+
const head = baseWord(words[h]);
|
|
3208
|
+
const from = h + 1;
|
|
3209
|
+
const flags = words.slice(from).filter((w) => w !== null && w.startsWith("-"));
|
|
3210
|
+
return [
|
|
3211
|
+
...readTargets(head, positionedArgs(words, from), flags, words, from),
|
|
3212
|
+
...flagOperandFiles(head, words, from)
|
|
3213
|
+
];
|
|
3214
|
+
}
|
|
2949
3215
|
function readTargets(verb, args, flags, words = [], from = 1) {
|
|
2950
3216
|
const shape = PATTERN_VERBS[verb];
|
|
2951
3217
|
if (!shape) return args.map((a) => a.value);
|
|
@@ -3027,18 +3293,21 @@ function flagOperandFiles(verb, words, from) {
|
|
|
3027
3293
|
function wrappedReadPaths(words, name) {
|
|
3028
3294
|
if (name === "find") {
|
|
3029
3295
|
const { k, starts } = findStartPoints(words, 0);
|
|
3030
|
-
|
|
3296
|
+
if (k < 0) return null;
|
|
3297
|
+
const paths = [];
|
|
3298
|
+
let reads = false;
|
|
3299
|
+
for (const action of findActions(words, 0)) {
|
|
3300
|
+
const h2 = unwrapCommandHead(action);
|
|
3301
|
+
if (!isReaderWord(action[h2] ?? null)) continue;
|
|
3302
|
+
reads = true;
|
|
3303
|
+
paths.push(...readerPaths(action, h2));
|
|
3304
|
+
}
|
|
3305
|
+
return reads ? [...starts, ...paths] : paths;
|
|
3031
3306
|
}
|
|
3032
3307
|
if (!COMMAND_WRAPPERS.has(name) && !RUNNER_WRAPPERS.has(name)) return null;
|
|
3033
3308
|
const h = unwrapCommandHead(words);
|
|
3034
3309
|
if (h <= 0 || !isReaderWord(words[h] ?? null)) return null;
|
|
3035
|
-
|
|
3036
|
-
const rest = words.slice(h + 1);
|
|
3037
|
-
const restFlags = rest.filter((w) => w !== null && w.startsWith("-"));
|
|
3038
|
-
return [
|
|
3039
|
-
...readTargets(head, positionedArgs(words, h + 1), restFlags, words, h + 1),
|
|
3040
|
-
...flagOperandFiles(head, words, h + 1)
|
|
3041
|
-
];
|
|
3310
|
+
return readerPaths(words, h);
|
|
3042
3311
|
}
|
|
3043
3312
|
function literalShellPayload(words, name) {
|
|
3044
3313
|
const h = COMMAND_WRAPPERS.has(name) || RUNNER_WRAPPERS.has(name) ? unwrapCommandHead(words) : 0;
|
|
@@ -5647,8 +5916,8 @@ function matchCanaryArgs(args, values) {
|
|
|
5647
5916
|
|
|
5648
5917
|
// src/scan/canonical.ts
|
|
5649
5918
|
var LONG_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
|
|
5650
|
-
var CANONICAL_EXTRACTOR_VERSION = "canonical-
|
|
5651
|
-
var CANONICAL_EXTRACTOR_HASH = "
|
|
5919
|
+
var CANONICAL_EXTRACTOR_VERSION = "canonical-v18";
|
|
5920
|
+
var CANONICAL_EXTRACTOR_HASH = "5b2e76537f368d99";
|
|
5652
5921
|
var DEDUPE_PREVIEW_LEN = 120;
|
|
5653
5922
|
function extractCanonicalFindings(call, ctx) {
|
|
5654
5923
|
const out = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@node9/policy-engine",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.23.1",
|
|
4
4
|
"description": "Shared policy evaluation engine for node9 — DLP, smart rules, AST shell parsing, shields, loop detection. Pure functions, no I/O. Used by both node9-proxy and the node9 SaaS firewall.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://node9.ai",
|