@node9/proxy 2.13.1 → 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/cli.js +262 -26
- package/dist/cli.mjs +262 -26
- package/dist/dashboard.mjs +246 -24
- package/dist/index.js +219 -21
- package/dist/index.mjs +219 -21
- package/dist/scan-ink.mjs +44 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1291,20 +1291,32 @@ function isProtectedHomePath(rawPath) {
|
|
|
1291
1291
|
}
|
|
1292
1292
|
return true;
|
|
1293
1293
|
}
|
|
1294
|
-
function
|
|
1295
|
-
const
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
const name = (words[0] ?? "").toLowerCase();
|
|
1299
|
-
const flags = [];
|
|
1300
|
-
const paths = [];
|
|
1301
|
-
for (let i = 1; i < words.length; i++) {
|
|
1294
|
+
function positionedArgs(words, from = 1, to = words.length) {
|
|
1295
|
+
const out = [];
|
|
1296
|
+
let afterFlag = null;
|
|
1297
|
+
for (let i = from; i < to; i++) {
|
|
1302
1298
|
const v = words[i];
|
|
1303
|
-
if (v === null)
|
|
1304
|
-
|
|
1305
|
-
|
|
1299
|
+
if (v === null) {
|
|
1300
|
+
afterFlag = null;
|
|
1301
|
+
continue;
|
|
1302
|
+
}
|
|
1303
|
+
if (v.startsWith("-")) {
|
|
1304
|
+
afterFlag = v;
|
|
1305
|
+
continue;
|
|
1306
|
+
}
|
|
1307
|
+
out.push({ value: v, index: out.length, argv: i, afterFlag });
|
|
1308
|
+
afterFlag = null;
|
|
1306
1309
|
}
|
|
1307
|
-
return
|
|
1310
|
+
return out;
|
|
1311
|
+
}
|
|
1312
|
+
function extractLiteralArgs(callExpr) {
|
|
1313
|
+
const rawArgs = callExpr.Args || [];
|
|
1314
|
+
if (rawArgs.length === 0) return { name: "", flags: [], paths: [], words: [], args: [] };
|
|
1315
|
+
const words = rawArgs.map((a) => resolveWordLiteral(a));
|
|
1316
|
+
const name = baseWord(words[0]);
|
|
1317
|
+
const flags = words.slice(1).filter((w) => w !== null && w.startsWith("-"));
|
|
1318
|
+
const args = positionedArgs(words);
|
|
1319
|
+
return { name, flags, paths: args.map((a) => a.value), words, args };
|
|
1308
1320
|
}
|
|
1309
1321
|
function resolveWordLiteral(w) {
|
|
1310
1322
|
const parts = w?.Parts || [];
|
|
@@ -1623,6 +1635,9 @@ function analyzeFsOperationImpl(command, depth = 0) {
|
|
|
1623
1635
|
if (result?.verdict === "block") return false;
|
|
1624
1636
|
}
|
|
1625
1637
|
}
|
|
1638
|
+
for (const p of copySourcePaths(words)) {
|
|
1639
|
+
result = stricter(result, copyVerdictOf(matchSensitivePath2(p)));
|
|
1640
|
+
}
|
|
1626
1641
|
return true;
|
|
1627
1642
|
});
|
|
1628
1643
|
return result;
|
|
@@ -1635,6 +1650,133 @@ function stricter(a, b) {
|
|
|
1635
1650
|
if (!b) return a;
|
|
1636
1651
|
return b.verdict === "block" && a.verdict !== "block" ? b : a;
|
|
1637
1652
|
}
|
|
1653
|
+
function flagInfo(w) {
|
|
1654
|
+
if (w.startsWith("--")) {
|
|
1655
|
+
const eq = w.indexOf("=");
|
|
1656
|
+
return eq < 0 ? { letter: null, long: w, attached: null } : { letter: null, long: w.slice(0, eq), attached: w.slice(eq + 1) };
|
|
1657
|
+
}
|
|
1658
|
+
const m = /^-([a-zA-Z]+)(.*)$/.exec(w);
|
|
1659
|
+
if (!m) return { letter: null, long: null, attached: null };
|
|
1660
|
+
return { letter: m[1][m[1].length - 1], long: null, attached: m[2] === "" ? null : m[2] };
|
|
1661
|
+
}
|
|
1662
|
+
function flagIs(w, names) {
|
|
1663
|
+
if (w === null) return false;
|
|
1664
|
+
const f = flagInfo(w);
|
|
1665
|
+
return names.some((n) => n.startsWith("--") ? f.long === n : f.letter === n);
|
|
1666
|
+
}
|
|
1667
|
+
function operandOf(a, names) {
|
|
1668
|
+
if (!names || a.afterFlag === null) return false;
|
|
1669
|
+
return flagIs(a.afterFlag, names) && flagInfo(a.afterFlag).attached === null;
|
|
1670
|
+
}
|
|
1671
|
+
function resolveCopyShape(words, h) {
|
|
1672
|
+
const verb = baseWord(words[h]);
|
|
1673
|
+
if (!verb) return null;
|
|
1674
|
+
const direct = COPY_VERBS[verb];
|
|
1675
|
+
if (direct) return { shape: direct, last: h };
|
|
1676
|
+
const slots = positionedArgs(words, h + 1);
|
|
1677
|
+
for (let i = 0; i < slots.length; i++) {
|
|
1678
|
+
for (let n = 3; n >= 1; n--) {
|
|
1679
|
+
const part = slots.slice(i, i + n);
|
|
1680
|
+
if (part.length < n) continue;
|
|
1681
|
+
const key = [verb, ...part.map((a) => a.value.toLowerCase())].join(" ");
|
|
1682
|
+
const shape = COPY_VERBS[key];
|
|
1683
|
+
if (shape) return { shape, last: part[n - 1].argv };
|
|
1684
|
+
}
|
|
1685
|
+
if (slots[i].afterFlag === null) return null;
|
|
1686
|
+
}
|
|
1687
|
+
return null;
|
|
1688
|
+
}
|
|
1689
|
+
function findStartPoints(words, h) {
|
|
1690
|
+
const k = words.findIndex((w, i) => i > h && w !== null && FIND_EXEC_FLAGS.has(w));
|
|
1691
|
+
if (k < 0) return { k, starts: [] };
|
|
1692
|
+
const firstPredicate = words.findIndex(
|
|
1693
|
+
(w, i) => i > h && w !== null && w.startsWith("-") && !FIND_OPTIONS.has(w)
|
|
1694
|
+
);
|
|
1695
|
+
const end = firstPredicate > h ? firstPredicate : k;
|
|
1696
|
+
return { k, starts: positionalAfter(words, h + 1, end) };
|
|
1697
|
+
}
|
|
1698
|
+
function copySourcePaths(words) {
|
|
1699
|
+
const h = unwrapCommandHead(words);
|
|
1700
|
+
const fi = words.findIndex((w, i) => i <= h && baseWord(w) === "find");
|
|
1701
|
+
if (fi >= 0) {
|
|
1702
|
+
const { k, starts } = findStartPoints(words, fi);
|
|
1703
|
+
if (k < 0) return [];
|
|
1704
|
+
const action = unwrapCommandHead(words.slice(k + 1));
|
|
1705
|
+
return resolveCopyShape(words.slice(k + 1), action) ? starts : [];
|
|
1706
|
+
}
|
|
1707
|
+
if (!COPY_VERB_HEADS.has(baseWord(words[h]))) return [];
|
|
1708
|
+
const r = resolveCopyShape(words, h);
|
|
1709
|
+
if (!r) return [];
|
|
1710
|
+
const { shape, last } = r;
|
|
1711
|
+
const args = positionedArgs(words, last + 1);
|
|
1712
|
+
const tail = words.slice(last + 1);
|
|
1713
|
+
const skipped = (a) => operandOf(a, shape.skipFlags);
|
|
1714
|
+
const targetDir = shape.targetDirFlag === true && tail.some((w) => w !== null && w.startsWith("-") && flagIs(w, ["t", "--target-directory"]));
|
|
1715
|
+
const targetOperand = (a) => targetDir && operandOf(a, ["t", "--target-directory"]);
|
|
1716
|
+
const lastOperand = [...tail].reverse().find((w) => w === null || !w.startsWith("-"));
|
|
1717
|
+
const dynamicDest = lastOperand === null;
|
|
1718
|
+
const destIsLastOperand = (shape.source === "allButLast" || shape.source === "first") && !targetDir && !dynamicDest;
|
|
1719
|
+
if (destIsLastOperand && typeof lastOperand === "string" && matchSensitivePath2(lastOperand))
|
|
1720
|
+
return [];
|
|
1721
|
+
let src;
|
|
1722
|
+
switch (shape.source) {
|
|
1723
|
+
case "all":
|
|
1724
|
+
src = args;
|
|
1725
|
+
break;
|
|
1726
|
+
case "first":
|
|
1727
|
+
src = targetDir ? args : args.slice(0, 1);
|
|
1728
|
+
break;
|
|
1729
|
+
case "flagOperand": {
|
|
1730
|
+
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);
|
|
1731
|
+
return [
|
|
1732
|
+
...args.filter(
|
|
1733
|
+
(a) => flagIs(a.afterFlag, shape.sourceFlags ?? []) && flagInfo(a.afterFlag).attached === null
|
|
1734
|
+
).map((a) => a.value),
|
|
1735
|
+
...inline
|
|
1736
|
+
];
|
|
1737
|
+
}
|
|
1738
|
+
case "archive":
|
|
1739
|
+
src = archiveInputs(shape.archive, args, tail);
|
|
1740
|
+
break;
|
|
1741
|
+
case "allButLast":
|
|
1742
|
+
src = targetDir || dynamicDest ? args : args.slice(0, -1);
|
|
1743
|
+
break;
|
|
1744
|
+
}
|
|
1745
|
+
return src.filter((a) => !skipped(a) && !targetOperand(a)).map((a) => a.value);
|
|
1746
|
+
}
|
|
1747
|
+
function archiveInputs(kind, args, tail) {
|
|
1748
|
+
const first = args[0];
|
|
1749
|
+
const bareKey = first && first.afterFlag === null && TAR_MODE_WORD.test(first.value);
|
|
1750
|
+
if (kind === "tar") {
|
|
1751
|
+
const flagsText = tail.filter((w) => w !== null && w.startsWith("-")).join(" ");
|
|
1752
|
+
const mode = (bareKey ? first.value : "") + flagsText;
|
|
1753
|
+
const extracting = /x|t/.test(bareKey ? first.value.replace(/f/g, "") : "") || /(^|\s)-[a-zA-Z]*[xt]|--extract|--list|--get/.test(flagsText);
|
|
1754
|
+
const writing = /[cruA]/.test(bareKey ? first.value : "") || /(^|\s)-[a-zA-Z]*[cruA]|--create|--append|--update|--concatenate/.test(flagsText);
|
|
1755
|
+
if (extracting && !writing) return [];
|
|
1756
|
+
void mode;
|
|
1757
|
+
let i = 0;
|
|
1758
|
+
if (bareKey) {
|
|
1759
|
+
i = 1;
|
|
1760
|
+
const next = args[1];
|
|
1761
|
+
if (first.value.includes("f") && next && next.afterFlag === null) i = 2;
|
|
1762
|
+
}
|
|
1763
|
+
return args.slice(i);
|
|
1764
|
+
}
|
|
1765
|
+
if (kind === "zip") return first && first.afterFlag === "-" ? args : args.slice(1);
|
|
1766
|
+
if (kind === "ar") return bareKey ? args.slice(2) : args.slice(1);
|
|
1767
|
+
return args.slice(2);
|
|
1768
|
+
}
|
|
1769
|
+
function copyVerdictOf(hit) {
|
|
1770
|
+
if (!hit) return null;
|
|
1771
|
+
const ruleName = COPY_RULE_OF[hit.ruleName];
|
|
1772
|
+
if (!ruleName) return null;
|
|
1773
|
+
return {
|
|
1774
|
+
ruleName,
|
|
1775
|
+
verdict: "review",
|
|
1776
|
+
reason: `Copying ${hit.path} moves a credential out of its jail (project-jail shield)`,
|
|
1777
|
+
path: hit.path
|
|
1778
|
+
};
|
|
1779
|
+
}
|
|
1638
1780
|
function matchSensitivePath2(p) {
|
|
1639
1781
|
for (const sp of SENSITIVE_PATH_RULES) {
|
|
1640
1782
|
if (sp.match(p))
|
|
@@ -1642,12 +1784,13 @@ function matchSensitivePath2(p) {
|
|
|
1642
1784
|
}
|
|
1643
1785
|
return null;
|
|
1644
1786
|
}
|
|
1787
|
+
function baseWord(w) {
|
|
1788
|
+
return (w ?? "").split("/").pop()?.toLowerCase() ?? "";
|
|
1789
|
+
}
|
|
1645
1790
|
function wrappedReadPaths(words, name) {
|
|
1646
1791
|
if (name === "find") {
|
|
1647
|
-
const k = words
|
|
1648
|
-
|
|
1649
|
-
const firstPredicate = words.findIndex((w, i) => i > 0 && w !== null && w.startsWith("-"));
|
|
1650
|
-
return positionalAfter(words, 1, firstPredicate > 0 ? firstPredicate : k);
|
|
1792
|
+
const { k, starts } = findStartPoints(words, 0);
|
|
1793
|
+
return k > 0 && isReaderWord(words[k + 1] ?? null) ? starts : null;
|
|
1651
1794
|
}
|
|
1652
1795
|
if (!COMMAND_WRAPPERS.has(name) && !RUNNER_WRAPPERS.has(name)) return null;
|
|
1653
1796
|
const h = unwrapCommandHead(words);
|
|
@@ -2319,6 +2462,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2319
2462
|
const shellShaped = isBashTool(toolName) || inspectsShellCommand(toolName, config.policy.toolInspection);
|
|
2320
2463
|
const shellShapedCommand = shellShaped ? isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : extractShellCommand(toolName, args, config.policy.toolInspection) : null;
|
|
2321
2464
|
const bashCommand = agent !== "Terminal" ? shellShapedCommand : null;
|
|
2465
|
+
let pendingAstReview;
|
|
2322
2466
|
if (bashCommand !== null) {
|
|
2323
2467
|
const pipeVerdict = pipeChainVerdict(
|
|
2324
2468
|
bashCommand,
|
|
@@ -2330,7 +2474,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2330
2474
|
if (fsVerdict) {
|
|
2331
2475
|
const isShieldRule = fsVerdict.ruleName.startsWith("shield:");
|
|
2332
2476
|
const labelPrefix = isShieldRule ? "project-jail (AST)" : "Node9 (AST)";
|
|
2333
|
-
|
|
2477
|
+
const astVerdict = {
|
|
2334
2478
|
decision: fsVerdict.verdict,
|
|
2335
2479
|
blockedByLabel: `${labelPrefix}: ${fsVerdict.ruleName}`,
|
|
2336
2480
|
reason: fsVerdict.reason,
|
|
@@ -2338,6 +2482,8 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2338
2482
|
ruleName: fsVerdict.ruleName,
|
|
2339
2483
|
ruleDescription: fsVerdict.reason
|
|
2340
2484
|
};
|
|
2485
|
+
if (fsVerdict.verdict === "block") return astVerdict;
|
|
2486
|
+
pendingAstReview = astVerdict;
|
|
2341
2487
|
}
|
|
2342
2488
|
const sqlAction = resolveCheck(config.policy.commandChecks?.sqlDdl);
|
|
2343
2489
|
const sqlVerdict = sqlAction === "off" ? null : analyzeSqlDestructive(bashCommand);
|
|
@@ -2380,7 +2526,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2380
2526
|
const matchedRule = resolvePinned(matches);
|
|
2381
2527
|
if (matchedRule) {
|
|
2382
2528
|
if (matchedRule.verdict === "allow")
|
|
2383
|
-
return { decision: "allow", ruleName: matchedRule.name ?? matchedRule.tool };
|
|
2529
|
+
return pendingAstReview ?? { decision: "allow", ruleName: matchedRule.name ?? matchedRule.tool };
|
|
2384
2530
|
return {
|
|
2385
2531
|
decision: matchedRule.verdict,
|
|
2386
2532
|
blockedByLabel: `Smart Rule: ${matchedRule.name ?? matchedRule.tool}`,
|
|
@@ -2407,6 +2553,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2407
2553
|
allTokens = analyzed.allTokens;
|
|
2408
2554
|
pathTokens = analyzed.paths;
|
|
2409
2555
|
const candidates2 = [];
|
|
2556
|
+
if (pendingAstReview) candidates2.push(pendingAstReview);
|
|
2410
2557
|
const evalVerdict = detectDangerousShellExec(shellCommand);
|
|
2411
2558
|
if (evalVerdict === "block") {
|
|
2412
2559
|
return {
|
|
@@ -2703,6 +2850,12 @@ function classifyRuleSeverity(name, verdict) {
|
|
|
2703
2850
|
"read-ssh",
|
|
2704
2851
|
"read-gcp",
|
|
2705
2852
|
"read-cred",
|
|
2853
|
+
// Stage 4 (2026-09-11): a copy of a credential file scores like a READ of it --
|
|
2854
|
+
// read-ssh/aws/cred are critical, so copy-ssh/aws/cred are; read-env is high
|
|
2855
|
+
// (below), so copy-env joins the high list, not this one (/code-review).
|
|
2856
|
+
"copy-ssh",
|
|
2857
|
+
"copy-aws",
|
|
2858
|
+
"copy-cred",
|
|
2706
2859
|
"delete-repo",
|
|
2707
2860
|
"helm-uninstall",
|
|
2708
2861
|
"drop-table",
|
|
@@ -2715,6 +2868,7 @@ function classifyRuleSeverity(name, verdict) {
|
|
|
2715
2868
|
];
|
|
2716
2869
|
if (criticalPatterns.some((p) => n.includes(p))) return "critical";
|
|
2717
2870
|
const highPatterns = [
|
|
2871
|
+
"copy-env",
|
|
2718
2872
|
"force-push",
|
|
2719
2873
|
"force_push",
|
|
2720
2874
|
"git-destructive",
|
|
@@ -2732,6 +2886,11 @@ function narrativeRuleLabel(name) {
|
|
|
2732
2886
|
const map = {
|
|
2733
2887
|
"read-aws": "AWS credentials read",
|
|
2734
2888
|
"read-ssh": "SSH private key read",
|
|
2889
|
+
// Stage 4 copy twins, so `scan --narrative` prints a label, not a raw slug.
|
|
2890
|
+
"copy-ssh": "SSH private key copied out",
|
|
2891
|
+
"copy-aws": "AWS credentials copied out",
|
|
2892
|
+
"copy-env": ".env file copied out",
|
|
2893
|
+
"copy-cred": "credential file copied out",
|
|
2735
2894
|
"read-gcp": "GCP credentials read",
|
|
2736
2895
|
"read-cred": "credential file read",
|
|
2737
2896
|
"delete-repo": "GitHub repository deletion",
|
|
@@ -3321,7 +3480,7 @@ function* stringValues(obj, depth = 0) {
|
|
|
3321
3480
|
}
|
|
3322
3481
|
for (const v of Object.values(obj)) yield* stringValues(v, depth + 1);
|
|
3323
3482
|
}
|
|
3324
|
-
var import_safe_regex2, import_crypto3, import_mvdan_sh, import_picomatch, import_safe_regex22, import_safe_regex23, import_crypto4, IBAN_LENGTH, B58, B58_INDEX, XPRV_VERSIONS, MAX, UNTRUSTED_TOOLS, SIGNALS, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, DLP_SCAN_LIMITS, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, FS_READ_TOOLS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, BASH_TOOL_NAMES, AST_FS_REGEX_RULES, SQL_DB_CLIS, SQL_DDL_RE, CHMOD_OPEN_PERM_TOKENS, COMMAND_WRAPPERS, INLINE_INTERPRETER, RUNNER_WRAPPERS, _redirStdinOps, _listOps, WRAPPER_TAKES_TARGET, FIND_EXEC_FLAGS, INTERP_LEADING_TARGET, NET_BINARIES, VALUE_FLAGS, FS_OP_CACHE_MAX, fsOpCache, stripDotSlash, REDIR_TRUNCATE_OPS, REDIR_FILE_IN_OPS, REDIR_HEREDOC_OPS, isReaderWord, positionalAfter, DEFAULT_EGRESS_ALLOWLIST, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, SSRF_MAX_HOST, METADATA_ADDRESSES, STRICT_TIERS, METADATA_HOSTNAMES, v4Octets, TIER_REASON, DESTINATION_ARGS, VERDICT_RANK, SQL_DML_KEYWORDS, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, LOOP_MAX_RECORDS, FINDING_TO_SIGNAL, SCAN_SIGNAL_WEIGHTS, LOOP_THRESHOLD_FOR_WASTE, DESTRUCTIVE_OP_RE, SENSITIVE_PATH_RE, FILE_TOOLS, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC16_RE, PII_CC15_RE, PII_IBAN_RE, REALTIME_PII_PATTERNS, MAX_PII_SCAN_BYTES, CANARY_MIN_LENGTH, MAX_TEXT, MAX_DEPTH2, MAX_JSON_PARSE, URL_DEPTH, B64_DEPTH, MIN_SEGMENT, SEPARATORS, stripSeparators, looksText, CANARY_DECODERS, VIEWS, LONG_OUTPUT_THRESHOLD_BYTES, CANONICAL_EXTRACTOR_VERSION, DEDUPE_PREVIEW_LEN, TERMINAL_ESCAPE_RE, ENGINE_VERSION;
|
|
3483
|
+
var import_safe_regex2, import_crypto3, import_mvdan_sh, import_picomatch, import_safe_regex22, import_safe_regex23, import_crypto4, IBAN_LENGTH, B58, B58_INDEX, XPRV_VERSIONS, MAX, UNTRUSTED_TOOLS, SIGNALS, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, DLP_SCAN_LIMITS, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, FS_READ_TOOLS, SCP_VALUE_FLAGS, RSYNC_SKIP, COPY_VERBS, TAR_MODE_WORD, COPY_VERB_HEADS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, BASH_TOOL_NAMES, AST_FS_REGEX_RULES, SQL_DB_CLIS, SQL_DDL_RE, CHMOD_OPEN_PERM_TOKENS, COMMAND_WRAPPERS, INLINE_INTERPRETER, RUNNER_WRAPPERS, _redirStdinOps, _listOps, WRAPPER_TAKES_TARGET, FIND_EXEC_FLAGS, INTERP_LEADING_TARGET, NET_BINARIES, VALUE_FLAGS, FS_OP_CACHE_MAX, fsOpCache, stripDotSlash, REDIR_TRUNCATE_OPS, REDIR_FILE_IN_OPS, REDIR_HEREDOC_OPS, FIND_OPTIONS, COPY_RULE_OF, isReaderWord, positionalAfter, DEFAULT_EGRESS_ALLOWLIST, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, SSRF_MAX_HOST, METADATA_ADDRESSES, STRICT_TIERS, METADATA_HOSTNAMES, v4Octets, TIER_REASON, DESTINATION_ARGS, VERDICT_RANK, SQL_DML_KEYWORDS, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, LOOP_MAX_RECORDS, FINDING_TO_SIGNAL, SCAN_SIGNAL_WEIGHTS, LOOP_THRESHOLD_FOR_WASTE, DESTRUCTIVE_OP_RE, SENSITIVE_PATH_RE, FILE_TOOLS, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC16_RE, PII_CC15_RE, PII_IBAN_RE, REALTIME_PII_PATTERNS, MAX_PII_SCAN_BYTES, CANARY_MIN_LENGTH, MAX_TEXT, MAX_DEPTH2, MAX_JSON_PARSE, URL_DEPTH, B64_DEPTH, MIN_SEGMENT, SEPARATORS, stripSeparators, looksText, CANARY_DECODERS, VIEWS, LONG_OUTPUT_THRESHOLD_BYTES, CANONICAL_EXTRACTOR_VERSION, DEDUPE_PREVIEW_LEN, TERMINAL_ESCAPE_RE, ENGINE_VERSION;
|
|
3325
3484
|
var init_dist = __esm({
|
|
3326
3485
|
"packages/policy-engine/dist/index.mjs"() {
|
|
3327
3486
|
"use strict";
|
|
@@ -4064,12 +4223,56 @@ var init_dist = __esm({
|
|
|
4064
4223
|
"nl",
|
|
4065
4224
|
"dd"
|
|
4066
4225
|
]);
|
|
4226
|
+
SCP_VALUE_FLAGS = ["i", "F", "o", "c", "S", "P", "J", "D", "W", "l"];
|
|
4227
|
+
RSYNC_SKIP = [
|
|
4228
|
+
"e",
|
|
4229
|
+
"--rsh",
|
|
4230
|
+
"--exclude",
|
|
4231
|
+
"--exclude-from",
|
|
4232
|
+
"--include",
|
|
4233
|
+
"--include-from",
|
|
4234
|
+
"--files-from",
|
|
4235
|
+
"f",
|
|
4236
|
+
"--filter"
|
|
4237
|
+
];
|
|
4238
|
+
COPY_VERBS = {
|
|
4239
|
+
cp: { source: "allButLast", targetDirFlag: true },
|
|
4240
|
+
mv: { source: "allButLast", targetDirFlag: true },
|
|
4241
|
+
install: { source: "allButLast", targetDirFlag: true },
|
|
4242
|
+
ln: { source: "first", targetDirFlag: true },
|
|
4243
|
+
scp: { source: "allButLast", skipFlags: SCP_VALUE_FLAGS },
|
|
4244
|
+
rsync: { source: "allButLast", skipFlags: RSYNC_SKIP },
|
|
4245
|
+
tar: {
|
|
4246
|
+
source: "archive",
|
|
4247
|
+
archive: "tar",
|
|
4248
|
+
skipFlags: ["f", "X", "T", "--file", "--exclude", "--exclude-from", "--files-from"]
|
|
4249
|
+
},
|
|
4250
|
+
zip: { source: "archive", archive: "zip", skipFlags: ["x", "i", "--exclude", "--include"] },
|
|
4251
|
+
ar: { source: "archive", archive: "ar" },
|
|
4252
|
+
"7z": { source: "archive", archive: "7z", skipFlags: ["x", "--exclude"] },
|
|
4253
|
+
gzip: { source: "all" },
|
|
4254
|
+
bzip2: { source: "all" },
|
|
4255
|
+
xz: { source: "all" },
|
|
4256
|
+
"docker cp": { source: "allButLast" },
|
|
4257
|
+
"kubectl cp": { source: "allButLast" },
|
|
4258
|
+
"gsutil cp": { source: "allButLast" },
|
|
4259
|
+
"gsutil rsync": { source: "allButLast" },
|
|
4260
|
+
"rclone copy": { source: "allButLast" },
|
|
4261
|
+
"rclone sync": { source: "allButLast" },
|
|
4262
|
+
"aws s3 cp": { source: "allButLast" },
|
|
4263
|
+
"aws s3 mv": { source: "allButLast" },
|
|
4264
|
+
"aws s3 sync": { source: "allButLast" },
|
|
4265
|
+
"gcloud storage cp": { source: "allButLast" },
|
|
4266
|
+
"az storage blob upload": { source: "flagOperand", sourceFlags: ["f", "--file"] }
|
|
4267
|
+
};
|
|
4268
|
+
TAR_MODE_WORD = /^[a-zA-Z]+$/;
|
|
4269
|
+
COPY_VERB_HEADS = new Set(Object.keys(COPY_VERBS).map((k) => k.split(" ")[0]));
|
|
4067
4270
|
FS_OP_PRESCREEN_RE = new RegExp(
|
|
4068
4271
|
// A quote is a separator too: `eval "cat X"` and `sh -c 'cat X'` put the
|
|
4069
4272
|
// reader right after `"` / `'`, and without these two characters the
|
|
4070
4273
|
// prescreen rejected every string-wrapped read before the parser ran.
|
|
4071
4274
|
// Found 2026-09-11 by instrumenting the walk -- no CallExpr was ever visited.
|
|
4072
|
-
`(?:^|[\\s|;&("'\`\\n])(?:rm|${[...FS_READ_TOOLS].map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\b|(?<!<)<(?!<)`
|
|
4275
|
+
`(?:^|[\\s|;&("'\`\\n/])(?:rm|${[...FS_READ_TOOLS, ...COPY_VERB_HEADS].map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\b|(?<!<)<(?!<)`
|
|
4073
4276
|
);
|
|
4074
4277
|
HOME_CACHE_ALLOWLIST = [
|
|
4075
4278
|
".cache",
|
|
@@ -4359,8 +4562,15 @@ var init_dist = __esm({
|
|
|
4359
4562
|
deriveRedirOp("cat <<X\nX"),
|
|
4360
4563
|
deriveRedirOp("cat <<-X\nX")
|
|
4361
4564
|
]);
|
|
4362
|
-
|
|
4363
|
-
|
|
4565
|
+
FIND_OPTIONS = /* @__PURE__ */ new Set(["-H", "-L", "-P"]);
|
|
4566
|
+
COPY_RULE_OF = {
|
|
4567
|
+
"shield:project-jail:block-read-ssh": "shield:project-jail:review-copy-ssh",
|
|
4568
|
+
"shield:project-jail:block-read-aws": "shield:project-jail:review-copy-aws",
|
|
4569
|
+
"shield:project-jail:block-read-env": "shield:project-jail:review-copy-env",
|
|
4570
|
+
"shield:project-jail:review-read-credentials": "shield:project-jail:review-copy-credentials"
|
|
4571
|
+
};
|
|
4572
|
+
isReaderWord = (w) => w !== null && FS_READ_TOOLS.has(baseWord(w));
|
|
4573
|
+
positionalAfter = (words, from, to = words.length) => positionedArgs(words, from, to).map((a) => a.value);
|
|
4364
4574
|
DEFAULT_EGRESS_ALLOWLIST = [
|
|
4365
4575
|
// node9's own control plane (api, app, dev-api, staging and the apex).
|
|
4366
4576
|
// Without it, turning egress on asks the user to approve node9 itself.
|
|
@@ -5405,7 +5615,7 @@ var init_dist = __esm({
|
|
|
5405
5615
|
{ view: "separators-stripped", decoder: "separators", stripped: true }
|
|
5406
5616
|
];
|
|
5407
5617
|
LONG_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
|
|
5408
|
-
CANONICAL_EXTRACTOR_VERSION = "canonical-
|
|
5618
|
+
CANONICAL_EXTRACTOR_VERSION = "canonical-v14";
|
|
5409
5619
|
DEDUPE_PREVIEW_LEN = 120;
|
|
5410
5620
|
TERMINAL_ESCAPE_RE = // eslint-disable-next-line no-control-regex
|
|
5411
5621
|
/\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[@-_]|[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g;
|
|
@@ -12975,6 +13185,28 @@ function codexSessionCost(model, tokens, request2) {
|
|
|
12975
13185
|
const tierMultiplier = request2?.serviceTier === "flex" ? 0.5 : codexModel(model) === "gpt-6-astra" && (request2?.serviceTier === "fast" || request2?.serviceTier === "priority") ? 2 : 1;
|
|
12976
13186
|
return (((input - cached - written) * pin + cached * pcr + written * (pcw || pin)) * inputMultiplier + tokenNumber(tokens.output) * pout * outputMultiplier) * tierMultiplier;
|
|
12977
13187
|
}
|
|
13188
|
+
function statAndFirstLine(file) {
|
|
13189
|
+
const CAP = 4 * 1024 * 1024;
|
|
13190
|
+
const CHUNK = 64 * 1024;
|
|
13191
|
+
const fd = import_fs20.default.openSync(file, "r");
|
|
13192
|
+
try {
|
|
13193
|
+
const stat = import_fs20.default.fstatSync(fd);
|
|
13194
|
+
const limit = Math.min(stat.size, CAP);
|
|
13195
|
+
const parts = [];
|
|
13196
|
+
for (let pos = 0; pos < limit; pos += CHUNK) {
|
|
13197
|
+
const buf = Buffer.alloc(Math.min(CHUNK, limit - pos));
|
|
13198
|
+
const read2 = import_fs20.default.readSync(fd, buf, 0, buf.length, pos);
|
|
13199
|
+
if (read2 <= 0) break;
|
|
13200
|
+
const slice = buf.subarray(0, read2);
|
|
13201
|
+
const nl = slice.indexOf(10);
|
|
13202
|
+
parts.push(nl >= 0 ? slice.subarray(0, nl) : slice);
|
|
13203
|
+
if (nl >= 0) break;
|
|
13204
|
+
}
|
|
13205
|
+
return { stat, first: Buffer.concat(parts).toString("utf8") };
|
|
13206
|
+
} finally {
|
|
13207
|
+
import_fs20.default.closeSync(fd);
|
|
13208
|
+
}
|
|
13209
|
+
}
|
|
12978
13210
|
function listCodexSessionFiles(base = codexSessionsDir()) {
|
|
12979
13211
|
const files = [];
|
|
12980
13212
|
const walk = (dir) => {
|
|
@@ -12992,10 +13224,10 @@ function listCodexSessionFiles(base = codexSessionsDir()) {
|
|
|
12992
13224
|
const sessions = /* @__PURE__ */ new Map();
|
|
12993
13225
|
for (const file of files.sort()) {
|
|
12994
13226
|
try {
|
|
12995
|
-
const stat =
|
|
13227
|
+
const { stat, first: head } = statAndFirstLine(file);
|
|
12996
13228
|
let id = "";
|
|
12997
13229
|
try {
|
|
12998
|
-
const first = JSON.parse(
|
|
13230
|
+
const first = JSON.parse(head);
|
|
12999
13231
|
if (first?.type === "session_meta" && typeof first.payload?.id === "string")
|
|
13000
13232
|
id = first.payload.id;
|
|
13001
13233
|
} catch {
|
|
@@ -59460,7 +59692,11 @@ var BUILTIN_JAIL = [
|
|
|
59460
59692
|
"~/.ssh \u2014 SSH private keys",
|
|
59461
59693
|
"~/.aws \u2014 AWS credentials",
|
|
59462
59694
|
".env files",
|
|
59463
|
-
"credential files \u2014 credentials.json, .netrc, .npmrc, .docker, .kube, gcloud"
|
|
59695
|
+
"credential files \u2014 credentials.json, .netrc, .npmrc, .docker, .kube, gcloud",
|
|
59696
|
+
// Stage 4 (2026-09-11): reads are blocked; a COPY out of the jail (cp, tar,
|
|
59697
|
+
// scp, rsync, aws s3 cp, ...) is reviewed, because a backup and a theft are
|
|
59698
|
+
// the same command shape.
|
|
59699
|
+
"copies out of the jail (cp, tar, scp, rsync, cloud upload) \u2014 review"
|
|
59464
59700
|
];
|
|
59465
59701
|
function registerJailCommand(program2) {
|
|
59466
59702
|
const jail = program2.command("jail").description("Grow the credential jail \u2014 block/review reads of your sensitive paths");
|