@node9/policy-engine 1.4.0 → 1.5.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.js CHANGED
@@ -30,15 +30,25 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var src_exports = {};
32
32
  __export(src_exports, {
33
+ AST_FS_REGEX_RULES: () => AST_FS_REGEX_RULES,
34
+ BASH_TOOL_NAMES: () => BASH_TOOL_NAMES,
33
35
  BUILTIN_SHIELDS: () => BUILTIN_SHIELDS,
36
+ CANONICAL_EXTRACTOR_HASH: () => CANONICAL_EXTRACTOR_HASH,
37
+ CANONICAL_EXTRACTOR_VERSION: () => CANONICAL_EXTRACTOR_VERSION,
34
38
  COST_PER_LOOP_ITER_USD: () => COST_PER_LOOP_ITER_USD,
39
+ DESTRUCTIVE_OP_RE: () => DESTRUCTIVE_OP_RE,
35
40
  DLP_PATTERNS: () => DLP_PATTERNS,
36
41
  ENGINE_VERSION: () => ENGINE_VERSION,
42
+ FILE_TOOLS: () => FILE_TOOLS,
37
43
  FLAGS_WITH_VALUES: () => FLAGS_WITH_VALUES,
44
+ LONG_OUTPUT_THRESHOLD_BYTES: () => LONG_OUTPUT_THRESHOLD_BYTES,
38
45
  LOOP_MAX_RECORDS: () => LOOP_MAX_RECORDS,
39
46
  LOOP_THRESHOLD_FOR_WASTE: () => LOOP_THRESHOLD_FOR_WASTE,
47
+ PRIVILEGE_ESCALATION_RE: () => PRIVILEGE_ESCALATION_RE,
40
48
  SCAN_SIGNAL_WEIGHTS: () => SCAN_SIGNAL_WEIGHTS,
49
+ SENSITIVE_PATH_RE: () => SENSITIVE_PATH_RE,
41
50
  SENSITIVE_PATH_REGEXES: () => SENSITIVE_PATH_REGEXES,
51
+ analyzeFsOperation: () => analyzeFsOperation,
42
52
  analyzePipeChain: () => analyzePipeChain,
43
53
  analyzeShellCommand: () => analyzeShellCommand,
44
54
  checkDangerousSql: () => checkDangerousSql,
@@ -49,29 +59,37 @@ __export(src_exports, {
49
59
  computeBlendedSecurityScore: () => computeBlendedSecurityScore,
50
60
  computeScanScore: () => computeScanScore,
51
61
  computeSecurityScore: () => computeSecurityScore,
62
+ dedupeCanonicalFindings: () => dedupeCanonicalFindings,
52
63
  detectDangerousEval: () => detectDangerousEval,
53
64
  detectDangerousShellExec: () => detectDangerousShellExec,
65
+ detectPii: () => detectPii,
54
66
  evaluateLoopWindow: () => evaluateLoopWindow,
55
67
  evaluatePolicy: () => evaluatePolicy,
56
68
  evaluateSmartConditions: () => evaluateSmartConditions,
57
69
  extractAllSshHosts: () => extractAllSshHosts,
70
+ extractCanonicalFindings: () => extractCanonicalFindings,
58
71
  extractNetworkTargets: () => extractNetworkTargets,
59
72
  extractPositionalArgs: () => extractPositionalArgs,
73
+ extractSessionLevelFindings: () => extractSessionLevelFindings,
60
74
  getCompiledRegex: () => getCompiledRegex,
61
75
  getNestedValue: () => getNestedValue,
76
+ isBashTool: () => isBashTool,
62
77
  isIgnoredTool: () => isIgnoredTool,
78
+ isProtectedHomePath: () => isProtectedHomePath,
63
79
  isShieldVerdict: () => isShieldVerdict,
64
80
  matchSensitivePath: () => matchSensitivePath,
65
81
  matchesPattern: () => matchesPattern,
66
82
  narrativeRuleLabel: () => narrativeRuleLabel,
67
83
  normalizeCommandForPolicy: () => normalizeCommandForPolicy,
68
84
  parseAllSshHostsFromCommand: () => parseAllSshHostsFromCommand,
85
+ previewArgs: () => previewArgs,
69
86
  redactText: () => redactText,
70
87
  scanArgs: () => scanArgs,
71
88
  scanText: () => scanText,
72
89
  sensitivePathMatch: () => sensitivePathMatch,
73
90
  summarizeBlast: () => summarizeBlast,
74
91
  summarizeScan: () => summarizeScan,
92
+ toScanFinding: () => toScanFinding,
75
93
  truncateBlastPath: () => truncateBlastPath,
76
94
  validateOverrides: () => validateOverrides,
77
95
  validateRegex: () => validateRegex,
@@ -702,9 +720,70 @@ var MESSAGE_FLAGS = /* @__PURE__ */ new Set([
702
720
  ]);
703
721
  var SHELL_INTERPRETERS = /* @__PURE__ */ new Set(["bash", "sh", "zsh", "fish", "dash", "ksh"]);
704
722
  var DOWNLOAD_CMDS = /* @__PURE__ */ new Set(["curl", "wget"]);
723
+ function isCatHeredocOrLit(part) {
724
+ if (!part) return false;
725
+ const t = syntax.NodeType(part);
726
+ if (t === "Lit") return true;
727
+ if (t !== "CmdSubst") return false;
728
+ const stmts = part.Stmts || [];
729
+ if (stmts.length !== 1) return false;
730
+ const stmt = stmts[0];
731
+ const redirs = stmt.Redirs || stmt.Cmd?.Redirs || [];
732
+ const hasHeredoc = redirs.some((r) => r && r.Hdoc);
733
+ if (!hasHeredoc) return false;
734
+ const cmd = stmt.Cmd;
735
+ if (!cmd || syntax.NodeType(cmd) !== "CallExpr") return false;
736
+ const firstArg = cmd.Args?.[0]?.Parts || [];
737
+ if (firstArg.length !== 1 || syntax.NodeType(firstArg[0]) !== "Lit") return false;
738
+ return (firstArg[0].Value || "").toLowerCase() === "cat";
739
+ }
740
+ var NORMALIZE_CACHE_MAX = 5e3;
741
+ var normalizeCache = /* @__PURE__ */ new Map();
742
+ var AST_CACHE_MAX = 5e3;
743
+ var astCache = /* @__PURE__ */ new Map();
744
+ var PARSE_FAIL = /* @__PURE__ */ Symbol("parse-fail");
745
+ function parseShared(command) {
746
+ const cached = astCache.get(command);
747
+ if (cached !== void 0) {
748
+ astCache.delete(command);
749
+ astCache.set(command, cached);
750
+ return cached;
751
+ }
752
+ let parsed;
753
+ try {
754
+ parsed = sharedParser.Parse(command, "cmd");
755
+ } catch {
756
+ parsed = PARSE_FAIL;
757
+ }
758
+ if (astCache.size >= AST_CACHE_MAX) {
759
+ const oldest = astCache.keys().next().value;
760
+ if (oldest !== void 0) astCache.delete(oldest);
761
+ }
762
+ astCache.set(command, parsed);
763
+ return parsed;
764
+ }
765
+ function cachedNormalize(command, compute) {
766
+ const hit = normalizeCache.get(command);
767
+ if (hit !== void 0) {
768
+ normalizeCache.delete(command);
769
+ normalizeCache.set(command, hit);
770
+ return hit;
771
+ }
772
+ const result = compute();
773
+ if (normalizeCache.size >= NORMALIZE_CACHE_MAX) {
774
+ const oldest = normalizeCache.keys().next().value;
775
+ if (oldest !== void 0) normalizeCache.delete(oldest);
776
+ }
777
+ normalizeCache.set(command, result);
778
+ return result;
779
+ }
705
780
  function normalizeCommandForPolicy(command) {
781
+ return cachedNormalize(command, () => normalizeCommandForPolicyImpl(command));
782
+ }
783
+ function normalizeCommandForPolicyImpl(command) {
784
+ const f = parseShared(command);
785
+ if (f === PARSE_FAIL) return command;
706
786
  try {
707
- const f = sharedParser.Parse(command, "cmd");
708
787
  const strips = [];
709
788
  syntax.Walk(f, (node) => {
710
789
  if (!node) return false;
@@ -726,7 +805,11 @@ function normalizeCommandForPolicy(command) {
726
805
  } else if (nt === "DblQuoted") {
727
806
  const innerParts = quotedNode.Parts || [];
728
807
  const allLit = innerParts.length === 0 || innerParts.every((p) => syntax.NodeType(p) === "Lit");
729
- if (allLit) strips.push([next.Pos().Offset(), next.End().Offset()]);
808
+ if (allLit) {
809
+ strips.push([next.Pos().Offset(), next.End().Offset()]);
810
+ } else if (innerParts.every((p) => isCatHeredocOrLit(p))) {
811
+ strips.push([next.Pos().Offset(), next.End().Offset()]);
812
+ }
730
813
  }
731
814
  }
732
815
  return true;
@@ -795,6 +878,242 @@ function detectDangerousShellExec(command) {
795
878
  }
796
879
  }
797
880
  var detectDangerousEval = detectDangerousShellExec;
881
+ var FS_READ_TOOLS = /* @__PURE__ */ new Set([
882
+ "cat",
883
+ "less",
884
+ "head",
885
+ "tail",
886
+ "bat",
887
+ "more",
888
+ "open",
889
+ "print",
890
+ "nano",
891
+ "vim",
892
+ "vi",
893
+ "emacs",
894
+ "code",
895
+ "type"
896
+ ]);
897
+ var FS_OP_PRESCREEN_RE = /(?:^|[\s|;&(`\n])(?:rm|cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\b/;
898
+ var HOME_CACHE_ALLOWLIST = [
899
+ ".cache",
900
+ ".npm/_npx",
901
+ ".npm/_cacache",
902
+ ".cargo/registry",
903
+ ".gradle/caches",
904
+ ".gradle/.tmp",
905
+ ".m2/repository",
906
+ ".pnpm-store",
907
+ ".yarn/cache",
908
+ ".yarn/.cache",
909
+ ".cache/pip",
910
+ ".local/share/Trash",
911
+ ".rustup/downloads"
912
+ ];
913
+ var SENSITIVE_PATH_RULES = [
914
+ {
915
+ rule: "shield:project-jail:block-read-ssh",
916
+ reason: "Reading SSH private keys is blocked by project-jail shield",
917
+ match: (p) => /(^|[\\/])\.ssh[\\/]/i.test(p)
918
+ },
919
+ {
920
+ rule: "shield:project-jail:block-read-aws",
921
+ reason: "Reading AWS credentials is blocked by project-jail shield",
922
+ match: (p) => /(^|[\\/])\.aws[\\/]/i.test(p)
923
+ },
924
+ {
925
+ // Mirrors the JSON shield's `.env` pattern (project-jail.json's
926
+ // review-read-env-any-tool) so the AST FS-op path catches the
927
+ // same set the regex shield does — including Next.js / Vite's
928
+ // `.env.<env>.local` double-suffix overrides which are commonly
929
+ // gitignored AND commonly contain real secrets.
930
+ //
931
+ // Intentional non-matches (dev fixtures): .env.example, .env.sample,
932
+ // .env.template, .env.test, .envrc. See shields.test.ts:983-995
933
+ // for the canonical test-asserted contract.
934
+ rule: "shield:project-jail:block-read-env",
935
+ reason: "Reading .env files is blocked by project-jail shield",
936
+ match: (p) => /(?:^|[\\/])\.env(?:\.(?:local|production|staging|development|production\.local|staging\.local|development\.local))?$/i.test(
937
+ p
938
+ )
939
+ },
940
+ {
941
+ // verdict: 'review' (not 'block') is a deliberate design choice
942
+ // documented in commit 29327a8. SSH keys and AWS credentials are
943
+ // cryptographic material with no legitimate read use-case for
944
+ // an AI agent → hard `block`. But .netrc / .npmrc / .docker /
945
+ // .kube / gcloud are CONFIG files that hold tokens AND have
946
+ // legitimate diagnostic reads ("which registry am I configured
947
+ // for", "what cluster am I on"). Hard-blocking those creates
948
+ // friction without much safety win because the review gate
949
+ // still catches genuine exfiltration attempts.
950
+ //
951
+ // The review gate FAILS CLOSED on timeout (daemon.approvalTimeoutMs
952
+ // returns a deny verdict via the orchestrator's timeout branch),
953
+ // so a stuck or unattended approval does NOT silently grant
954
+ // credential access. If the threat model demands strict block,
955
+ // a future per-shield strict-mode toggle is the right fix —
956
+ // not a regex-level upgrade here.
957
+ rule: "shield:project-jail:review-read-credentials",
958
+ reason: "Reading credential files requires approval (project-jail shield)",
959
+ verdict: "review",
960
+ match: (p) => (
961
+ // .kube/config holds Kubernetes cluster credentials and was
962
+ // flagged as missing by the node9-pr-agent review (the comment
963
+ // above mentioned .kube but the regex didn't include it — a
964
+ // textbook code-comment vs code drift). The JSON shield's
965
+ // review-read-credentials-any-tool already had it. Now aligned.
966
+ /(?:credentials\.json|\.netrc|\.npmrc|\.docker[\\/]config\.json|gcloud[\\/]credentials|\.kube[\\/]config)$/i.test(
967
+ p
968
+ )
969
+ )
970
+ }
971
+ ];
972
+ var BASH_TOOL_NAMES = /* @__PURE__ */ new Set([
973
+ "bash",
974
+ "execute_bash",
975
+ "run_shell_command",
976
+ "shell",
977
+ "exec_command"
978
+ ]);
979
+ function isBashTool(toolName) {
980
+ return BASH_TOOL_NAMES.has(toolName.toLowerCase());
981
+ }
982
+ var AST_FS_REGEX_RULES = /* @__PURE__ */ new Set([
983
+ "block-rm-rf-home",
984
+ "shield:project-jail:block-read-ssh",
985
+ "shield:project-jail:block-read-aws",
986
+ "shield:project-jail:block-read-env",
987
+ "shield:project-jail:review-read-credentials"
988
+ ]);
989
+ function isProtectedHomePath(rawPath) {
990
+ let p = rawPath.replace(/^\$HOME[\\/]?|^\$\{HOME\}[\\/]?/, "~/");
991
+ let underHome = false;
992
+ if (p === "~" || p.startsWith("~/") || p.startsWith("~\\")) {
993
+ p = p.replace(/^~[\\/]?/, "");
994
+ underHome = true;
995
+ } else if (/^\/home\/[^/]+/.test(p) || /^\/root(\/|$)/.test(p)) {
996
+ p = p.replace(/^\/home\/[^/]+[\\/]?|^\/root[\\/]?/, "");
997
+ underHome = true;
998
+ }
999
+ if (!underHome) return false;
1000
+ if (p === "" || p === "." || p === "./") return true;
1001
+ for (const safe of HOME_CACHE_ALLOWLIST) {
1002
+ if (p === safe || p.startsWith(safe + "/") || p.startsWith(safe + "\\")) {
1003
+ return false;
1004
+ }
1005
+ }
1006
+ return true;
1007
+ }
1008
+ function extractLiteralArgs(callExpr) {
1009
+ const args = callExpr.Args || [];
1010
+ if (args.length === 0) return { name: "", flags: [], paths: [] };
1011
+ const litFromWord = (w) => {
1012
+ const parts = w?.Parts || [];
1013
+ let s = "";
1014
+ for (const p of parts) {
1015
+ const t = syntax.NodeType(p);
1016
+ if (t === "Lit") s += (p.Value ?? "").replace(/\\(.)/g, "$1");
1017
+ else if (t === "SglQuoted") s += p.Value ?? "";
1018
+ else if (t === "DblQuoted") {
1019
+ const inner = p.Parts || [];
1020
+ if (!inner.every((ip) => syntax.NodeType(ip) === "Lit")) return null;
1021
+ s += inner.map((ip) => ip.Value ?? "").join("");
1022
+ } else {
1023
+ return null;
1024
+ }
1025
+ }
1026
+ return s;
1027
+ };
1028
+ const name = (litFromWord(args[0]) || "").toLowerCase();
1029
+ const flags = [];
1030
+ const paths = [];
1031
+ for (let i = 1; i < args.length; i++) {
1032
+ const v = litFromWord(args[i]);
1033
+ if (v === null) continue;
1034
+ if (v.startsWith("-")) flags.push(v);
1035
+ else paths.push(v);
1036
+ }
1037
+ return { name, flags, paths };
1038
+ }
1039
+ var FS_OP_CACHE_MAX = 5e3;
1040
+ var fsOpCache = /* @__PURE__ */ new Map();
1041
+ function analyzeFsOperation(command) {
1042
+ if (!FS_OP_PRESCREEN_RE.test(command)) return null;
1043
+ if (fsOpCache.has(command)) {
1044
+ const hit = fsOpCache.get(command) ?? null;
1045
+ fsOpCache.delete(command);
1046
+ fsOpCache.set(command, hit);
1047
+ return hit;
1048
+ }
1049
+ const computed = analyzeFsOperationImpl(command);
1050
+ if (fsOpCache.size >= FS_OP_CACHE_MAX) {
1051
+ const oldest = fsOpCache.keys().next().value;
1052
+ if (oldest !== void 0) fsOpCache.delete(oldest);
1053
+ }
1054
+ fsOpCache.set(command, computed);
1055
+ return computed;
1056
+ }
1057
+ function analyzeFsOperationImpl(command) {
1058
+ const f = parseShared(command);
1059
+ if (f === PARSE_FAIL) return null;
1060
+ let result = null;
1061
+ try {
1062
+ syntax.Walk(f, (node) => {
1063
+ if (!node || result) return false;
1064
+ const n = node;
1065
+ if (syntax.NodeType(n) !== "CallExpr") return true;
1066
+ const { name, flags, paths } = extractLiteralArgs(n);
1067
+ if (!name) return true;
1068
+ if (name === "rm") {
1069
+ const flagStr = flags.join("").toLowerCase();
1070
+ const hasR = /[r]/.test(flagStr) || flags.includes("--recursive");
1071
+ const hasF = /[f]/.test(flagStr) || flags.includes("--force");
1072
+ if (hasR && hasF) {
1073
+ for (const p of paths) {
1074
+ if (isProtectedHomePath(p)) {
1075
+ result = {
1076
+ ruleName: "block-rm-rf-home",
1077
+ verdict: "block",
1078
+ reason: "Recursive delete of home directory is irreversible",
1079
+ path: p
1080
+ };
1081
+ return false;
1082
+ }
1083
+ if (p === "/" || /^\/+$/.test(p)) {
1084
+ result = {
1085
+ ruleName: "block-rm-rf-home",
1086
+ verdict: "block",
1087
+ reason: "Recursive delete of root is catastrophic",
1088
+ path: p
1089
+ };
1090
+ return false;
1091
+ }
1092
+ }
1093
+ }
1094
+ }
1095
+ if (FS_READ_TOOLS.has(name)) {
1096
+ for (const p of paths) {
1097
+ for (const sp of SENSITIVE_PATH_RULES) {
1098
+ if (sp.match(p)) {
1099
+ result = {
1100
+ ruleName: sp.rule,
1101
+ verdict: sp.verdict ?? "block",
1102
+ reason: sp.reason,
1103
+ path: p
1104
+ };
1105
+ return false;
1106
+ }
1107
+ }
1108
+ }
1109
+ }
1110
+ return true;
1111
+ });
1112
+ return result;
1113
+ } catch {
1114
+ return null;
1115
+ }
1116
+ }
798
1117
  function analyzeShellCommand(command) {
799
1118
  const actions = [];
800
1119
  const paths = [];
@@ -1234,10 +1553,18 @@ function getNestedValue(obj, path) {
1234
1553
  function evaluateSmartConditions(args, rule) {
1235
1554
  if (!rule.conditions || rule.conditions.length === 0) return true;
1236
1555
  const mode = rule.conditionMode ?? "all";
1556
+ const fieldCache = /* @__PURE__ */ new Map();
1557
+ const resolveField = (field) => {
1558
+ if (fieldCache.has(field)) return fieldCache.get(field) ?? null;
1559
+ const rawVal = getNestedValue(args, field);
1560
+ const rawStr = rawVal !== null && rawVal !== void 0 ? String(rawVal) : null;
1561
+ const stripped = field === "command" && rawStr !== null ? normalizeCommandForPolicy(rawStr) : rawStr;
1562
+ const val = stripped !== null ? stripped.replace(/\s+/g, " ").trim() : null;
1563
+ fieldCache.set(field, val);
1564
+ return val;
1565
+ };
1237
1566
  const results = rule.conditions.map((cond) => {
1238
- const rawVal = getNestedValue(args, cond.field);
1239
- const normalized = rawVal !== null && rawVal !== void 0 ? String(rawVal).replace(/\s+/g, " ").trim() : null;
1240
- const val = cond.field === "command" && normalized !== null ? normalizeCommandForPolicy(normalized) : normalized;
1567
+ const val = resolveField(cond.field);
1241
1568
  switch (cond.op) {
1242
1569
  case "exists":
1243
1570
  return val !== null && val !== "";
@@ -1300,6 +1627,43 @@ function checkDangerousSql(sql) {
1300
1627
  return "UPDATE without WHERE \u2014 updates every row";
1301
1628
  return null;
1302
1629
  }
1630
+ function pipeChainVerdict(command, isTrustedHost) {
1631
+ const pipeAnalysis = analyzePipeChain(command);
1632
+ if (!pipeAnalysis.isPipeline) return null;
1633
+ if (pipeAnalysis.risk !== "critical" && pipeAnalysis.risk !== "high") return null;
1634
+ const sinks = pipeAnalysis.sinkTargets;
1635
+ const allTrusted = sinks.length > 0 && sinks.every((host) => isTrustedHost ? isTrustedHost(host) : false);
1636
+ if (pipeAnalysis.risk === "critical") {
1637
+ if (allTrusted) {
1638
+ return {
1639
+ decision: "review",
1640
+ blockedByLabel: "Node9: Pipe-Chain to Trusted Host (obfuscated)",
1641
+ reason: `Obfuscated pipe to trusted host(s): ${sinks.join(", ")} \u2014 requires approval`,
1642
+ tier: 3
1643
+ };
1644
+ }
1645
+ return {
1646
+ decision: "block",
1647
+ blockedByLabel: "Node9: Pipe-Chain Exfiltration (critical)",
1648
+ reason: `Sensitive file piped through obfuscator to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
1649
+ tier: 3
1650
+ };
1651
+ }
1652
+ if (allTrusted) {
1653
+ return {
1654
+ decision: "allow",
1655
+ blockedByLabel: "Node9: Pipe-Chain to Trusted Host",
1656
+ reason: `Sensitive file piped to trusted host(s): ${sinks.join(", ")}`,
1657
+ tier: 3
1658
+ };
1659
+ }
1660
+ return {
1661
+ decision: "review",
1662
+ blockedByLabel: "Node9: Pipe-Chain Exfiltration (high)",
1663
+ reason: `Sensitive file piped to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
1664
+ tier: 3
1665
+ };
1666
+ }
1303
1667
  async function evaluatePolicy(config, toolName, args, context = {}, hooks = {}) {
1304
1668
  const { agent, cwd, activeEnvironment } = context;
1305
1669
  const { checkProvenance, isTrustedHost } = hooks;
@@ -1315,9 +1679,27 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
1315
1679
  }
1316
1680
  }
1317
1681
  if (wouldBeIgnored) return { decision: "allow" };
1682
+ const bashCommand = agent !== "Terminal" && isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : null;
1683
+ if (bashCommand !== null) {
1684
+ const pipeVerdict = pipeChainVerdict(bashCommand, isTrustedHost);
1685
+ if (pipeVerdict) return pipeVerdict;
1686
+ const fsVerdict = analyzeFsOperation(bashCommand);
1687
+ if (fsVerdict) {
1688
+ const isShieldRule = fsVerdict.ruleName.startsWith("shield:");
1689
+ const labelPrefix = isShieldRule ? "project-jail (AST)" : "Node9 (AST)";
1690
+ return {
1691
+ decision: fsVerdict.verdict,
1692
+ blockedByLabel: `${labelPrefix}: ${fsVerdict.ruleName}`,
1693
+ reason: fsVerdict.reason,
1694
+ tier: 2,
1695
+ ruleName: fsVerdict.ruleName,
1696
+ ruleDescription: fsVerdict.reason
1697
+ };
1698
+ }
1699
+ }
1318
1700
  if (config.policy.smartRules.length > 0) {
1319
1701
  const matchedRule = config.policy.smartRules.find(
1320
- (rule) => matchesPattern(toolName, rule.tool) && evaluateSmartConditions(args, rule)
1702
+ (rule) => matchesPattern(toolName, rule.tool) && !(bashCommand !== null && rule.name && AST_FS_REGEX_RULES.has(rule.name)) && evaluateSmartConditions(args, rule)
1321
1703
  );
1322
1704
  if (matchedRule) {
1323
1705
  if (matchedRule.verdict === "allow")
@@ -1375,41 +1757,8 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
1375
1757
  tier: 3
1376
1758
  };
1377
1759
  }
1378
- const pipeAnalysis = analyzePipeChain(shellCommand);
1379
- if (pipeAnalysis.isPipeline && (pipeAnalysis.risk === "critical" || pipeAnalysis.risk === "high")) {
1380
- const sinks = pipeAnalysis.sinkTargets;
1381
- const allTrusted = sinks.length > 0 && sinks.every((host) => isTrustedHost ? isTrustedHost(host) : false);
1382
- if (pipeAnalysis.risk === "critical") {
1383
- if (allTrusted) {
1384
- return {
1385
- decision: "review",
1386
- blockedByLabel: "Node9: Pipe-Chain to Trusted Host (obfuscated)",
1387
- reason: `Obfuscated pipe to trusted host(s): ${sinks.join(", ")} \u2014 requires approval`,
1388
- tier: 3
1389
- };
1390
- }
1391
- return {
1392
- decision: "block",
1393
- blockedByLabel: "Node9: Pipe-Chain Exfiltration (critical)",
1394
- reason: `Sensitive file piped through obfuscator to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
1395
- tier: 3
1396
- };
1397
- }
1398
- if (allTrusted) {
1399
- return {
1400
- decision: "allow",
1401
- blockedByLabel: "Node9: Pipe-Chain to Trusted Host",
1402
- reason: `Sensitive file piped to trusted host(s): ${sinks.join(", ")}`,
1403
- tier: 3
1404
- };
1405
- }
1406
- return {
1407
- decision: "review",
1408
- blockedByLabel: "Node9: Pipe-Chain Exfiltration (high)",
1409
- reason: `Sensitive file piped to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
1410
- tier: 3
1411
- };
1412
- }
1760
+ const ptVerdict = pipeChainVerdict(shellCommand, isTrustedHost);
1761
+ if (ptVerdict) return ptVerdict;
1413
1762
  const firstToken = analyzed.actions[0] ?? "";
1414
1763
  if (["ssh", "scp", "rsync"].includes(firstToken)) {
1415
1764
  const rawTokens = shellCommand.trim().split(/\s+/);
@@ -2086,7 +2435,7 @@ var project_jail_default = {
2086
2435
  {
2087
2436
  field: "command",
2088
2437
  op: "matches",
2089
- value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*[\\/\\\\]\\.ssh[\\/\\\\]",
2438
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*?\\.ssh[\\/\\\\]",
2090
2439
  flags: "i"
2091
2440
  }
2092
2441
  ],
@@ -2100,7 +2449,7 @@ var project_jail_default = {
2100
2449
  {
2101
2450
  field: "command",
2102
2451
  op: "matches",
2103
- value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*[\\/\\\\]\\.aws[\\/\\\\]",
2452
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*?\\.aws[\\/\\\\]",
2104
2453
  flags: "i"
2105
2454
  }
2106
2455
  ],
@@ -2122,7 +2471,7 @@ var project_jail_default = {
2122
2471
  reason: "Reading .env files is blocked by project-jail shield"
2123
2472
  },
2124
2473
  {
2125
- name: "shield:project-jail:block-read-credentials",
2474
+ name: "shield:project-jail:review-read-credentials",
2126
2475
  tool: "bash",
2127
2476
  conditions: [
2128
2477
  {
@@ -2132,8 +2481,64 @@ var project_jail_default = {
2132
2481
  flags: "i"
2133
2482
  }
2134
2483
  ],
2484
+ verdict: "review",
2485
+ reason: "Reading credential files requires approval (project-jail shield)"
2486
+ },
2487
+ {
2488
+ name: "shield:project-jail:block-read-ssh-any-tool",
2489
+ tool: "*",
2490
+ conditions: [
2491
+ {
2492
+ field: "file_path",
2493
+ op: "matches",
2494
+ value: "(^|[\\/\\\\])\\.ssh[\\/\\\\]",
2495
+ flags: "i"
2496
+ }
2497
+ ],
2135
2498
  verdict: "block",
2136
- reason: "Reading credential files is blocked by project-jail shield"
2499
+ reason: "Reading SSH private keys is blocked by project-jail shield"
2500
+ },
2501
+ {
2502
+ name: "shield:project-jail:block-read-aws-any-tool",
2503
+ tool: "*",
2504
+ conditions: [
2505
+ {
2506
+ field: "file_path",
2507
+ op: "matches",
2508
+ value: "(^|[\\/\\\\])\\.aws[\\/\\\\]",
2509
+ flags: "i"
2510
+ }
2511
+ ],
2512
+ verdict: "block",
2513
+ reason: "Reading AWS credentials is blocked by project-jail shield"
2514
+ },
2515
+ {
2516
+ name: "shield:project-jail:review-read-env-any-tool",
2517
+ tool: "*",
2518
+ conditions: [
2519
+ {
2520
+ field: "file_path",
2521
+ op: "matches",
2522
+ value: "(^|[\\/\\\\])\\.env(\\.(local|production|staging|development|production\\.local|staging\\.local|development\\.local))?$",
2523
+ flags: "i"
2524
+ }
2525
+ ],
2526
+ verdict: "review",
2527
+ reason: "Reading .env files requires approval (project-jail shield)"
2528
+ },
2529
+ {
2530
+ name: "shield:project-jail:review-read-credentials-any-tool",
2531
+ tool: "*",
2532
+ conditions: [
2533
+ {
2534
+ field: "file_path",
2535
+ op: "matches",
2536
+ value: ".*(credentials\\.json|\\.netrc|\\.npmrc|\\.docker[\\/\\\\]config\\.json|gcloud[\\/\\\\]credentials|\\.kube[\\/\\\\]config)",
2537
+ flags: "i"
2538
+ }
2539
+ ],
2540
+ verdict: "review",
2541
+ reason: "Reading credential files requires approval (project-jail shield)"
2137
2542
  }
2138
2543
  ],
2139
2544
  dangerousWords: []
@@ -2542,19 +2947,409 @@ function summarizeBlast(result, opts = {}) {
2542
2947
  };
2543
2948
  }
2544
2949
 
2950
+ // src/scan/destructive-regex.ts
2951
+ 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;
2952
+ var PRIVILEGE_ESCALATION_RE = /\bchmod\s+(0?777|\+x)\b|\bchown\s+root\b/i;
2953
+ var SENSITIVE_PATH_RE = /\.aws\/(credentials|config)\b|\.ssh\/(id_rsa|id_ed25519|id_ecdsa|id_dsa)\b|\.env(\.|$|\b)|\.config\/gcloud\/credentials\.db\b|\.docker\/config\.json\b|\.netrc\b|\.npmrc\b|\.node9\/credentials\.json\b/i;
2954
+ var FILE_TOOLS = /* @__PURE__ */ new Set([
2955
+ "read",
2956
+ "read_file",
2957
+ "edit",
2958
+ "edit_file",
2959
+ "write",
2960
+ "write_file",
2961
+ "multiedit",
2962
+ "grep",
2963
+ "grep_search",
2964
+ "glob",
2965
+ "list_files"
2966
+ ]);
2967
+
2968
+ // src/scan/pii.ts
2969
+ var PII_EMAIL_RE = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/;
2970
+ var PII_SSN_RE = /\b\d{3}-\d{2}-\d{4}\b/;
2971
+ var PII_PHONE_RE = /\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]\d{3}[-.\s]\d{4}\b/;
2972
+ var PII_CC_RE = /\b(?:4\d{3}|5[1-5]\d{2}|3[47]\d{2}|6\d{3})[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b/;
2973
+ function detectPii(text) {
2974
+ const found = /* @__PURE__ */ new Set();
2975
+ if (/@/.test(text) && PII_EMAIL_RE.test(text)) found.add("Email");
2976
+ if (/-/.test(text) && PII_SSN_RE.test(text)) found.add("SSN");
2977
+ if (PII_PHONE_RE.test(text)) found.add("Phone");
2978
+ if (PII_CC_RE.test(text)) found.add("Credit Card");
2979
+ return [...found];
2980
+ }
2981
+
2982
+ // src/scan/canonical.ts
2983
+ var LONG_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
2984
+ var CANONICAL_EXTRACTOR_VERSION = "canonical-v4";
2985
+ var CANONICAL_EXTRACTOR_HASH = "64a6a63a27f4646f";
2986
+ var DEDUPE_PREVIEW_LEN = 120;
2987
+ function extractCanonicalFindings(call, ctx) {
2988
+ const out = [];
2989
+ const ts = call.timestamp;
2990
+ const toolNameLower = call.toolName.toLowerCase();
2991
+ const command = typeof call.args.command === "string" ? call.args.command : null;
2992
+ const isBash = isBashTool(call.toolName) && command !== null;
2993
+ if (call.outputBytes !== void 0 && call.outputBytes > LONG_OUTPUT_THRESHOLD_BYTES) {
2994
+ out.push(
2995
+ makeFinding({
2996
+ type: "long-output-redacted",
2997
+ ruleName: "long-output-redacted",
2998
+ verdict: "review",
2999
+ severity: "medium",
3000
+ reason: `Tool output exceeded ${LONG_OUTPUT_THRESHOLD_BYTES} bytes and was redacted`,
3001
+ toolName: call.toolName,
3002
+ ctx,
3003
+ ts,
3004
+ sourceType: "engine"
3005
+ })
3006
+ );
3007
+ }
3008
+ if (ctx.dlpEnabled) {
3009
+ const dlp = scanArgs(call.args);
3010
+ if (dlp) {
3011
+ out.push(
3012
+ makeFinding({
3013
+ type: "dlp",
3014
+ ruleName: `dlp:${dlp.patternName}`,
3015
+ patternName: dlp.patternName,
3016
+ verdict: dlp.severity === "block" ? "block" : "review",
3017
+ severity: dlp.severity === "block" ? "critical" : "medium",
3018
+ reason: `${dlp.patternName} detected in ${dlp.fieldPath}`,
3019
+ toolName: call.toolName,
3020
+ ctx,
3021
+ ts,
3022
+ sourceType: "engine",
3023
+ input: call.args,
3024
+ redactedSample: dlp.redactedSample
3025
+ })
3026
+ );
3027
+ }
3028
+ }
3029
+ for (const value of stringValues(call.args)) {
3030
+ const piiHits = detectPii(value);
3031
+ for (const pattern of piiHits) {
3032
+ out.push(
3033
+ makeFinding({
3034
+ type: "pii",
3035
+ ruleName: `pii:${pattern.toLowerCase().replace(/\s+/g, "-")}`,
3036
+ patternName: pattern,
3037
+ verdict: "review",
3038
+ severity: "medium",
3039
+ reason: `${pattern} pattern detected in tool input`,
3040
+ toolName: call.toolName,
3041
+ ctx,
3042
+ ts,
3043
+ sourceType: "engine"
3044
+ })
3045
+ );
3046
+ }
3047
+ }
3048
+ if (FILE_TOOLS.has(toolNameLower)) {
3049
+ const filePath = typeof call.args.file_path === "string" && call.args.file_path || typeof call.args.path === "string" && call.args.path || typeof call.args.pattern === "string" && call.args.pattern || "";
3050
+ if (filePath && SENSITIVE_PATH_RE.test(filePath)) {
3051
+ out.push(
3052
+ makeFinding({
3053
+ type: "sensitive-file-read",
3054
+ ruleName: "sensitive-file-read",
3055
+ verdict: "review",
3056
+ severity: "critical",
3057
+ reason: `Sensitive file path read via ${call.toolName}`,
3058
+ toolName: call.toolName,
3059
+ ctx,
3060
+ ts,
3061
+ sourceType: "engine",
3062
+ subjectPath: filePath
3063
+ })
3064
+ );
3065
+ }
3066
+ }
3067
+ if (!isBash || command === null) {
3068
+ return out;
3069
+ }
3070
+ const fsVerdict = analyzeFsOperation(command);
3071
+ if (fsVerdict) {
3072
+ const isShield = fsVerdict.ruleName.startsWith("shield:");
3073
+ out.push(
3074
+ makeFinding({
3075
+ type: "ast-fs-op",
3076
+ ruleName: fsVerdict.ruleName,
3077
+ verdict: fsVerdict.verdict,
3078
+ severity: classifyRuleSeverity(fsVerdict.ruleName, fsVerdict.verdict),
3079
+ reason: fsVerdict.reason,
3080
+ toolName: call.toolName,
3081
+ ctx,
3082
+ ts,
3083
+ sourceType: isShield ? "shield" : "engine",
3084
+ shieldLabel: isShield ? "project-jail (AST)" : "Node9 (AST)",
3085
+ subjectPath: fsVerdict.path,
3086
+ input: call.args
3087
+ })
3088
+ );
3089
+ }
3090
+ for (const source of ctx.rules) {
3091
+ const r = source.rule;
3092
+ if (r.verdict === "allow") continue;
3093
+ if (r.tool && !matchesPattern(toolNameLower, r.tool)) continue;
3094
+ if (r.name && AST_FS_REGEX_RULES.has(r.name)) continue;
3095
+ if (!evaluateSmartConditions(call.args, r)) continue;
3096
+ out.push(
3097
+ makeFinding({
3098
+ type: "smart-rule",
3099
+ ruleName: r.name ?? r.tool,
3100
+ verdict: r.verdict === "block" ? "block" : "review",
3101
+ severity: classifyRuleSeverity(r.name ?? r.tool, r.verdict),
3102
+ reason: r.reason ?? `Smart rule ${r.name ?? r.tool} fired`,
3103
+ toolName: call.toolName,
3104
+ ctx,
3105
+ ts,
3106
+ sourceType: source.sourceType,
3107
+ shieldLabel: source.shieldLabel,
3108
+ input: call.args
3109
+ })
3110
+ );
3111
+ break;
3112
+ }
3113
+ const evalVerdict = detectDangerousShellExec(command);
3114
+ if (evalVerdict) {
3115
+ out.push(
3116
+ makeFinding({
3117
+ type: "eval-of-remote",
3118
+ ruleName: "eval-of-remote",
3119
+ verdict: evalVerdict,
3120
+ severity: classifyRuleSeverity("eval-remote", evalVerdict),
3121
+ reason: evalVerdict === "block" ? "Eval of remote download is a near-certain supply-chain attack" : "Eval of dynamic content (variable / subshell) requires approval",
3122
+ toolName: call.toolName,
3123
+ ctx,
3124
+ ts,
3125
+ sourceType: "engine",
3126
+ input: call.args
3127
+ })
3128
+ );
3129
+ }
3130
+ const pipe = analyzePipeChain(command);
3131
+ if (pipe.isPipeline && pipe.risk === "critical") {
3132
+ out.push(
3133
+ makeFinding({
3134
+ type: "pipe-to-shell",
3135
+ ruleName: "pipe-to-shell",
3136
+ verdict: "block",
3137
+ severity: "critical",
3138
+ reason: `Sensitive file piped through obfuscator to network sink: ${pipe.sourceFiles.join(", ")} \u2192 ${pipe.sinkTargets.join(", ")}`,
3139
+ toolName: call.toolName,
3140
+ ctx,
3141
+ ts,
3142
+ sourceType: "engine",
3143
+ input: call.args
3144
+ })
3145
+ );
3146
+ }
3147
+ if (DESTRUCTIVE_OP_RE.test(command)) {
3148
+ out.push(
3149
+ makeFinding({
3150
+ type: "destructive-op",
3151
+ ruleName: "destructive-op",
3152
+ verdict: "review",
3153
+ severity: "high",
3154
+ reason: "Destructive operation pattern detected",
3155
+ toolName: call.toolName,
3156
+ ctx,
3157
+ ts,
3158
+ sourceType: "engine",
3159
+ input: call.args
3160
+ })
3161
+ );
3162
+ }
3163
+ const ast = analyzeShellCommand(command);
3164
+ const sudoVariant = ast.actions.includes("sudo") || ast.actions.includes("su");
3165
+ const chmodVariant = ast.actions.includes("chmod") && (ast.allTokens.includes("777") || ast.allTokens.includes("0777") || ast.allTokens.includes("+x"));
3166
+ const chownVariant = ast.actions.includes("chown") && ast.allTokens.includes("root");
3167
+ if (sudoVariant || chmodVariant || chownVariant) {
3168
+ out.push(
3169
+ makeFinding({
3170
+ type: "privilege-escalation",
3171
+ ruleName: "privilege-escalation",
3172
+ verdict: "review",
3173
+ severity: "high",
3174
+ reason: "Privilege-escalation pattern detected",
3175
+ toolName: call.toolName,
3176
+ ctx,
3177
+ ts,
3178
+ sourceType: "engine",
3179
+ input: call.args
3180
+ })
3181
+ );
3182
+ }
3183
+ return out;
3184
+ }
3185
+ function extractSessionLevelFindings(calls, ctx) {
3186
+ if (!ctx.loopDetection.enabled || calls.length === 0) return [];
3187
+ const out = [];
3188
+ const seenLoopKeys = /* @__PURE__ */ new Set();
3189
+ const ONE_YEAR_MS = 365 * 24 * 60 * 60 * 1e3;
3190
+ const windowMs = ctx.loopDetection.windowSeconds <= 0 ? ONE_YEAR_MS : ctx.loopDetection.windowSeconds * 1e3;
3191
+ let records = [];
3192
+ let syntheticTs = 0;
3193
+ for (let i = 0; i < calls.length; i++) {
3194
+ const call = calls[i];
3195
+ const parsed = new Date(call.timestamp).getTime();
3196
+ const now = Number.isFinite(parsed) ? parsed : ++syntheticTs;
3197
+ const verdict = evaluateLoopWindow(
3198
+ records,
3199
+ call.toolName,
3200
+ call.args,
3201
+ ctx.loopDetection.threshold,
3202
+ windowMs,
3203
+ now
3204
+ );
3205
+ records = verdict.nextRecords;
3206
+ if (!verdict.looping) continue;
3207
+ const last = records[records.length - 1];
3208
+ const key = `${last.t}|${last.h}`;
3209
+ if (seenLoopKeys.has(key)) continue;
3210
+ seenLoopKeys.add(key);
3211
+ out.push({
3212
+ type: "loop",
3213
+ ruleName: "loop",
3214
+ verdict: "review",
3215
+ severity: "medium",
3216
+ reason: `Tool called ${verdict.count} times with identical args within window`,
3217
+ toolName: call.toolName,
3218
+ agent: ctx.agent,
3219
+ sessionId: ctx.sessionId,
3220
+ project: ctx.project,
3221
+ lineIndex: call.lineIndex,
3222
+ sourceType: "engine",
3223
+ firstSeenAt: call.timestamp,
3224
+ lastSeenAt: call.timestamp,
3225
+ occurrenceCount: 1,
3226
+ loopCount: verdict.count,
3227
+ loopKind: "loop",
3228
+ commandPreview: previewArgs(call.args, DEDUPE_PREVIEW_LEN),
3229
+ costUsd: verdict.count * COST_PER_LOOP_ITER_USD
3230
+ });
3231
+ }
3232
+ return out;
3233
+ }
3234
+ function dedupeCanonicalFindings(findings) {
3235
+ const merged = /* @__PURE__ */ new Map();
3236
+ for (const f of findings) {
3237
+ const inputPreview = f.input ? previewArgs(f.input, DEDUPE_PREVIEW_LEN) : "";
3238
+ const key = `${f.type}|${f.ruleName}|${inputPreview}|${f.project}|${f.agent}`;
3239
+ const prev = merged.get(key);
3240
+ if (!prev) {
3241
+ merged.set(key, { ...f });
3242
+ continue;
3243
+ }
3244
+ prev.occurrenceCount += f.occurrenceCount;
3245
+ if (f.firstSeenAt && (!prev.firstSeenAt || f.firstSeenAt < prev.firstSeenAt)) {
3246
+ prev.firstSeenAt = f.firstSeenAt;
3247
+ }
3248
+ if (f.lastSeenAt && f.lastSeenAt > prev.lastSeenAt) {
3249
+ prev.lastSeenAt = f.lastSeenAt;
3250
+ }
3251
+ if (f.costUsd !== void 0) {
3252
+ prev.costUsd = (prev.costUsd ?? 0) + f.costUsd;
3253
+ }
3254
+ if (f.loopCount !== void 0) {
3255
+ prev.loopCount = (prev.loopCount ?? 0) + f.loopCount;
3256
+ }
3257
+ }
3258
+ return [...merged.values()];
3259
+ }
3260
+ function toScanFinding(c) {
3261
+ const typeMap = {
3262
+ "smart-rule": null,
3263
+ "ast-fs-op": null,
3264
+ dlp: "dlp",
3265
+ pii: "pii",
3266
+ "sensitive-file-read": "sensitive-file-read",
3267
+ "privilege-escalation": "privilege-escalation",
3268
+ "destructive-op": "destructive-op",
3269
+ "pipe-to-shell": "pipe-to-shell",
3270
+ "eval-of-remote": "eval-of-remote",
3271
+ loop: "loop",
3272
+ "long-output-redacted": "long-output-redacted"
3273
+ };
3274
+ const sfType = typeMap[c.type];
3275
+ if (sfType === null) return null;
3276
+ return {
3277
+ sessionId: c.sessionId,
3278
+ type: sfType,
3279
+ ...c.patternName && { patternName: c.patternName },
3280
+ lineIndex: c.lineIndex
3281
+ };
3282
+ }
3283
+ var TERMINAL_ESCAPE_RE = (
3284
+ // eslint-disable-next-line no-control-regex
3285
+ /\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[@-_]|[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g
3286
+ );
3287
+ function previewArgs(input, max) {
3288
+ const cmd = input.command ?? input.query ?? input.file_path ?? JSON.stringify(input);
3289
+ const s = String(cmd).replace(TERMINAL_ESCAPE_RE, "").replace(/\s+/g, " ").trim();
3290
+ return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
3291
+ }
3292
+ function makeFinding(args) {
3293
+ const f = {
3294
+ type: args.type,
3295
+ ruleName: args.ruleName,
3296
+ verdict: args.verdict,
3297
+ severity: args.severity,
3298
+ reason: args.reason,
3299
+ toolName: args.toolName,
3300
+ agent: args.ctx.agent,
3301
+ sessionId: args.ctx.sessionId,
3302
+ project: args.ctx.project,
3303
+ lineIndex: args.ctx.lineIndex,
3304
+ sourceType: args.sourceType,
3305
+ firstSeenAt: args.ts,
3306
+ lastSeenAt: args.ts,
3307
+ occurrenceCount: 1
3308
+ };
3309
+ if (args.shieldLabel) f.shieldLabel = args.shieldLabel;
3310
+ if (args.subjectPath) f.subjectPath = args.subjectPath;
3311
+ if (args.input) f.input = args.input;
3312
+ if (args.patternName) f.patternName = args.patternName;
3313
+ if (args.redactedSample) f.redactedSample = args.redactedSample;
3314
+ return f;
3315
+ }
3316
+ function* stringValues(obj, depth = 0) {
3317
+ if (depth > 6) return;
3318
+ if (typeof obj === "string") {
3319
+ if (obj.length > 0) yield obj;
3320
+ return;
3321
+ }
3322
+ if (!obj || typeof obj !== "object") return;
3323
+ if (Array.isArray(obj)) {
3324
+ for (const v of obj) yield* stringValues(v, depth + 1);
3325
+ return;
3326
+ }
3327
+ for (const v of Object.values(obj)) yield* stringValues(v, depth + 1);
3328
+ }
3329
+
2545
3330
  // src/index.ts
2546
3331
  var ENGINE_VERSION = "1.4.0";
2547
3332
  // Annotate the CommonJS export names for ESM import in node:
2548
3333
  0 && (module.exports = {
3334
+ AST_FS_REGEX_RULES,
3335
+ BASH_TOOL_NAMES,
2549
3336
  BUILTIN_SHIELDS,
3337
+ CANONICAL_EXTRACTOR_HASH,
3338
+ CANONICAL_EXTRACTOR_VERSION,
2550
3339
  COST_PER_LOOP_ITER_USD,
3340
+ DESTRUCTIVE_OP_RE,
2551
3341
  DLP_PATTERNS,
2552
3342
  ENGINE_VERSION,
3343
+ FILE_TOOLS,
2553
3344
  FLAGS_WITH_VALUES,
3345
+ LONG_OUTPUT_THRESHOLD_BYTES,
2554
3346
  LOOP_MAX_RECORDS,
2555
3347
  LOOP_THRESHOLD_FOR_WASTE,
3348
+ PRIVILEGE_ESCALATION_RE,
2556
3349
  SCAN_SIGNAL_WEIGHTS,
3350
+ SENSITIVE_PATH_RE,
2557
3351
  SENSITIVE_PATH_REGEXES,
3352
+ analyzeFsOperation,
2558
3353
  analyzePipeChain,
2559
3354
  analyzeShellCommand,
2560
3355
  checkDangerousSql,
@@ -2565,29 +3360,37 @@ var ENGINE_VERSION = "1.4.0";
2565
3360
  computeBlendedSecurityScore,
2566
3361
  computeScanScore,
2567
3362
  computeSecurityScore,
3363
+ dedupeCanonicalFindings,
2568
3364
  detectDangerousEval,
2569
3365
  detectDangerousShellExec,
3366
+ detectPii,
2570
3367
  evaluateLoopWindow,
2571
3368
  evaluatePolicy,
2572
3369
  evaluateSmartConditions,
2573
3370
  extractAllSshHosts,
3371
+ extractCanonicalFindings,
2574
3372
  extractNetworkTargets,
2575
3373
  extractPositionalArgs,
3374
+ extractSessionLevelFindings,
2576
3375
  getCompiledRegex,
2577
3376
  getNestedValue,
3377
+ isBashTool,
2578
3378
  isIgnoredTool,
3379
+ isProtectedHomePath,
2579
3380
  isShieldVerdict,
2580
3381
  matchSensitivePath,
2581
3382
  matchesPattern,
2582
3383
  narrativeRuleLabel,
2583
3384
  normalizeCommandForPolicy,
2584
3385
  parseAllSshHostsFromCommand,
3386
+ previewArgs,
2585
3387
  redactText,
2586
3388
  scanArgs,
2587
3389
  scanText,
2588
3390
  sensitivePathMatch,
2589
3391
  summarizeBlast,
2590
3392
  summarizeScan,
3393
+ toScanFinding,
2591
3394
  truncateBlastPath,
2592
3395
  validateOverrides,
2593
3396
  validateRegex,