@node9/policy-engine 2.13.0 → 2.14.0
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 +58 -3
- package/dist/index.d.ts +58 -3
- package/dist/index.js +417 -94
- package/dist/index.mjs +411 -94
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -36,6 +36,8 @@ __export(src_exports, {
|
|
|
36
36
|
CANARY_MIN_LENGTH: () => CANARY_MIN_LENGTH,
|
|
37
37
|
CANONICAL_EXTRACTOR_HASH: () => CANONICAL_EXTRACTOR_HASH,
|
|
38
38
|
CANONICAL_EXTRACTOR_VERSION: () => CANONICAL_EXTRACTOR_VERSION,
|
|
39
|
+
COMMAND_WRAPPERS: () => COMMAND_WRAPPERS,
|
|
40
|
+
COPY_VERBS: () => COPY_VERBS,
|
|
39
41
|
COST_PER_LOOP_ITER_USD: () => COST_PER_LOOP_ITER_USD,
|
|
40
42
|
DEFAULT_EGRESS_ALLOWLIST: () => DEFAULT_EGRESS_ALLOWLIST,
|
|
41
43
|
DESTINATION_ARGS: () => DESTINATION_ARGS,
|
|
@@ -49,6 +51,7 @@ __export(src_exports, {
|
|
|
49
51
|
LONG_OUTPUT_THRESHOLD_BYTES: () => LONG_OUTPUT_THRESHOLD_BYTES,
|
|
50
52
|
LOOP_MAX_RECORDS: () => LOOP_MAX_RECORDS,
|
|
51
53
|
LOOP_THRESHOLD_FOR_WASTE: () => LOOP_THRESHOLD_FOR_WASTE,
|
|
54
|
+
NET_BINARIES: () => NET_BINARIES,
|
|
52
55
|
PRIVILEGE_ESCALATION_RE: () => PRIVILEGE_ESCALATION_RE,
|
|
53
56
|
REALTIME_PII_PATTERNS: () => REALTIME_PII_PATTERNS,
|
|
54
57
|
SCAN_SIGNAL_WEIGHTS: () => SCAN_SIGNAL_WEIGHTS,
|
|
@@ -104,9 +107,11 @@ __export(src_exports, {
|
|
|
104
107
|
normalizeIpLiteral: () => normalizeIpLiteral,
|
|
105
108
|
parseAllSshHostsFromCommand: () => parseAllSshHostsFromCommand,
|
|
106
109
|
parseDestHost: () => parseDestHost,
|
|
110
|
+
positionedArgs: () => positionedArgs,
|
|
107
111
|
previewArgs: () => previewArgs,
|
|
108
112
|
redactText: () => redactText,
|
|
109
113
|
resolvePinned: () => resolvePinned,
|
|
114
|
+
sampleCopyCommand: () => sampleCopyCommand,
|
|
110
115
|
scanArgs: () => scanArgs,
|
|
111
116
|
scanInjection: () => scanInjection,
|
|
112
117
|
scanText: () => scanText,
|
|
@@ -119,6 +124,7 @@ __export(src_exports, {
|
|
|
119
124
|
toScanFinding: () => toScanFinding,
|
|
120
125
|
toolMatchesRule: () => toolMatchesRule,
|
|
121
126
|
truncateBlastPath: () => truncateBlastPath,
|
|
127
|
+
unwrapCommandHead: () => unwrapCommandHead,
|
|
122
128
|
validateOverrides: () => validateOverrides,
|
|
123
129
|
validateRegex: () => validateRegex,
|
|
124
130
|
validateShieldDefinition: () => validateShieldDefinition
|
|
@@ -844,13 +850,32 @@ var DLP_PATTERNS_GLOBAL = DLP_PATTERNS.map(
|
|
|
844
850
|
})
|
|
845
851
|
);
|
|
846
852
|
var SENSITIVE_PATH_PATTERNS = [
|
|
847
|
-
/[/\\]\.ssh[/\\]/i,
|
|
848
|
-
/[/\\]\.aws[/\\]/i,
|
|
853
|
+
/[/\\]\.ssh([/\\]|$)/i,
|
|
854
|
+
/[/\\]\.aws([/\\]|$)/i,
|
|
849
855
|
/[/\\]\.config[/\\]gcloud[/\\]/i,
|
|
850
856
|
/[/\\]\.azure[/\\]/i,
|
|
851
857
|
/[/\\]\.kube[/\\]config$/i,
|
|
852
|
-
|
|
853
|
-
// .
|
|
858
|
+
// ⚠️ ONE SEMANTIC, FOUR COPIES. This is the AST tier's `.env` rule verbatim
|
|
859
|
+
// (shell/index.ts SENSITIVE_PATH_RULES), whose reasoning is documented there:
|
|
860
|
+
// structural suffix chain rather than a hand-written list, `example|sample|
|
|
861
|
+
// template` exempt because a fixture stays a fixture whatever follows, and
|
|
862
|
+
// `.test` anchored because `test` names an ENVIRONMENT -- `.env.test` is the
|
|
863
|
+
// committed template, `.env.test.local` is gitignored and holds real values.
|
|
864
|
+
//
|
|
865
|
+
// It was previously `[/\\]\.env($|\.)` with NO exemptions, so `Read .env.example`
|
|
866
|
+
// blocked while `cat .env.example` allowed: the same file, opposite verdicts,
|
|
867
|
+
// decided only by which tool asked. See src/__tests__/jail-both-doors.test.ts,
|
|
868
|
+
// which is the contract that now holds these copies in step, and stage 5 of
|
|
869
|
+
// doc/credential-jail-architecture.md, which replaces them with one generated
|
|
870
|
+
// source.
|
|
871
|
+
// ⚠️ The `.local` branch comes FIRST and takes no exemption. A fixture stays a
|
|
872
|
+
// fixture whatever follows it -- `.env.example.md` is documentation -- but
|
|
873
|
+
// `.env.example.local` is gitignored by the `.env*.local` convention and holds
|
|
874
|
+
// real values, exactly the reasoning that anchors `(?!\.test$)` rather than
|
|
875
|
+
// using `\b`. Without this branch the fixture exemption also bought a two-step
|
|
876
|
+
// bypass: `cp .env .env.sample`, then read the copy.
|
|
877
|
+
/[/\\]\.env(?![\w-])(?:[\w.-]*\.local$|(?!\.(?:example|sample|template)\b)(?!\.test$)[\w.-]*$)/i,
|
|
878
|
+
// .env + any suffix chain; fixtures exempt unless .local
|
|
854
879
|
/[/\\]\.git-credentials$/i,
|
|
855
880
|
/[/\\]\.npmrc$/i,
|
|
856
881
|
/[/\\]\.docker[/\\]config\.json$/i,
|
|
@@ -1409,6 +1434,10 @@ var FS_READ_TOOLS = /* @__PURE__ */ new Set([
|
|
|
1409
1434
|
"od",
|
|
1410
1435
|
"xxd",
|
|
1411
1436
|
"hexdump",
|
|
1437
|
+
// Emits the file's bytes, re-encoded, so it is a read by the set's own test
|
|
1438
|
+
// ("does it emit file contents"). Absent until 2026-09-10, which is why
|
|
1439
|
+
// `base64 ~/.ssh/id_rsa` printed a private key with no verdict.
|
|
1440
|
+
"base64",
|
|
1412
1441
|
"strings",
|
|
1413
1442
|
"sort",
|
|
1414
1443
|
"uniq",
|
|
@@ -1416,8 +1445,56 @@ var FS_READ_TOOLS = /* @__PURE__ */ new Set([
|
|
|
1416
1445
|
"nl",
|
|
1417
1446
|
"dd"
|
|
1418
1447
|
]);
|
|
1448
|
+
var SCP_VALUE_FLAGS = ["i", "F", "o", "c", "S", "P", "J", "D", "W", "l"];
|
|
1449
|
+
var RSYNC_SKIP = [
|
|
1450
|
+
"e",
|
|
1451
|
+
"--rsh",
|
|
1452
|
+
"--exclude",
|
|
1453
|
+
"--exclude-from",
|
|
1454
|
+
"--include",
|
|
1455
|
+
"--include-from",
|
|
1456
|
+
"--files-from",
|
|
1457
|
+
"f",
|
|
1458
|
+
"--filter"
|
|
1459
|
+
];
|
|
1460
|
+
var COPY_VERBS = {
|
|
1461
|
+
cp: { source: "allButLast", targetDirFlag: true },
|
|
1462
|
+
mv: { source: "allButLast", targetDirFlag: true },
|
|
1463
|
+
install: { source: "allButLast", targetDirFlag: true },
|
|
1464
|
+
ln: { source: "first", targetDirFlag: true },
|
|
1465
|
+
scp: { source: "allButLast", skipFlags: SCP_VALUE_FLAGS },
|
|
1466
|
+
rsync: { source: "allButLast", skipFlags: RSYNC_SKIP },
|
|
1467
|
+
tar: {
|
|
1468
|
+
source: "archive",
|
|
1469
|
+
archive: "tar",
|
|
1470
|
+
skipFlags: ["f", "X", "T", "--file", "--exclude", "--exclude-from", "--files-from"]
|
|
1471
|
+
},
|
|
1472
|
+
zip: { source: "archive", archive: "zip", skipFlags: ["x", "i", "--exclude", "--include"] },
|
|
1473
|
+
ar: { source: "archive", archive: "ar" },
|
|
1474
|
+
"7z": { source: "archive", archive: "7z", skipFlags: ["x", "--exclude"] },
|
|
1475
|
+
gzip: { source: "all" },
|
|
1476
|
+
bzip2: { source: "all" },
|
|
1477
|
+
xz: { source: "all" },
|
|
1478
|
+
"docker cp": { source: "allButLast" },
|
|
1479
|
+
"kubectl cp": { source: "allButLast" },
|
|
1480
|
+
"gsutil cp": { source: "allButLast" },
|
|
1481
|
+
"gsutil rsync": { source: "allButLast" },
|
|
1482
|
+
"rclone copy": { source: "allButLast" },
|
|
1483
|
+
"rclone sync": { source: "allButLast" },
|
|
1484
|
+
"aws s3 cp": { source: "allButLast" },
|
|
1485
|
+
"aws s3 mv": { source: "allButLast" },
|
|
1486
|
+
"aws s3 sync": { source: "allButLast" },
|
|
1487
|
+
"gcloud storage cp": { source: "allButLast" },
|
|
1488
|
+
"az storage blob upload": { source: "flagOperand", sourceFlags: ["f", "--file"] }
|
|
1489
|
+
};
|
|
1490
|
+
var TAR_MODE_WORD = /^[a-zA-Z]+$/;
|
|
1491
|
+
var COPY_VERB_HEADS = new Set(Object.keys(COPY_VERBS).map((k) => k.split(" ")[0]));
|
|
1419
1492
|
var FS_OP_PRESCREEN_RE = new RegExp(
|
|
1420
|
-
|
|
1493
|
+
// A quote is a separator too: `eval "cat X"` and `sh -c 'cat X'` put the
|
|
1494
|
+
// reader right after `"` / `'`, and without these two characters the
|
|
1495
|
+
// prescreen rejected every string-wrapped read before the parser ran.
|
|
1496
|
+
// Found 2026-09-11 by instrumenting the walk -- no CallExpr was ever visited.
|
|
1497
|
+
`(?:^|[\\s|;&("'\`\\n/])(?:rm|${[...FS_READ_TOOLS, ...COPY_VERB_HEADS].map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\b|(?<!<)<(?!<)`
|
|
1421
1498
|
);
|
|
1422
1499
|
var HOME_CACHE_ALLOWLIST = [
|
|
1423
1500
|
".cache",
|
|
@@ -1438,12 +1515,12 @@ var SENSITIVE_PATH_RULES = [
|
|
|
1438
1515
|
{
|
|
1439
1516
|
rule: "shield:project-jail:block-read-ssh",
|
|
1440
1517
|
reason: "Reading SSH private keys is blocked by project-jail shield",
|
|
1441
|
-
match: (p) => /(
|
|
1518
|
+
match: (p) => /([\\/]\.ssh[\\/]|^\.ssh[\\/]|^(?:[~/]|[A-Za-z]:).*[\\/]\.ssh$)/i.test(p)
|
|
1442
1519
|
},
|
|
1443
1520
|
{
|
|
1444
1521
|
rule: "shield:project-jail:block-read-aws",
|
|
1445
1522
|
reason: "Reading AWS credentials is blocked by project-jail shield",
|
|
1446
|
-
match: (p) => /(
|
|
1523
|
+
match: (p) => /([\\/]\.aws[\\/]|^\.aws[\\/]|^(?:[~/]|[A-Za-z]:).*[\\/]\.aws$)/i.test(p)
|
|
1447
1524
|
},
|
|
1448
1525
|
{
|
|
1449
1526
|
// Mirrors the JSON shield's `.env` pattern (project-jail.json's
|
|
@@ -1487,7 +1564,9 @@ var SENSITIVE_PATH_RULES = [
|
|
|
1487
1564
|
// symmetry — silently exempts every `.env.test.*` file.
|
|
1488
1565
|
//
|
|
1489
1566
|
// shields.test.ts:983-995 is the canonical contract; keep both in step.
|
|
1490
|
-
match: (p) => /(?:^|[\\/])\.env(?![\w-])(?!\.(?:example|sample|template)\b)(?!\.test$)[\w.-]
|
|
1567
|
+
match: (p) => /(?:^|[\\/])\.env(?![\w-])(?:[\w.-]*\.local$|(?!\.(?:example|sample|template)\b)(?!\.test$)[\w.-]*$)/i.test(
|
|
1568
|
+
p
|
|
1569
|
+
)
|
|
1491
1570
|
},
|
|
1492
1571
|
{
|
|
1493
1572
|
// verdict: 'review' (not 'block') is a deliberate design choice
|
|
@@ -1698,15 +1777,14 @@ function listOps() {
|
|
|
1698
1777
|
return _listOps;
|
|
1699
1778
|
}
|
|
1700
1779
|
var WRAPPER_TAKES_TARGET = /* @__PURE__ */ new Set(["chroot"]);
|
|
1780
|
+
var FIND_EXEC_FLAGS = /* @__PURE__ */ new Set(["-exec", "-execdir", "-ok", "-okdir"]);
|
|
1701
1781
|
var INTERP_LEADING_TARGET = /* @__PURE__ */ new Set(["su"]);
|
|
1702
1782
|
function unwrapCommandHead(words) {
|
|
1703
1783
|
let i = 0;
|
|
1704
1784
|
while (i < words.length) {
|
|
1705
1785
|
const head = (words[i] ?? "").toLowerCase().split("/").pop() ?? "";
|
|
1706
1786
|
if (head === "find") {
|
|
1707
|
-
const x = words.findIndex(
|
|
1708
|
-
(w, k) => k > i && (w === "-exec" || w === "-execdir" || w === "-ok")
|
|
1709
|
-
);
|
|
1787
|
+
const x = words.findIndex((w, k) => k > i && w !== null && FIND_EXEC_FLAGS.has(w));
|
|
1710
1788
|
if (x < 0) break;
|
|
1711
1789
|
i = x + 1;
|
|
1712
1790
|
continue;
|
|
@@ -1728,7 +1806,11 @@ function unwrapCommandHead(words) {
|
|
|
1728
1806
|
if (t.startsWith("-")) {
|
|
1729
1807
|
i++;
|
|
1730
1808
|
const nxt = words[i];
|
|
1731
|
-
if (nxt != null && !nxt.startsWith("-") && !INLINE_INTERPRETER.test(nxt.split("/").pop() ?? "") && !COMMAND_WRAPPERS.has(nxt.toLowerCase()) && !RUNNER_WRAPPERS.has(nxt.toLowerCase())
|
|
1809
|
+
if (nxt != null && !nxt.startsWith("-") && !INLINE_INTERPRETER.test(nxt.split("/").pop() ?? "") && !COMMAND_WRAPPERS.has(nxt.toLowerCase()) && !RUNNER_WRAPPERS.has(nxt.toLowerCase()) && // A reader is a command, never a flag's operand: `env - cat X`,
|
|
1810
|
+
// `stdbuf -o0 cat X`, `ionice -c3 cat X`. Without this the head was
|
|
1811
|
+
// swallowed and the jail needed a looser fallback whose cost was a
|
|
1812
|
+
// false positive on `sudo echo cat X`.
|
|
1813
|
+
!FS_READ_TOOLS.has(nxt.split("/").pop()?.toLowerCase() ?? ""))
|
|
1732
1814
|
i++;
|
|
1733
1815
|
continue;
|
|
1734
1816
|
}
|
|
@@ -1847,38 +1929,43 @@ function isProtectedHomePath(rawPath) {
|
|
|
1847
1929
|
}
|
|
1848
1930
|
return true;
|
|
1849
1931
|
}
|
|
1850
|
-
function
|
|
1851
|
-
const
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
const
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
if (t === "Lit") s += (p.Value ?? "").replace(/\\(.)/g, "$1");
|
|
1859
|
-
else if (t === "SglQuoted") s += p.Value ?? "";
|
|
1860
|
-
else if (t === "DblQuoted") {
|
|
1861
|
-
const inner = p.Parts || [];
|
|
1862
|
-
if (!inner.every((ip) => syntax.NodeType(ip) === "Lit")) return null;
|
|
1863
|
-
s += inner.map((ip) => ip.Value ?? "").join("");
|
|
1864
|
-
} else {
|
|
1865
|
-
return null;
|
|
1866
|
-
}
|
|
1932
|
+
function positionedArgs(words, from = 1, to = words.length) {
|
|
1933
|
+
const out = [];
|
|
1934
|
+
let afterFlag = null;
|
|
1935
|
+
for (let i = from; i < to; i++) {
|
|
1936
|
+
const v = words[i];
|
|
1937
|
+
if (v === null) {
|
|
1938
|
+
afterFlag = null;
|
|
1939
|
+
continue;
|
|
1867
1940
|
}
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
const v = litFromWord(args[i]);
|
|
1875
|
-
if (v === null) continue;
|
|
1876
|
-
if (v.startsWith("-")) flags.push(v);
|
|
1877
|
-
else paths.push(v);
|
|
1941
|
+
if (v.startsWith("-")) {
|
|
1942
|
+
afterFlag = v;
|
|
1943
|
+
continue;
|
|
1944
|
+
}
|
|
1945
|
+
out.push({ value: v, index: out.length, argv: i, afterFlag });
|
|
1946
|
+
afterFlag = null;
|
|
1878
1947
|
}
|
|
1879
|
-
return
|
|
1948
|
+
return out;
|
|
1880
1949
|
}
|
|
1881
|
-
|
|
1950
|
+
function extractLiteralArgs(callExpr) {
|
|
1951
|
+
const rawArgs = callExpr.Args || [];
|
|
1952
|
+
if (rawArgs.length === 0) return { name: "", flags: [], paths: [], words: [], args: [] };
|
|
1953
|
+
const words = rawArgs.map((a) => resolveWordLiteral(a));
|
|
1954
|
+
const name = baseWord(words[0]);
|
|
1955
|
+
const flags = words.slice(1).filter((w) => w !== null && w.startsWith("-"));
|
|
1956
|
+
const args = positionedArgs(words);
|
|
1957
|
+
return { name, flags, paths: args.map((a) => a.value), words, args };
|
|
1958
|
+
}
|
|
1959
|
+
var NET_BINARIES = /* @__PURE__ */ new Set([
|
|
1960
|
+
"curl",
|
|
1961
|
+
"wget",
|
|
1962
|
+
"scp",
|
|
1963
|
+
"ssh",
|
|
1964
|
+
"nc",
|
|
1965
|
+
"ncat",
|
|
1966
|
+
"netcat",
|
|
1967
|
+
"rsync"
|
|
1968
|
+
]);
|
|
1882
1969
|
var VALUE_FLAGS = {
|
|
1883
1970
|
curl: /* @__PURE__ */ new Set([
|
|
1884
1971
|
"-d",
|
|
@@ -2163,6 +2250,9 @@ function deriveRedirOp(sample) {
|
|
|
2163
2250
|
}
|
|
2164
2251
|
}
|
|
2165
2252
|
var REDIR_TRUNCATE_OPS = /* @__PURE__ */ new Set([deriveRedirOp(">_f")]);
|
|
2253
|
+
var REDIR_FILE_IN_OPS = new Set(
|
|
2254
|
+
[deriveRedirOp("cat < f"), deriveRedirOp("cat <> f")].filter((op) => op >= 0)
|
|
2255
|
+
);
|
|
2166
2256
|
var REDIR_HEREDOC_OPS = /* @__PURE__ */ new Set([
|
|
2167
2257
|
deriveRedirOp("cat <<X\nX"),
|
|
2168
2258
|
deriveRedirOp("cat <<-X\nX")
|
|
@@ -2227,16 +2317,21 @@ function isRmCreatedInCommandCleanup(command) {
|
|
|
2227
2317
|
}
|
|
2228
2318
|
return sawRm && ok;
|
|
2229
2319
|
}
|
|
2230
|
-
function analyzeFsOperationImpl(command) {
|
|
2320
|
+
function analyzeFsOperationImpl(command, depth = 0) {
|
|
2231
2321
|
const f = parseShared(command);
|
|
2232
2322
|
if (f === PARSE_FAIL) return null;
|
|
2233
2323
|
let result = null;
|
|
2234
2324
|
try {
|
|
2235
2325
|
syntax.Walk(f, (node) => {
|
|
2236
|
-
if (!node || result) return false;
|
|
2326
|
+
if (!node || result?.verdict === "block") return false;
|
|
2237
2327
|
const n = node;
|
|
2238
|
-
|
|
2239
|
-
|
|
2328
|
+
const nodeType = syntax.NodeType(n);
|
|
2329
|
+
if (nodeType === "Stmt") {
|
|
2330
|
+
result = stricter(result, jailedRedirectRead(n));
|
|
2331
|
+
return result?.verdict !== "block";
|
|
2332
|
+
}
|
|
2333
|
+
if (nodeType !== "CallExpr") return true;
|
|
2334
|
+
const { name, flags, paths, words } = extractLiteralArgs(n);
|
|
2240
2335
|
if (!name) return true;
|
|
2241
2336
|
if (name === "rm") {
|
|
2242
2337
|
const flagStr = flags.join("").toLowerCase();
|
|
@@ -2265,21 +2360,27 @@ function analyzeFsOperationImpl(command) {
|
|
|
2265
2360
|
}
|
|
2266
2361
|
}
|
|
2267
2362
|
}
|
|
2268
|
-
if (
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
reason: sp.reason,
|
|
2276
|
-
path: p
|
|
2277
|
-
};
|
|
2278
|
-
return false;
|
|
2279
|
-
}
|
|
2363
|
+
if (depth < 1) {
|
|
2364
|
+
const payload = literalShellPayload(words, name);
|
|
2365
|
+
if (payload !== null) {
|
|
2366
|
+
const inner = analyzeFsOperationImpl(payload, depth + 1);
|
|
2367
|
+
if (inner) {
|
|
2368
|
+
result = inner;
|
|
2369
|
+
return false;
|
|
2280
2370
|
}
|
|
2371
|
+
return true;
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
const readPaths = FS_READ_TOOLS.has(name) ? paths : wrappedReadPaths(words, name);
|
|
2375
|
+
if (readPaths) {
|
|
2376
|
+
for (const p of readPaths) {
|
|
2377
|
+
result = stricter(result, matchSensitivePath2(p));
|
|
2378
|
+
if (result?.verdict === "block") return false;
|
|
2281
2379
|
}
|
|
2282
2380
|
}
|
|
2381
|
+
for (const p of copySourcePaths(words)) {
|
|
2382
|
+
result = stricter(result, copyVerdictOf(matchSensitivePath2(p)));
|
|
2383
|
+
}
|
|
2283
2384
|
return true;
|
|
2284
2385
|
});
|
|
2285
2386
|
return result;
|
|
@@ -2287,6 +2388,209 @@ function analyzeFsOperationImpl(command) {
|
|
|
2287
2388
|
return null;
|
|
2288
2389
|
}
|
|
2289
2390
|
}
|
|
2391
|
+
function stricter(a, b) {
|
|
2392
|
+
if (!a) return b;
|
|
2393
|
+
if (!b) return a;
|
|
2394
|
+
return b.verdict === "block" && a.verdict !== "block" ? b : a;
|
|
2395
|
+
}
|
|
2396
|
+
function flagInfo(w) {
|
|
2397
|
+
if (w.startsWith("--")) {
|
|
2398
|
+
const eq = w.indexOf("=");
|
|
2399
|
+
return eq < 0 ? { letter: null, long: w, attached: null } : { letter: null, long: w.slice(0, eq), attached: w.slice(eq + 1) };
|
|
2400
|
+
}
|
|
2401
|
+
const m = /^-([a-zA-Z]+)(.*)$/.exec(w);
|
|
2402
|
+
if (!m) return { letter: null, long: null, attached: null };
|
|
2403
|
+
return { letter: m[1][m[1].length - 1], long: null, attached: m[2] === "" ? null : m[2] };
|
|
2404
|
+
}
|
|
2405
|
+
function flagIs(w, names) {
|
|
2406
|
+
if (w === null) return false;
|
|
2407
|
+
const f = flagInfo(w);
|
|
2408
|
+
return names.some((n) => n.startsWith("--") ? f.long === n : f.letter === n);
|
|
2409
|
+
}
|
|
2410
|
+
function operandOf(a, names) {
|
|
2411
|
+
if (!names || a.afterFlag === null) return false;
|
|
2412
|
+
return flagIs(a.afterFlag, names) && flagInfo(a.afterFlag).attached === null;
|
|
2413
|
+
}
|
|
2414
|
+
function resolveCopyShape(words, h) {
|
|
2415
|
+
const verb = baseWord(words[h]);
|
|
2416
|
+
if (!verb) return null;
|
|
2417
|
+
const direct = COPY_VERBS[verb];
|
|
2418
|
+
if (direct) return { shape: direct, last: h };
|
|
2419
|
+
const slots = positionedArgs(words, h + 1);
|
|
2420
|
+
for (let i = 0; i < slots.length; i++) {
|
|
2421
|
+
for (let n = 3; n >= 1; n--) {
|
|
2422
|
+
const part = slots.slice(i, i + n);
|
|
2423
|
+
if (part.length < n) continue;
|
|
2424
|
+
const key = [verb, ...part.map((a) => a.value.toLowerCase())].join(" ");
|
|
2425
|
+
const shape = COPY_VERBS[key];
|
|
2426
|
+
if (shape) return { shape, last: part[n - 1].argv };
|
|
2427
|
+
}
|
|
2428
|
+
if (slots[i].afterFlag === null) return null;
|
|
2429
|
+
}
|
|
2430
|
+
return null;
|
|
2431
|
+
}
|
|
2432
|
+
var FIND_OPTIONS = /* @__PURE__ */ new Set(["-H", "-L", "-P"]);
|
|
2433
|
+
function findStartPoints(words, h) {
|
|
2434
|
+
const k = words.findIndex((w, i) => i > h && w !== null && FIND_EXEC_FLAGS.has(w));
|
|
2435
|
+
if (k < 0) return { k, starts: [] };
|
|
2436
|
+
const firstPredicate = words.findIndex(
|
|
2437
|
+
(w, i) => i > h && w !== null && w.startsWith("-") && !FIND_OPTIONS.has(w)
|
|
2438
|
+
);
|
|
2439
|
+
const end = firstPredicate > h ? firstPredicate : k;
|
|
2440
|
+
return { k, starts: positionalAfter(words, h + 1, end) };
|
|
2441
|
+
}
|
|
2442
|
+
function copySourcePaths(words) {
|
|
2443
|
+
const h = unwrapCommandHead(words);
|
|
2444
|
+
const fi = words.findIndex((w, i) => i <= h && baseWord(w) === "find");
|
|
2445
|
+
if (fi >= 0) {
|
|
2446
|
+
const { k, starts } = findStartPoints(words, fi);
|
|
2447
|
+
if (k < 0) return [];
|
|
2448
|
+
const action = unwrapCommandHead(words.slice(k + 1));
|
|
2449
|
+
return resolveCopyShape(words.slice(k + 1), action) ? starts : [];
|
|
2450
|
+
}
|
|
2451
|
+
if (!COPY_VERB_HEADS.has(baseWord(words[h]))) return [];
|
|
2452
|
+
const r = resolveCopyShape(words, h);
|
|
2453
|
+
if (!r) return [];
|
|
2454
|
+
const { shape, last } = r;
|
|
2455
|
+
const args = positionedArgs(words, last + 1);
|
|
2456
|
+
const tail = words.slice(last + 1);
|
|
2457
|
+
const skipped = (a) => operandOf(a, shape.skipFlags);
|
|
2458
|
+
const targetDir = shape.targetDirFlag === true && tail.some((w) => w !== null && w.startsWith("-") && flagIs(w, ["t", "--target-directory"]));
|
|
2459
|
+
const targetOperand = (a) => targetDir && operandOf(a, ["t", "--target-directory"]);
|
|
2460
|
+
const lastOperand = [...tail].reverse().find((w) => w === null || !w.startsWith("-"));
|
|
2461
|
+
const dynamicDest = lastOperand === null;
|
|
2462
|
+
const destIsLastOperand = (shape.source === "allButLast" || shape.source === "first") && !targetDir && !dynamicDest;
|
|
2463
|
+
if (destIsLastOperand && typeof lastOperand === "string" && matchSensitivePath2(lastOperand))
|
|
2464
|
+
return [];
|
|
2465
|
+
let src;
|
|
2466
|
+
switch (shape.source) {
|
|
2467
|
+
case "all":
|
|
2468
|
+
src = args;
|
|
2469
|
+
break;
|
|
2470
|
+
case "first":
|
|
2471
|
+
src = targetDir ? args : args.slice(0, 1);
|
|
2472
|
+
break;
|
|
2473
|
+
case "flagOperand": {
|
|
2474
|
+
const inline = tail.filter((w) => w !== null && w.startsWith("--")).map((w) => flagInfo(w)).filter((f) => f.attached !== null && (shape.sourceFlags ?? []).includes(f.long ?? "")).map((f) => f.attached);
|
|
2475
|
+
return [
|
|
2476
|
+
...args.filter(
|
|
2477
|
+
(a) => flagIs(a.afterFlag, shape.sourceFlags ?? []) && flagInfo(a.afterFlag).attached === null
|
|
2478
|
+
).map((a) => a.value),
|
|
2479
|
+
...inline
|
|
2480
|
+
];
|
|
2481
|
+
}
|
|
2482
|
+
case "archive":
|
|
2483
|
+
src = archiveInputs(shape.archive, args, tail);
|
|
2484
|
+
break;
|
|
2485
|
+
case "allButLast":
|
|
2486
|
+
src = targetDir || dynamicDest ? args : args.slice(0, -1);
|
|
2487
|
+
break;
|
|
2488
|
+
}
|
|
2489
|
+
return src.filter((a) => !skipped(a) && !targetOperand(a)).map((a) => a.value);
|
|
2490
|
+
}
|
|
2491
|
+
function archiveInputs(kind, args, tail) {
|
|
2492
|
+
const first = args[0];
|
|
2493
|
+
const bareKey = first && first.afterFlag === null && TAR_MODE_WORD.test(first.value);
|
|
2494
|
+
if (kind === "tar") {
|
|
2495
|
+
const flagsText = tail.filter((w) => w !== null && w.startsWith("-")).join(" ");
|
|
2496
|
+
const mode = (bareKey ? first.value : "") + flagsText;
|
|
2497
|
+
const extracting = /x|t/.test(bareKey ? first.value.replace(/f/g, "") : "") || /(^|\s)-[a-zA-Z]*[xt]|--extract|--list|--get/.test(flagsText);
|
|
2498
|
+
const writing = /[cruA]/.test(bareKey ? first.value : "") || /(^|\s)-[a-zA-Z]*[cruA]|--create|--append|--update|--concatenate/.test(flagsText);
|
|
2499
|
+
if (extracting && !writing) return [];
|
|
2500
|
+
void mode;
|
|
2501
|
+
let i = 0;
|
|
2502
|
+
if (bareKey) {
|
|
2503
|
+
i = 1;
|
|
2504
|
+
const next = args[1];
|
|
2505
|
+
if (first.value.includes("f") && next && next.afterFlag === null) i = 2;
|
|
2506
|
+
}
|
|
2507
|
+
return args.slice(i);
|
|
2508
|
+
}
|
|
2509
|
+
if (kind === "zip") return first && first.afterFlag === "-" ? args : args.slice(1);
|
|
2510
|
+
if (kind === "ar") return bareKey ? args.slice(2) : args.slice(1);
|
|
2511
|
+
return args.slice(2);
|
|
2512
|
+
}
|
|
2513
|
+
function sampleCopyCommand(verb, src) {
|
|
2514
|
+
const shape = COPY_VERBS[verb];
|
|
2515
|
+
if (!shape) throw new Error(`not a copy verb: ${verb}`);
|
|
2516
|
+
const remote = /^(scp|rsync|aws |gsutil|gcloud|rclone|docker|kubectl)/.test(verb);
|
|
2517
|
+
switch (shape.source) {
|
|
2518
|
+
case "first":
|
|
2519
|
+
return `${verb} -s ${src} /tmp/n9-link`;
|
|
2520
|
+
case "all":
|
|
2521
|
+
return `${verb} -c ${src} > /tmp/n9-out`;
|
|
2522
|
+
case "flagOperand":
|
|
2523
|
+
return `${verb} -f ${src} -c n9`;
|
|
2524
|
+
case "archive":
|
|
2525
|
+
return shape.archive === "tar" ? `tar czf /tmp/n9-out.tgz ${src}` : shape.archive === "zip" ? `zip -r /tmp/n9-out.zip ${src}` : shape.archive === "ar" ? `ar rc /tmp/n9-out.a ${src}` : `7z a /tmp/n9-out.7z ${src}`;
|
|
2526
|
+
case "allButLast":
|
|
2527
|
+
return `${verb} ${src} ${remote ? verb.startsWith("scp") || verb === "rsync" ? "user@host.invalid:/tmp/" : verb.startsWith("docker") || verb.startsWith("kubectl") ? "ctr:/tmp/" : "remote:/n9/" : "/tmp/n9-copy"}`;
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
var COPY_RULE_OF = {
|
|
2531
|
+
"shield:project-jail:block-read-ssh": "shield:project-jail:review-copy-ssh",
|
|
2532
|
+
"shield:project-jail:block-read-aws": "shield:project-jail:review-copy-aws",
|
|
2533
|
+
"shield:project-jail:block-read-env": "shield:project-jail:review-copy-env",
|
|
2534
|
+
"shield:project-jail:review-read-credentials": "shield:project-jail:review-copy-credentials"
|
|
2535
|
+
};
|
|
2536
|
+
function copyVerdictOf(hit) {
|
|
2537
|
+
if (!hit) return null;
|
|
2538
|
+
const ruleName = COPY_RULE_OF[hit.ruleName];
|
|
2539
|
+
if (!ruleName) return null;
|
|
2540
|
+
return {
|
|
2541
|
+
ruleName,
|
|
2542
|
+
verdict: "review",
|
|
2543
|
+
reason: `Copying ${hit.path} moves a credential out of its jail (project-jail shield)`,
|
|
2544
|
+
path: hit.path
|
|
2545
|
+
};
|
|
2546
|
+
}
|
|
2547
|
+
function matchSensitivePath2(p) {
|
|
2548
|
+
for (const sp of SENSITIVE_PATH_RULES) {
|
|
2549
|
+
if (sp.match(p))
|
|
2550
|
+
return { ruleName: sp.rule, verdict: sp.verdict ?? "block", reason: sp.reason, path: p };
|
|
2551
|
+
}
|
|
2552
|
+
return null;
|
|
2553
|
+
}
|
|
2554
|
+
function baseWord(w) {
|
|
2555
|
+
return (w ?? "").split("/").pop()?.toLowerCase() ?? "";
|
|
2556
|
+
}
|
|
2557
|
+
var isReaderWord = (w) => w !== null && FS_READ_TOOLS.has(baseWord(w));
|
|
2558
|
+
var positionalAfter = (words, from, to = words.length) => positionedArgs(words, from, to).map((a) => a.value);
|
|
2559
|
+
function wrappedReadPaths(words, name) {
|
|
2560
|
+
if (name === "find") {
|
|
2561
|
+
const { k, starts } = findStartPoints(words, 0);
|
|
2562
|
+
return k > 0 && isReaderWord(words[k + 1] ?? null) ? starts : null;
|
|
2563
|
+
}
|
|
2564
|
+
if (!COMMAND_WRAPPERS.has(name) && !RUNNER_WRAPPERS.has(name)) return null;
|
|
2565
|
+
const h = unwrapCommandHead(words);
|
|
2566
|
+
return h > 0 && isReaderWord(words[h] ?? null) ? positionalAfter(words, h + 1) : null;
|
|
2567
|
+
}
|
|
2568
|
+
function literalShellPayload(words, name) {
|
|
2569
|
+
const h = COMMAND_WRAPPERS.has(name) || RUNNER_WRAPPERS.has(name) ? unwrapCommandHead(words) : 0;
|
|
2570
|
+
const head = (words[h] ?? "").split("/").pop()?.toLowerCase() ?? "";
|
|
2571
|
+
if (head === "eval") {
|
|
2572
|
+
const rest = words.slice(h + 1);
|
|
2573
|
+
if (rest.length === 0 || rest.some((w) => w === null)) return null;
|
|
2574
|
+
return rest.join(" ");
|
|
2575
|
+
}
|
|
2576
|
+
if (SHELL_INTERPRETERS.has(head)) {
|
|
2577
|
+
const c = words.findIndex((w, i) => i > h && w !== null && isInlineCodeFlag(head, w));
|
|
2578
|
+
if (c < 0) return null;
|
|
2579
|
+
return words[c + 1] ?? null;
|
|
2580
|
+
}
|
|
2581
|
+
return null;
|
|
2582
|
+
}
|
|
2583
|
+
function jailedRedirectRead(stmt) {
|
|
2584
|
+
const redirs = stmt.Redirs || [];
|
|
2585
|
+
for (const r of redirs) {
|
|
2586
|
+
if (!r || !REDIR_FILE_IN_OPS.has(r.Op)) continue;
|
|
2587
|
+
const p = resolveWordLiteral(r.Word);
|
|
2588
|
+
if (p === null || p === "") continue;
|
|
2589
|
+
const hit = matchSensitivePath2(p);
|
|
2590
|
+
if (hit) return hit;
|
|
2591
|
+
}
|
|
2592
|
+
return null;
|
|
2593
|
+
}
|
|
2290
2594
|
function analyzeShellCommand(command) {
|
|
2291
2595
|
const actions = [];
|
|
2292
2596
|
const paths = [];
|
|
@@ -2423,21 +2727,7 @@ function evaluateEgress(dests, policy) {
|
|
|
2423
2727
|
}
|
|
2424
2728
|
|
|
2425
2729
|
// src/policy/pipe-chain.ts
|
|
2426
|
-
var SOURCE_COMMANDS = /* @__PURE__ */ new Set([
|
|
2427
|
-
"cat",
|
|
2428
|
-
"head",
|
|
2429
|
-
"tail",
|
|
2430
|
-
"grep",
|
|
2431
|
-
"awk",
|
|
2432
|
-
"sed",
|
|
2433
|
-
"cut",
|
|
2434
|
-
"sort",
|
|
2435
|
-
"tee",
|
|
2436
|
-
"less",
|
|
2437
|
-
"more",
|
|
2438
|
-
"strings",
|
|
2439
|
-
"xxd"
|
|
2440
|
-
]);
|
|
2730
|
+
var SOURCE_COMMANDS = /* @__PURE__ */ new Set([...FS_READ_TOOLS, "tee"]);
|
|
2441
2731
|
var SINK_COMMANDS = /* @__PURE__ */ new Set([
|
|
2442
2732
|
"curl",
|
|
2443
2733
|
"wget",
|
|
@@ -2468,16 +2758,25 @@ var OBFUSCATORS = /* @__PURE__ */ new Set([
|
|
|
2468
2758
|
"node"
|
|
2469
2759
|
]);
|
|
2470
2760
|
var SENSITIVE_PATTERNS = [
|
|
2471
|
-
/
|
|
2472
|
-
|
|
2761
|
+
// Kept in step with the AST tier and dlp/ -- see jail-both-doors.test.ts.
|
|
2762
|
+
/(?:^|\/)\.env(?![\w-])(?:[\w.-]*\.local$|(?!\.(?:example|sample|template)\b)(?!\.test$)[\w.-]*$)/i,
|
|
2763
|
+
// .env chain; fixtures exempt unless .local
|
|
2473
2764
|
/id_rsa|id_ed25519|id_ecdsa|id_dsa/i,
|
|
2474
2765
|
// SSH private keys
|
|
2475
2766
|
/\.pem$|\.key$|\.p12$|\.pfx$/i,
|
|
2476
2767
|
// certificate files
|
|
2477
|
-
/
|
|
2478
|
-
//
|
|
2479
|
-
/
|
|
2480
|
-
//
|
|
2768
|
+
// The `$` half mirrors shell/index.ts's SENSITIVE_PATH_RULES: a file INSIDE
|
|
2769
|
+
// the directory counts wherever it appears, while the directory ITSELF counts
|
|
2770
|
+
// only when the path is ROOTED (`~/.ssh`, `/home/u/.ssh`) -- an unrooted
|
|
2771
|
+
// `config/.ssh` is more likely a search pattern than a read. These are
|
|
2772
|
+
// extracted TOKENS (see `args.some(isSensitivePath)` below), the same input
|
|
2773
|
+
// contract as the shell tier, so the same boundary is the right one.
|
|
2774
|
+
// Without it `grep -r x ~/.ssh | curl -d @-` scored one tier BELOW the
|
|
2775
|
+
// identical pipeline naming a file inside that directory.
|
|
2776
|
+
/(?:^|\/)\.ssh\/|^(?:[~/]|[A-Za-z]:).*\/\.ssh$/i,
|
|
2777
|
+
// ~/.ssh/ and ~/.ssh
|
|
2778
|
+
/(?:^|\/)\.aws\/credentials|^(?:[~/]|[A-Za-z]:).*\/\.aws$/i,
|
|
2779
|
+
// AWS creds + dir
|
|
2481
2780
|
/(?:^|\/)\.netrc$/i,
|
|
2482
2781
|
// netrc (stores HTTP credentials)
|
|
2483
2782
|
/(?:^|\/)(passwd|shadow|sudoers)$/i,
|
|
@@ -2511,8 +2810,8 @@ function splitOnPipe(cmd) {
|
|
|
2511
2810
|
if (current.trim()) segments2.push(current.trim());
|
|
2512
2811
|
return segments2.filter(Boolean);
|
|
2513
2812
|
}
|
|
2514
|
-
function positionalTokens(
|
|
2515
|
-
return
|
|
2813
|
+
function positionalTokens(tokens) {
|
|
2814
|
+
return tokens.slice(1).filter((t) => !t.startsWith("-") && !t.startsWith("@") && t.length > 0);
|
|
2516
2815
|
}
|
|
2517
2816
|
function analyzePipeChain(command) {
|
|
2518
2817
|
const segments2 = splitOnPipe(command);
|
|
@@ -2535,8 +2834,10 @@ function analyzePipeChain(command) {
|
|
|
2535
2834
|
for (const segment of segments2) {
|
|
2536
2835
|
const tokens = segment.split(/\s+/).filter(Boolean);
|
|
2537
2836
|
if (tokens.length === 0) continue;
|
|
2538
|
-
const
|
|
2539
|
-
const
|
|
2837
|
+
const h = unwrapCommandHead(tokens);
|
|
2838
|
+
const head = h < tokens.length ? h : 0;
|
|
2839
|
+
const binary = tokens[head].toLowerCase();
|
|
2840
|
+
const args = positionalTokens(tokens.slice(head));
|
|
2540
2841
|
if (SOURCE_COMMANDS.has(binary)) {
|
|
2541
2842
|
sourceFiles.push(...args);
|
|
2542
2843
|
if (args.some(isSensitivePath)) hasSensitiveSource = true;
|
|
@@ -3146,6 +3447,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
3146
3447
|
const shellShaped = isBashTool(toolName) || inspectsShellCommand(toolName, config.policy.toolInspection);
|
|
3147
3448
|
const shellShapedCommand = shellShaped ? isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : extractShellCommand(toolName, args, config.policy.toolInspection) : null;
|
|
3148
3449
|
const bashCommand = agent !== "Terminal" ? shellShapedCommand : null;
|
|
3450
|
+
let pendingAstReview;
|
|
3149
3451
|
if (bashCommand !== null) {
|
|
3150
3452
|
const pipeVerdict = pipeChainVerdict(
|
|
3151
3453
|
bashCommand,
|
|
@@ -3157,7 +3459,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
3157
3459
|
if (fsVerdict) {
|
|
3158
3460
|
const isShieldRule = fsVerdict.ruleName.startsWith("shield:");
|
|
3159
3461
|
const labelPrefix = isShieldRule ? "project-jail (AST)" : "Node9 (AST)";
|
|
3160
|
-
|
|
3462
|
+
const astVerdict = {
|
|
3161
3463
|
decision: fsVerdict.verdict,
|
|
3162
3464
|
blockedByLabel: `${labelPrefix}: ${fsVerdict.ruleName}`,
|
|
3163
3465
|
reason: fsVerdict.reason,
|
|
@@ -3165,6 +3467,8 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
3165
3467
|
ruleName: fsVerdict.ruleName,
|
|
3166
3468
|
ruleDescription: fsVerdict.reason
|
|
3167
3469
|
};
|
|
3470
|
+
if (fsVerdict.verdict === "block") return astVerdict;
|
|
3471
|
+
pendingAstReview = astVerdict;
|
|
3168
3472
|
}
|
|
3169
3473
|
const sqlAction = resolveCheck(config.policy.commandChecks?.sqlDdl);
|
|
3170
3474
|
const sqlVerdict = sqlAction === "off" ? null : analyzeSqlDestructive(bashCommand);
|
|
@@ -3207,7 +3511,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
3207
3511
|
const matchedRule = resolvePinned(matches);
|
|
3208
3512
|
if (matchedRule) {
|
|
3209
3513
|
if (matchedRule.verdict === "allow")
|
|
3210
|
-
return { decision: "allow", ruleName: matchedRule.name ?? matchedRule.tool };
|
|
3514
|
+
return pendingAstReview ?? { decision: "allow", ruleName: matchedRule.name ?? matchedRule.tool };
|
|
3211
3515
|
return {
|
|
3212
3516
|
decision: matchedRule.verdict,
|
|
3213
3517
|
blockedByLabel: `Smart Rule: ${matchedRule.name ?? matchedRule.tool}`,
|
|
@@ -3234,6 +3538,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
3234
3538
|
allTokens = analyzed.allTokens;
|
|
3235
3539
|
pathTokens = analyzed.paths;
|
|
3236
3540
|
const candidates = [];
|
|
3541
|
+
if (pendingAstReview) candidates.push(pendingAstReview);
|
|
3237
3542
|
const evalVerdict = detectDangerousShellExec(shellCommand);
|
|
3238
3543
|
if (evalVerdict === "block") {
|
|
3239
3544
|
return {
|
|
@@ -3979,7 +4284,7 @@ var project_jail_default = {
|
|
|
3979
4284
|
{
|
|
3980
4285
|
field: "command",
|
|
3981
4286
|
op: "matches",
|
|
3982
|
-
value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|sort|uniq|tac|nl|dd)\\s+.*?\\.ssh[\\/\\\\]",
|
|
4287
|
+
value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|base64|sort|uniq|tac|nl|dd)\\s+.*?\\.ssh[\\/\\\\]",
|
|
3983
4288
|
flags: "i"
|
|
3984
4289
|
}
|
|
3985
4290
|
],
|
|
@@ -3993,7 +4298,7 @@ var project_jail_default = {
|
|
|
3993
4298
|
{
|
|
3994
4299
|
field: "command",
|
|
3995
4300
|
op: "matches",
|
|
3996
|
-
value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|sort|uniq|tac|nl|dd)\\s+.*?\\.aws[\\/\\\\]",
|
|
4301
|
+
value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|base64|sort|uniq|tac|nl|dd)\\s+.*?\\.aws[\\/\\\\]",
|
|
3997
4302
|
flags: "i"
|
|
3998
4303
|
}
|
|
3999
4304
|
],
|
|
@@ -4007,7 +4312,7 @@ var project_jail_default = {
|
|
|
4007
4312
|
{
|
|
4008
4313
|
field: "command",
|
|
4009
4314
|
op: "matches",
|
|
4010
|
-
value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|sort|uniq|tac|nl|dd)\\s+.*?\\.env(\\.(local|production|staging|development|production\\.local|staging\\.local|development\\.local))?(?=\\s|$|[;&|>)<])",
|
|
4315
|
+
value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|base64|sort|uniq|tac|nl|dd)\\s+.*?\\.env(\\.(local|production|staging|development|production\\.local|staging\\.local|development\\.local))?(?=\\s|$|[;&|>)<])",
|
|
4011
4316
|
flags: "i"
|
|
4012
4317
|
}
|
|
4013
4318
|
],
|
|
@@ -4021,7 +4326,7 @@ var project_jail_default = {
|
|
|
4021
4326
|
{
|
|
4022
4327
|
field: "command",
|
|
4023
4328
|
op: "matches",
|
|
4024
|
-
value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|sort|uniq|tac|nl|dd)\\s+.*(credentials\\.json|\\.netrc|\\.npmrc|\\.docker[\\/\\\\]config\\.json|gcloud[\\/\\\\]credentials)",
|
|
4329
|
+
value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|base64|sort|uniq|tac|nl|dd)\\s+.*(credentials\\.json|\\.netrc|\\.npmrc|\\.docker[\\/\\\\]config\\.json|gcloud[\\/\\\\]credentials)",
|
|
4025
4330
|
flags: "i"
|
|
4026
4331
|
}
|
|
4027
4332
|
],
|
|
@@ -4035,7 +4340,7 @@ var project_jail_default = {
|
|
|
4035
4340
|
{
|
|
4036
4341
|
field: "file_path",
|
|
4037
4342
|
op: "matches",
|
|
4038
|
-
value: "(
|
|
4343
|
+
value: "([\\/\\\\]\\.ssh([\\/\\\\]|$)|^\\.ssh[\\/\\\\])",
|
|
4039
4344
|
flags: "i"
|
|
4040
4345
|
}
|
|
4041
4346
|
],
|
|
@@ -4049,7 +4354,7 @@ var project_jail_default = {
|
|
|
4049
4354
|
{
|
|
4050
4355
|
field: "file_path",
|
|
4051
4356
|
op: "matches",
|
|
4052
|
-
value: "(
|
|
4357
|
+
value: "([\\/\\\\]\\.aws([\\/\\\\]|$)|^\\.aws[\\/\\\\])",
|
|
4053
4358
|
flags: "i"
|
|
4054
4359
|
}
|
|
4055
4360
|
],
|
|
@@ -4063,7 +4368,7 @@ var project_jail_default = {
|
|
|
4063
4368
|
{
|
|
4064
4369
|
field: "file_path",
|
|
4065
4370
|
op: "matches",
|
|
4066
|
-
value: "(^|[\\/\\\\])\\.env(
|
|
4371
|
+
value: "(^|[\\/\\\\])\\.env(?![\\w-])(?:[\\w.-]*\\.local$|(?!\\.(example|sample|template)\\b)(?!\\.test$)[\\w.-]*$)",
|
|
4067
4372
|
flags: "i"
|
|
4068
4373
|
}
|
|
4069
4374
|
],
|
|
@@ -4340,6 +4645,12 @@ function classifyRuleSeverity(name, verdict) {
|
|
|
4340
4645
|
"read-ssh",
|
|
4341
4646
|
"read-gcp",
|
|
4342
4647
|
"read-cred",
|
|
4648
|
+
// Stage 4 (2026-09-11): a copy of a credential file scores like a READ of it --
|
|
4649
|
+
// read-ssh/aws/cred are critical, so copy-ssh/aws/cred are; read-env is high
|
|
4650
|
+
// (below), so copy-env joins the high list, not this one (/code-review).
|
|
4651
|
+
"copy-ssh",
|
|
4652
|
+
"copy-aws",
|
|
4653
|
+
"copy-cred",
|
|
4343
4654
|
"delete-repo",
|
|
4344
4655
|
"helm-uninstall",
|
|
4345
4656
|
"drop-table",
|
|
@@ -4352,6 +4663,7 @@ function classifyRuleSeverity(name, verdict) {
|
|
|
4352
4663
|
];
|
|
4353
4664
|
if (criticalPatterns.some((p) => n.includes(p))) return "critical";
|
|
4354
4665
|
const highPatterns = [
|
|
4666
|
+
"copy-env",
|
|
4355
4667
|
"force-push",
|
|
4356
4668
|
"force_push",
|
|
4357
4669
|
"git-destructive",
|
|
@@ -4369,6 +4681,11 @@ function narrativeRuleLabel(name) {
|
|
|
4369
4681
|
const map = {
|
|
4370
4682
|
"read-aws": "AWS credentials read",
|
|
4371
4683
|
"read-ssh": "SSH private key read",
|
|
4684
|
+
// Stage 4 copy twins, so `scan --narrative` prints a label, not a raw slug.
|
|
4685
|
+
"copy-ssh": "SSH private key copied out",
|
|
4686
|
+
"copy-aws": "AWS credentials copied out",
|
|
4687
|
+
"copy-env": ".env file copied out",
|
|
4688
|
+
"copy-cred": "credential file copied out",
|
|
4372
4689
|
"read-gcp": "GCP credentials read",
|
|
4373
4690
|
"read-cred": "credential file read",
|
|
4374
4691
|
"delete-repo": "GitHub repository deletion",
|
|
@@ -4506,7 +4823,7 @@ function summarizeBlast(result, opts = {}) {
|
|
|
4506
4823
|
// src/scan/destructive-regex.ts
|
|
4507
4824
|
var DESTRUCTIVE_OP_RE = /\brm\s+-[rRf]+\b|\bDROP\s+(TABLE|DATABASE|COLLECTION|SCHEMA)\b|\bTRUNCATE\s+TABLE\b|\bgit\s+push\s+(--force|-f)\b|\bFLUSHALL\b|\bFLUSHDB\b|\bkubectl\s+delete\b|\bhelm\s+uninstall\b/i;
|
|
4508
4825
|
var PRIVILEGE_ESCALATION_RE = /\bchmod\s+(0?777|\+x)\b|\bchown\s+root\b/i;
|
|
4509
|
-
var SENSITIVE_PATH_RE =
|
|
4826
|
+
var SENSITIVE_PATH_RE = /[\\/]\.aws(?:[\\/]|$)|^\.aws[\\/]|[\\/]\.ssh(?:[\\/]|$)|^\.ssh[\\/]|(?:^|[\\/])\.env(?![\w-])(?:[\w.-]*\.local$|(?!\.(?:example|sample|template)\b)(?!\.test$)[\w.-]*$)|\.config\/gcloud\/credentials\.db\b|\.docker\/config\.json\b|\.netrc\b|\.npmrc\b|\.node9\/credentials\.json\b/i;
|
|
4510
4827
|
var FILE_TOOLS = /* @__PURE__ */ new Set([
|
|
4511
4828
|
"read",
|
|
4512
4829
|
"read_file",
|
|
@@ -4766,8 +5083,8 @@ function matchCanaryArgs(args, values) {
|
|
|
4766
5083
|
|
|
4767
5084
|
// src/scan/canonical.ts
|
|
4768
5085
|
var LONG_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
|
|
4769
|
-
var CANONICAL_EXTRACTOR_VERSION = "canonical-
|
|
4770
|
-
var CANONICAL_EXTRACTOR_HASH = "
|
|
5086
|
+
var CANONICAL_EXTRACTOR_VERSION = "canonical-v14";
|
|
5087
|
+
var CANONICAL_EXTRACTOR_HASH = "8b5729fe236a195b";
|
|
4771
5088
|
var DEDUPE_PREVIEW_LEN = 120;
|
|
4772
5089
|
function extractCanonicalFindings(call, ctx) {
|
|
4773
5090
|
const out = [];
|
|
@@ -5143,6 +5460,8 @@ var ENGINE_VERSION = "1.4.0";
|
|
|
5143
5460
|
CANARY_MIN_LENGTH,
|
|
5144
5461
|
CANONICAL_EXTRACTOR_HASH,
|
|
5145
5462
|
CANONICAL_EXTRACTOR_VERSION,
|
|
5463
|
+
COMMAND_WRAPPERS,
|
|
5464
|
+
COPY_VERBS,
|
|
5146
5465
|
COST_PER_LOOP_ITER_USD,
|
|
5147
5466
|
DEFAULT_EGRESS_ALLOWLIST,
|
|
5148
5467
|
DESTINATION_ARGS,
|
|
@@ -5156,6 +5475,7 @@ var ENGINE_VERSION = "1.4.0";
|
|
|
5156
5475
|
LONG_OUTPUT_THRESHOLD_BYTES,
|
|
5157
5476
|
LOOP_MAX_RECORDS,
|
|
5158
5477
|
LOOP_THRESHOLD_FOR_WASTE,
|
|
5478
|
+
NET_BINARIES,
|
|
5159
5479
|
PRIVILEGE_ESCALATION_RE,
|
|
5160
5480
|
REALTIME_PII_PATTERNS,
|
|
5161
5481
|
SCAN_SIGNAL_WEIGHTS,
|
|
@@ -5211,9 +5531,11 @@ var ENGINE_VERSION = "1.4.0";
|
|
|
5211
5531
|
normalizeIpLiteral,
|
|
5212
5532
|
parseAllSshHostsFromCommand,
|
|
5213
5533
|
parseDestHost,
|
|
5534
|
+
positionedArgs,
|
|
5214
5535
|
previewArgs,
|
|
5215
5536
|
redactText,
|
|
5216
5537
|
resolvePinned,
|
|
5538
|
+
sampleCopyCommand,
|
|
5217
5539
|
scanArgs,
|
|
5218
5540
|
scanInjection,
|
|
5219
5541
|
scanText,
|
|
@@ -5226,6 +5548,7 @@ var ENGINE_VERSION = "1.4.0";
|
|
|
5226
5548
|
toScanFinding,
|
|
5227
5549
|
toolMatchesRule,
|
|
5228
5550
|
truncateBlastPath,
|
|
5551
|
+
unwrapCommandHead,
|
|
5229
5552
|
validateOverrides,
|
|
5230
5553
|
validateRegex,
|
|
5231
5554
|
validateShieldDefinition
|