@node9/proxy 1.18.3 → 1.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -596,6 +596,9 @@ function detectDangerousShellExec(command) {
596
596
  return null;
597
597
  }
598
598
  }
599
+ function isBashTool(toolName) {
600
+ return BASH_TOOL_NAMES.has(toolName.toLowerCase());
601
+ }
599
602
  function isProtectedHomePath(rawPath) {
600
603
  let p = rawPath.replace(/^\$HOME[\\/]?|^\$\{HOME\}[\\/]?/, "~/");
601
604
  let underHome = false;
@@ -1068,6 +1071,43 @@ function isSqlTool(toolName, toolInspection) {
1068
1071
  const fieldName = toolInspection[matchingPattern];
1069
1072
  return fieldName === "sql" || fieldName === "query";
1070
1073
  }
1074
+ function pipeChainVerdict(command, isTrustedHost2) {
1075
+ const pipeAnalysis = analyzePipeChain(command);
1076
+ if (!pipeAnalysis.isPipeline) return null;
1077
+ if (pipeAnalysis.risk !== "critical" && pipeAnalysis.risk !== "high") return null;
1078
+ const sinks = pipeAnalysis.sinkTargets;
1079
+ const allTrusted = sinks.length > 0 && sinks.every((host) => isTrustedHost2 ? isTrustedHost2(host) : false);
1080
+ if (pipeAnalysis.risk === "critical") {
1081
+ if (allTrusted) {
1082
+ return {
1083
+ decision: "review",
1084
+ blockedByLabel: "Node9: Pipe-Chain to Trusted Host (obfuscated)",
1085
+ reason: `Obfuscated pipe to trusted host(s): ${sinks.join(", ")} \u2014 requires approval`,
1086
+ tier: 3
1087
+ };
1088
+ }
1089
+ return {
1090
+ decision: "block",
1091
+ blockedByLabel: "Node9: Pipe-Chain Exfiltration (critical)",
1092
+ reason: `Sensitive file piped through obfuscator to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
1093
+ tier: 3
1094
+ };
1095
+ }
1096
+ if (allTrusted) {
1097
+ return {
1098
+ decision: "allow",
1099
+ blockedByLabel: "Node9: Pipe-Chain to Trusted Host",
1100
+ reason: `Sensitive file piped to trusted host(s): ${sinks.join(", ")}`,
1101
+ tier: 3
1102
+ };
1103
+ }
1104
+ return {
1105
+ decision: "review",
1106
+ blockedByLabel: "Node9: Pipe-Chain Exfiltration (high)",
1107
+ reason: `Sensitive file piped to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
1108
+ tier: 3
1109
+ };
1110
+ }
1071
1111
  async function evaluatePolicy(config, toolName, args, context = {}, hooks = {}) {
1072
1112
  const { agent, cwd, activeEnvironment } = context;
1073
1113
  const { checkProvenance: checkProvenance2, isTrustedHost: isTrustedHost2 } = hooks;
@@ -1083,9 +1123,27 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
1083
1123
  }
1084
1124
  }
1085
1125
  if (wouldBeIgnored) return { decision: "allow" };
1126
+ const bashCommand = agent !== "Terminal" && isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : null;
1127
+ if (bashCommand !== null) {
1128
+ const pipeVerdict = pipeChainVerdict(bashCommand, isTrustedHost2);
1129
+ if (pipeVerdict) return pipeVerdict;
1130
+ const fsVerdict = analyzeFsOperation(bashCommand);
1131
+ if (fsVerdict) {
1132
+ const isShieldRule = fsVerdict.ruleName.startsWith("shield:");
1133
+ const labelPrefix = isShieldRule ? "project-jail (AST)" : "Node9 (AST)";
1134
+ return {
1135
+ decision: fsVerdict.verdict,
1136
+ blockedByLabel: `${labelPrefix}: ${fsVerdict.ruleName}`,
1137
+ reason: fsVerdict.reason,
1138
+ tier: 2,
1139
+ ruleName: fsVerdict.ruleName,
1140
+ ruleDescription: fsVerdict.reason
1141
+ };
1142
+ }
1143
+ }
1086
1144
  if (config.policy.smartRules.length > 0) {
1087
1145
  const matchedRule = config.policy.smartRules.find(
1088
- (rule) => matchesPattern(toolName, rule.tool) && evaluateSmartConditions(args, rule)
1146
+ (rule) => matchesPattern(toolName, rule.tool) && !(bashCommand !== null && rule.name && AST_FS_REGEX_RULES.has(rule.name)) && evaluateSmartConditions(args, rule)
1089
1147
  );
1090
1148
  if (matchedRule) {
1091
1149
  if (matchedRule.verdict === "allow")
@@ -1143,41 +1201,8 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
1143
1201
  tier: 3
1144
1202
  };
1145
1203
  }
1146
- const pipeAnalysis = analyzePipeChain(shellCommand);
1147
- if (pipeAnalysis.isPipeline && (pipeAnalysis.risk === "critical" || pipeAnalysis.risk === "high")) {
1148
- const sinks = pipeAnalysis.sinkTargets;
1149
- const allTrusted = sinks.length > 0 && sinks.every((host) => isTrustedHost2 ? isTrustedHost2(host) : false);
1150
- if (pipeAnalysis.risk === "critical") {
1151
- if (allTrusted) {
1152
- return {
1153
- decision: "review",
1154
- blockedByLabel: "Node9: Pipe-Chain to Trusted Host (obfuscated)",
1155
- reason: `Obfuscated pipe to trusted host(s): ${sinks.join(", ")} \u2014 requires approval`,
1156
- tier: 3
1157
- };
1158
- }
1159
- return {
1160
- decision: "block",
1161
- blockedByLabel: "Node9: Pipe-Chain Exfiltration (critical)",
1162
- reason: `Sensitive file piped through obfuscator to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
1163
- tier: 3
1164
- };
1165
- }
1166
- if (allTrusted) {
1167
- return {
1168
- decision: "allow",
1169
- blockedByLabel: "Node9: Pipe-Chain to Trusted Host",
1170
- reason: `Sensitive file piped to trusted host(s): ${sinks.join(", ")}`,
1171
- tier: 3
1172
- };
1173
- }
1174
- return {
1175
- decision: "review",
1176
- blockedByLabel: "Node9: Pipe-Chain Exfiltration (high)",
1177
- reason: `Sensitive file piped to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
1178
- tier: 3
1179
- };
1180
- }
1204
+ const ptVerdict = pipeChainVerdict(shellCommand, isTrustedHost2);
1205
+ if (ptVerdict) return ptVerdict;
1181
1206
  const firstToken = analyzed.actions[0] ?? "";
1182
1207
  if (["ssh", "scp", "rsync"].includes(firstToken)) {
1183
1208
  const rawTokens = shellCommand.trim().split(/\s+/);
@@ -1499,7 +1524,323 @@ function summarizeBlast(result, opts = {}) {
1499
1524
  }))
1500
1525
  };
1501
1526
  }
1502
- var import_safe_regex2, import_mvdan_sh, import_picomatch, import_safe_regex22, import_safe_regex23, import_crypto2, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, 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, FS_OP_CACHE_MAX, fsOpCache, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, 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, COST_PER_LOOP_ITER_USD;
1527
+ function detectPii(text) {
1528
+ const found = /* @__PURE__ */ new Set();
1529
+ if (/@/.test(text) && PII_EMAIL_RE.test(text)) found.add("Email");
1530
+ if (/-/.test(text) && PII_SSN_RE.test(text)) found.add("SSN");
1531
+ if (PII_PHONE_RE.test(text)) found.add("Phone");
1532
+ if (PII_CC_RE.test(text)) found.add("Credit Card");
1533
+ return [...found];
1534
+ }
1535
+ function extractCanonicalFindings(call, ctx) {
1536
+ const out = [];
1537
+ const ts = call.timestamp;
1538
+ const toolNameLower = call.toolName.toLowerCase();
1539
+ const command = typeof call.args.command === "string" ? call.args.command : null;
1540
+ const isBash = isBashTool(call.toolName) && command !== null;
1541
+ if (call.outputBytes !== void 0 && call.outputBytes > LONG_OUTPUT_THRESHOLD_BYTES) {
1542
+ out.push(
1543
+ makeFinding({
1544
+ type: "long-output-redacted",
1545
+ ruleName: "long-output-redacted",
1546
+ verdict: "review",
1547
+ severity: "medium",
1548
+ reason: `Tool output exceeded ${LONG_OUTPUT_THRESHOLD_BYTES} bytes and was redacted`,
1549
+ toolName: call.toolName,
1550
+ ctx,
1551
+ ts,
1552
+ sourceType: "engine"
1553
+ })
1554
+ );
1555
+ }
1556
+ if (ctx.dlpEnabled) {
1557
+ const dlp = scanArgs(call.args);
1558
+ if (dlp) {
1559
+ out.push(
1560
+ makeFinding({
1561
+ type: "dlp",
1562
+ ruleName: `dlp:${dlp.patternName}`,
1563
+ patternName: dlp.patternName,
1564
+ verdict: dlp.severity === "block" ? "block" : "review",
1565
+ severity: dlp.severity === "block" ? "critical" : "medium",
1566
+ reason: `${dlp.patternName} detected in ${dlp.fieldPath}`,
1567
+ toolName: call.toolName,
1568
+ ctx,
1569
+ ts,
1570
+ sourceType: "engine",
1571
+ input: call.args,
1572
+ redactedSample: dlp.redactedSample
1573
+ })
1574
+ );
1575
+ }
1576
+ }
1577
+ for (const value of stringValues(call.args)) {
1578
+ const piiHits = detectPii(value);
1579
+ for (const pattern of piiHits) {
1580
+ out.push(
1581
+ makeFinding({
1582
+ type: "pii",
1583
+ ruleName: `pii:${pattern.toLowerCase().replace(/\s+/g, "-")}`,
1584
+ patternName: pattern,
1585
+ verdict: "review",
1586
+ severity: "medium",
1587
+ reason: `${pattern} pattern detected in tool input`,
1588
+ toolName: call.toolName,
1589
+ ctx,
1590
+ ts,
1591
+ sourceType: "engine"
1592
+ })
1593
+ );
1594
+ }
1595
+ }
1596
+ if (FILE_TOOLS.has(toolNameLower)) {
1597
+ 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 || "";
1598
+ if (filePath && SENSITIVE_PATH_RE.test(filePath)) {
1599
+ out.push(
1600
+ makeFinding({
1601
+ type: "sensitive-file-read",
1602
+ ruleName: "sensitive-file-read",
1603
+ verdict: "review",
1604
+ severity: "critical",
1605
+ reason: `Sensitive file path read via ${call.toolName}`,
1606
+ toolName: call.toolName,
1607
+ ctx,
1608
+ ts,
1609
+ sourceType: "engine",
1610
+ subjectPath: filePath
1611
+ })
1612
+ );
1613
+ }
1614
+ }
1615
+ if (!isBash || command === null) {
1616
+ return out;
1617
+ }
1618
+ const fsVerdict = analyzeFsOperation(command);
1619
+ if (fsVerdict) {
1620
+ const isShield = fsVerdict.ruleName.startsWith("shield:");
1621
+ out.push(
1622
+ makeFinding({
1623
+ type: "ast-fs-op",
1624
+ ruleName: fsVerdict.ruleName,
1625
+ verdict: fsVerdict.verdict,
1626
+ severity: classifyRuleSeverity(fsVerdict.ruleName, fsVerdict.verdict),
1627
+ reason: fsVerdict.reason,
1628
+ toolName: call.toolName,
1629
+ ctx,
1630
+ ts,
1631
+ sourceType: isShield ? "shield" : "engine",
1632
+ shieldLabel: isShield ? "project-jail (AST)" : "Node9 (AST)",
1633
+ subjectPath: fsVerdict.path,
1634
+ input: call.args
1635
+ })
1636
+ );
1637
+ }
1638
+ for (const source of ctx.rules) {
1639
+ const r = source.rule;
1640
+ if (r.verdict === "allow") continue;
1641
+ if (r.tool && !matchesPattern(toolNameLower, r.tool)) continue;
1642
+ if (r.name && AST_FS_REGEX_RULES.has(r.name)) continue;
1643
+ if (!evaluateSmartConditions(call.args, r)) continue;
1644
+ out.push(
1645
+ makeFinding({
1646
+ type: "smart-rule",
1647
+ ruleName: r.name ?? r.tool,
1648
+ verdict: r.verdict === "block" ? "block" : "review",
1649
+ severity: classifyRuleSeverity(r.name ?? r.tool, r.verdict),
1650
+ reason: r.reason ?? `Smart rule ${r.name ?? r.tool} fired`,
1651
+ toolName: call.toolName,
1652
+ ctx,
1653
+ ts,
1654
+ sourceType: source.sourceType,
1655
+ shieldLabel: source.shieldLabel,
1656
+ input: call.args
1657
+ })
1658
+ );
1659
+ break;
1660
+ }
1661
+ const evalVerdict = detectDangerousShellExec(command);
1662
+ if (evalVerdict) {
1663
+ out.push(
1664
+ makeFinding({
1665
+ type: "eval-of-remote",
1666
+ ruleName: "eval-of-remote",
1667
+ verdict: evalVerdict,
1668
+ severity: classifyRuleSeverity("eval-remote", evalVerdict),
1669
+ reason: evalVerdict === "block" ? "Eval of remote download is a near-certain supply-chain attack" : "Eval of dynamic content (variable / subshell) requires approval",
1670
+ toolName: call.toolName,
1671
+ ctx,
1672
+ ts,
1673
+ sourceType: "engine",
1674
+ input: call.args
1675
+ })
1676
+ );
1677
+ }
1678
+ const pipe = analyzePipeChain(command);
1679
+ if (pipe.isPipeline && pipe.risk === "critical") {
1680
+ out.push(
1681
+ makeFinding({
1682
+ type: "pipe-to-shell",
1683
+ ruleName: "pipe-to-shell",
1684
+ verdict: "block",
1685
+ severity: "critical",
1686
+ reason: `Sensitive file piped through obfuscator to network sink: ${pipe.sourceFiles.join(", ")} \u2192 ${pipe.sinkTargets.join(", ")}`,
1687
+ toolName: call.toolName,
1688
+ ctx,
1689
+ ts,
1690
+ sourceType: "engine",
1691
+ input: call.args
1692
+ })
1693
+ );
1694
+ }
1695
+ if (DESTRUCTIVE_OP_RE.test(command)) {
1696
+ out.push(
1697
+ makeFinding({
1698
+ type: "destructive-op",
1699
+ ruleName: "destructive-op",
1700
+ verdict: "review",
1701
+ severity: "high",
1702
+ reason: "Destructive operation pattern detected",
1703
+ toolName: call.toolName,
1704
+ ctx,
1705
+ ts,
1706
+ sourceType: "engine",
1707
+ input: call.args
1708
+ })
1709
+ );
1710
+ }
1711
+ if (PRIVILEGE_ESCALATION_RE.test(command)) {
1712
+ out.push(
1713
+ makeFinding({
1714
+ type: "privilege-escalation",
1715
+ ruleName: "privilege-escalation",
1716
+ verdict: "review",
1717
+ severity: "high",
1718
+ reason: "Privilege-escalation pattern detected",
1719
+ toolName: call.toolName,
1720
+ ctx,
1721
+ ts,
1722
+ sourceType: "engine",
1723
+ input: call.args
1724
+ })
1725
+ );
1726
+ }
1727
+ return out;
1728
+ }
1729
+ function extractSessionLevelFindings(calls, ctx) {
1730
+ if (!ctx.loopDetection.enabled || calls.length === 0) return [];
1731
+ const out = [];
1732
+ const seenLoopKeys = /* @__PURE__ */ new Set();
1733
+ const ONE_YEAR_MS = 365 * 24 * 60 * 60 * 1e3;
1734
+ const windowMs = ctx.loopDetection.windowSeconds <= 0 ? ONE_YEAR_MS : ctx.loopDetection.windowSeconds * 1e3;
1735
+ let records = [];
1736
+ let syntheticTs = 0;
1737
+ for (let i = 0; i < calls.length; i++) {
1738
+ const call = calls[i];
1739
+ const parsed = new Date(call.timestamp).getTime();
1740
+ const now = Number.isFinite(parsed) ? parsed : ++syntheticTs;
1741
+ const verdict = evaluateLoopWindow(
1742
+ records,
1743
+ call.toolName,
1744
+ call.args,
1745
+ ctx.loopDetection.threshold,
1746
+ windowMs,
1747
+ now
1748
+ );
1749
+ records = verdict.nextRecords;
1750
+ if (!verdict.looping) continue;
1751
+ const last = records[records.length - 1];
1752
+ const key = `${last.t}|${last.h}`;
1753
+ if (seenLoopKeys.has(key)) continue;
1754
+ seenLoopKeys.add(key);
1755
+ out.push({
1756
+ type: "loop",
1757
+ ruleName: "loop",
1758
+ verdict: "review",
1759
+ severity: "medium",
1760
+ reason: `Tool called ${verdict.count} times with identical args within window`,
1761
+ toolName: call.toolName,
1762
+ agent: ctx.agent,
1763
+ sessionId: ctx.sessionId,
1764
+ project: ctx.project,
1765
+ lineIndex: call.lineIndex,
1766
+ sourceType: "engine",
1767
+ firstSeenAt: call.timestamp,
1768
+ lastSeenAt: call.timestamp,
1769
+ occurrenceCount: 1,
1770
+ loopCount: verdict.count,
1771
+ loopKind: "loop",
1772
+ commandPreview: previewArgs(call.args, DEDUPE_PREVIEW_LEN),
1773
+ costUsd: verdict.count * COST_PER_LOOP_ITER_USD
1774
+ });
1775
+ }
1776
+ return out;
1777
+ }
1778
+ function toScanFinding(c) {
1779
+ const typeMap = {
1780
+ "smart-rule": null,
1781
+ "ast-fs-op": null,
1782
+ dlp: "dlp",
1783
+ pii: "pii",
1784
+ "sensitive-file-read": "sensitive-file-read",
1785
+ "privilege-escalation": "privilege-escalation",
1786
+ "destructive-op": "destructive-op",
1787
+ "pipe-to-shell": "pipe-to-shell",
1788
+ "eval-of-remote": "eval-of-remote",
1789
+ loop: "loop",
1790
+ "long-output-redacted": "long-output-redacted"
1791
+ };
1792
+ const sfType = typeMap[c.type];
1793
+ if (sfType === null) return null;
1794
+ return {
1795
+ sessionId: c.sessionId,
1796
+ type: sfType,
1797
+ ...c.patternName && { patternName: c.patternName },
1798
+ lineIndex: c.lineIndex
1799
+ };
1800
+ }
1801
+ function previewArgs(input, max) {
1802
+ const cmd = input.command ?? input.query ?? input.file_path ?? JSON.stringify(input);
1803
+ const s = String(cmd).replace(TERMINAL_ESCAPE_RE, "").replace(/\s+/g, " ").trim();
1804
+ return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
1805
+ }
1806
+ function makeFinding(args) {
1807
+ const f = {
1808
+ type: args.type,
1809
+ ruleName: args.ruleName,
1810
+ verdict: args.verdict,
1811
+ severity: args.severity,
1812
+ reason: args.reason,
1813
+ toolName: args.toolName,
1814
+ agent: args.ctx.agent,
1815
+ sessionId: args.ctx.sessionId,
1816
+ project: args.ctx.project,
1817
+ lineIndex: args.ctx.lineIndex,
1818
+ sourceType: args.sourceType,
1819
+ firstSeenAt: args.ts,
1820
+ lastSeenAt: args.ts,
1821
+ occurrenceCount: 1
1822
+ };
1823
+ if (args.shieldLabel) f.shieldLabel = args.shieldLabel;
1824
+ if (args.subjectPath) f.subjectPath = args.subjectPath;
1825
+ if (args.input) f.input = args.input;
1826
+ if (args.patternName) f.patternName = args.patternName;
1827
+ if (args.redactedSample) f.redactedSample = args.redactedSample;
1828
+ return f;
1829
+ }
1830
+ function* stringValues(obj, depth = 0) {
1831
+ if (depth > 6) return;
1832
+ if (typeof obj === "string") {
1833
+ if (obj.length > 0) yield obj;
1834
+ return;
1835
+ }
1836
+ if (!obj || typeof obj !== "object") return;
1837
+ if (Array.isArray(obj)) {
1838
+ for (const v of obj) yield* stringValues(v, depth + 1);
1839
+ return;
1840
+ }
1841
+ for (const v of Object.values(obj)) yield* stringValues(v, depth + 1);
1842
+ }
1843
+ var import_safe_regex2, import_mvdan_sh, import_picomatch, import_safe_regex22, import_safe_regex23, import_crypto2, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, 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, FS_OP_CACHE_MAX, fsOpCache, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, 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, COST_PER_LOOP_ITER_USD, DESTRUCTIVE_OP_RE, PRIVILEGE_ESCALATION_RE, SENSITIVE_PATH_RE, FILE_TOOLS, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC_RE, LONG_OUTPUT_THRESHOLD_BYTES, CANONICAL_EXTRACTOR_VERSION, DEDUPE_PREVIEW_LEN, TERMINAL_ESCAPE_RE;
1503
1844
  var init_dist = __esm({
1504
1845
  "packages/policy-engine/dist/index.mjs"() {
1505
1846
  "use strict";
@@ -2034,6 +2375,20 @@ var init_dist = __esm({
2034
2375
  )
2035
2376
  }
2036
2377
  ];
2378
+ BASH_TOOL_NAMES = /* @__PURE__ */ new Set([
2379
+ "bash",
2380
+ "execute_bash",
2381
+ "run_shell_command",
2382
+ "shell",
2383
+ "exec_command"
2384
+ ]);
2385
+ AST_FS_REGEX_RULES = /* @__PURE__ */ new Set([
2386
+ "block-rm-rf-home",
2387
+ "shield:project-jail:block-read-ssh",
2388
+ "shield:project-jail:block-read-aws",
2389
+ "shield:project-jail:block-read-env",
2390
+ "shield:project-jail:block-read-credentials"
2391
+ ]);
2037
2392
  FS_OP_CACHE_MAX = 5e3;
2038
2393
  fsOpCache = /* @__PURE__ */ new Map();
2039
2394
  SOURCE_COMMANDS = /* @__PURE__ */ new Set([
@@ -2890,6 +3245,31 @@ var init_dist = __esm({
2890
3245
  };
2891
3246
  LOOP_THRESHOLD_FOR_WASTE = 3;
2892
3247
  COST_PER_LOOP_ITER_USD = 6e-3;
3248
+ 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;
3249
+ PRIVILEGE_ESCALATION_RE = /\b(sudo|su)\b\s+[a-z]|\bchmod\s+(0?777|\+x)\b|\bchown\s+root\b/i;
3250
+ 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;
3251
+ FILE_TOOLS = /* @__PURE__ */ new Set([
3252
+ "read",
3253
+ "read_file",
3254
+ "edit",
3255
+ "edit_file",
3256
+ "write",
3257
+ "write_file",
3258
+ "multiedit",
3259
+ "grep",
3260
+ "grep_search",
3261
+ "glob",
3262
+ "list_files"
3263
+ ]);
3264
+ PII_EMAIL_RE = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/;
3265
+ PII_SSN_RE = /\b\d{3}-\d{2}-\d{4}\b/;
3266
+ PII_PHONE_RE = /\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]\d{3}[-.\s]\d{4}\b/;
3267
+ 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/;
3268
+ LONG_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
3269
+ CANONICAL_EXTRACTOR_VERSION = "canonical-v1";
3270
+ DEDUPE_PREVIEW_LEN = 120;
3271
+ TERMINAL_ESCAPE_RE = // eslint-disable-next-line no-control-regex
3272
+ /\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[@-_]|[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g;
2893
3273
  }
2894
3274
  });
2895
3275
 
@@ -7536,24 +7916,76 @@ var init_costSync = __esm({
7536
7916
  // src/daemon/scan-watermark.ts
7537
7917
  var scan_watermark_exports = {};
7538
7918
  __export(scan_watermark_exports, {
7919
+ WATERMARK_SCHEMA_VERSION: () => WATERMARK_SCHEMA_VERSION,
7539
7920
  extractFindingsFromLine: () => extractFindingsFromLine,
7540
7921
  loadWatermark: () => loadWatermark,
7922
+ markUploadComplete: () => markUploadComplete,
7541
7923
  saveWatermark: () => saveWatermark,
7542
7924
  scanDelta: () => scanDelta,
7543
7925
  tickScanWatcher: () => tickScanWatcher
7544
7926
  });
7927
+ function freshWatermark() {
7928
+ return {
7929
+ schemaVersion: WATERMARK_SCHEMA_VERSION,
7930
+ extractorVersion: CANONICAL_EXTRACTOR_VERSION,
7931
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
7932
+ files: {}
7933
+ };
7934
+ }
7545
7935
  function loadWatermark() {
7936
+ let raw;
7546
7937
  try {
7547
- const raw = import_fs16.default.readFileSync(WATERMARK_FILE(), "utf-8");
7548
- const parsed = JSON.parse(raw);
7549
- if (typeof parsed.createdAt === "string" && parsed.files && typeof parsed.files === "object") {
7550
- return parsed;
7551
- }
7938
+ raw = import_fs16.default.readFileSync(WATERMARK_FILE(), "utf-8");
7552
7939
  } catch {
7940
+ return { status: "fresh", wm: freshWatermark() };
7553
7941
  }
7554
- return { createdAt: (/* @__PURE__ */ new Date()).toISOString(), files: {} };
7942
+ let parsed;
7943
+ try {
7944
+ parsed = JSON.parse(raw);
7945
+ } catch {
7946
+ return { status: "fresh", wm: freshWatermark() };
7947
+ }
7948
+ if (typeof parsed.createdAt !== "string" || !parsed.files || typeof parsed.files !== "object") {
7949
+ return { status: "fresh", wm: freshWatermark() };
7950
+ }
7951
+ const fileSchemaVersion = typeof parsed.schemaVersion === "number" ? parsed.schemaVersion : 1;
7952
+ if (fileSchemaVersion > WATERMARK_SCHEMA_VERSION) {
7953
+ const wm2 = {
7954
+ schemaVersion: fileSchemaVersion,
7955
+ extractorVersion: typeof parsed.extractorVersion === "string" ? parsed.extractorVersion : CANONICAL_EXTRACTOR_VERSION,
7956
+ createdAt: parsed.createdAt,
7957
+ files: parsed.files
7958
+ };
7959
+ return { status: "schema-future", wm: wm2 };
7960
+ }
7961
+ const fileExtractorVersion = typeof parsed.extractorVersion === "string" ? parsed.extractorVersion : "";
7962
+ if (fileExtractorVersion !== CANONICAL_EXTRACTOR_VERSION) {
7963
+ const filesIn = parsed.files;
7964
+ const filesOut = {};
7965
+ for (const [k, v] of Object.entries(filesIn)) {
7966
+ filesOut[k] = { scannedTo: 0 };
7967
+ void v;
7968
+ }
7969
+ const wm2 = {
7970
+ schemaVersion: WATERMARK_SCHEMA_VERSION,
7971
+ extractorVersion: CANONICAL_EXTRACTOR_VERSION,
7972
+ pendingResetUploadAs: "totals",
7973
+ createdAt: parsed.createdAt,
7974
+ files: filesOut
7975
+ };
7976
+ return { status: "extractor-stale", wm: wm2 };
7977
+ }
7978
+ const wm = {
7979
+ schemaVersion: WATERMARK_SCHEMA_VERSION,
7980
+ extractorVersion: CANONICAL_EXTRACTOR_VERSION,
7981
+ ...parsed.pendingResetUploadAs === "totals" && { pendingResetUploadAs: "totals" },
7982
+ createdAt: parsed.createdAt,
7983
+ files: parsed.files
7984
+ };
7985
+ return { status: "current", wm };
7555
7986
  }
7556
7987
  function saveWatermark(wm) {
7988
+ if (wm.schemaVersion > WATERMARK_SCHEMA_VERSION) return;
7557
7989
  const target = WATERMARK_FILE();
7558
7990
  const dir = import_path18.default.dirname(target);
7559
7991
  if (!import_fs16.default.existsSync(dir)) import_fs16.default.mkdirSync(dir, { recursive: true });
@@ -7646,6 +8078,16 @@ function extractFindingsFromLine(line, sessionId, lineIndex) {
7646
8078
  }
7647
8079
  }
7648
8080
  }
8081
+ const ctx = {
8082
+ sessionId,
8083
+ lineIndex,
8084
+ project: "",
8085
+ agent: "claude",
8086
+ rules: [],
8087
+ toolInspection: { bash: "command", execute_bash: "command" },
8088
+ dlpEnabled: false
8089
+ // line-level DLP runs above already
8090
+ };
7649
8091
  const message = line.message;
7650
8092
  if (message && typeof message === "object") {
7651
8093
  const content = message.content;
@@ -7656,73 +8098,105 @@ function extractFindingsFromLine(line, sessionId, lineIndex) {
7656
8098
  if (b.type === "tool_result") {
7657
8099
  const c = b.content;
7658
8100
  const len = typeof c === "string" ? c.length : Array.isArray(c) ? JSON.stringify(c).length : 0;
7659
- if (len > LONG_OUTPUT_THRESHOLD_BYTES) {
8101
+ if (len > LONG_OUTPUT_THRESHOLD_BYTES2) {
7660
8102
  findings.push({
7661
8103
  type: "long-output-redacted",
7662
8104
  sessionId,
7663
8105
  lineIndex
7664
8106
  });
7665
8107
  }
8108
+ continue;
7666
8109
  }
7667
8110
  if (b.type !== "tool_use") continue;
7668
- const toolName = typeof b.name === "string" ? b.name.toLowerCase() : "";
7669
- const input = b.input;
7670
- if (FILE_TOOLS.has(toolName)) {
7671
- const filePath = typeof input?.file_path === "string" && input.file_path || typeof input?.path === "string" && input.path || typeof input?.pattern === "string" && input.pattern || "";
7672
- if (filePath && SENSITIVE_PATH_RE.test(filePath)) {
7673
- findings.push({
7674
- type: "sensitive-file-read",
7675
- sessionId,
7676
- lineIndex
7677
- });
7678
- }
7679
- }
7680
- if (toolName !== "bash" && toolName !== "execute_bash") continue;
7681
- const command = input && typeof input.command === "string" ? input.command : "";
7682
- if (!command) continue;
7683
- const verdict = detectDangerousShellExec(command);
7684
- if (verdict) {
7685
- findings.push({ type: "eval-of-remote", sessionId, lineIndex });
7686
- }
7687
- const pipe = analyzePipeChain(command);
7688
- if (pipe.isPipeline && pipe.risk === "critical") {
7689
- findings.push({ type: "pipe-to-shell", sessionId, lineIndex });
7690
- }
7691
- if (DESTRUCTIVE_OP_RE.test(command)) {
7692
- findings.push({ type: "destructive-op", sessionId, lineIndex });
7693
- }
7694
- if (PRIVILEGE_ESCALATION_RE.test(command)) {
7695
- findings.push({
7696
- type: "privilege-escalation",
7697
- sessionId,
7698
- lineIndex
7699
- });
8111
+ const toolName = typeof b.name === "string" ? b.name : "";
8112
+ const input = b.input ?? {};
8113
+ const call = {
8114
+ toolName,
8115
+ args: input,
8116
+ timestamp: typeof obj.timestamp === "string" ? obj.timestamp : ""
8117
+ };
8118
+ const canonical = extractCanonicalFindings(call, ctx);
8119
+ for (const cf of canonical) {
8120
+ const sf = toScanFinding(cf);
8121
+ if (sf) findings.push(sf);
7700
8122
  }
7701
8123
  }
7702
8124
  }
7703
8125
  }
7704
8126
  return findings;
7705
8127
  }
7706
- function detectPii(text) {
7707
- const found = /* @__PURE__ */ new Set();
7708
- if (/@/.test(text) && PII_EMAIL_RE.test(text)) found.add("Email");
7709
- if (/-/.test(text) && PII_SSN_RE.test(text)) found.add("SSN");
7710
- if (PII_PHONE_RE.test(text)) found.add("Phone");
7711
- if (PII_CC_RE.test(text)) found.add("Credit Card");
7712
- return [...found];
8128
+ function markUploadComplete() {
8129
+ const state = loadWatermark();
8130
+ if (state.status === "schema-future") return;
8131
+ if (state.status === "extractor-stale") return;
8132
+ if (!state.wm.pendingResetUploadAs) return;
8133
+ delete state.wm.pendingResetUploadAs;
8134
+ saveWatermark(state.wm);
7713
8135
  }
7714
8136
  async function tickScanWatcher() {
7715
8137
  if (process.env.NODE9_SCAN_DISABLE === "1") {
7716
- return {
7717
- findings: [],
7718
- totalToolCalls: 0,
7719
- toolCallsBySession: {},
7720
- filesScanned: 0,
7721
- filesNew: 0,
7722
- filesSkipped: 0
7723
- };
8138
+ return emptyTick("deltas");
8139
+ }
8140
+ const state = loadWatermark();
8141
+ if (state.status === "schema-future") {
8142
+ if (process.env.NODE9_DEBUG === "1") {
8143
+ process.stderr.write("[node9] watermark schema is from a newer daemon \u2014 skipping tick.\n");
8144
+ }
8145
+ return { ...emptyTick("deltas"), schemaFuture: true };
8146
+ }
8147
+ if (state.status === "extractor-stale") {
8148
+ if (process.env.NODE9_SKIP_WATERMARK_RESET === "1") {
8149
+ const acknowledged = readRawWatermarkPreservingOffsets();
8150
+ if (acknowledged) {
8151
+ saveWatermark(acknowledged);
8152
+ }
8153
+ process.stderr.write(
8154
+ "[node9] Extractor upgrade acknowledged via NODE9_SKIP_WATERMARK_RESET.\n Existing verdicts not refreshed \u2014 run `node9 scan --upload-history`\n to backfill them through the new pipeline.\n"
8155
+ );
8156
+ return runActualTick(loadWatermark().wm);
8157
+ }
8158
+ process.stderr.write(
8159
+ "[node9] Detector upgrade detected \u2014 re-scanning history through the new\n pipeline. Expect a one-time SaaS payload spike on this tick.\n Set NODE9_SKIP_WATERMARK_RESET=1 to skip.\n"
8160
+ );
8161
+ }
8162
+ return runActualTick(state.wm);
8163
+ }
8164
+ function emptyTick(uploadAs) {
8165
+ return {
8166
+ findings: [],
8167
+ totalToolCalls: 0,
8168
+ toolCallsBySession: {},
8169
+ filesScanned: 0,
8170
+ filesNew: 0,
8171
+ filesSkipped: 0,
8172
+ uploadAs,
8173
+ schemaFuture: false
8174
+ };
8175
+ }
8176
+ function readRawWatermarkPreservingOffsets() {
8177
+ let raw;
8178
+ try {
8179
+ raw = import_fs16.default.readFileSync(WATERMARK_FILE(), "utf-8");
8180
+ } catch {
8181
+ return null;
7724
8182
  }
7725
- const wm = loadWatermark();
8183
+ let parsed;
8184
+ try {
8185
+ parsed = JSON.parse(raw);
8186
+ } catch {
8187
+ return null;
8188
+ }
8189
+ if (typeof parsed.createdAt !== "string" || !parsed.files || typeof parsed.files !== "object") {
8190
+ return null;
8191
+ }
8192
+ return {
8193
+ schemaVersion: WATERMARK_SCHEMA_VERSION,
8194
+ extractorVersion: CANONICAL_EXTRACTOR_VERSION,
8195
+ createdAt: parsed.createdAt,
8196
+ files: parsed.files
8197
+ };
8198
+ }
8199
+ async function runActualTick(wm) {
7726
8200
  const watermarkCreatedAt = new Date(wm.createdAt).getTime();
7727
8201
  const findings = [];
7728
8202
  let totalToolCalls = 0;
@@ -7769,10 +8243,20 @@ async function tickScanWatcher() {
7769
8243
  wm.files[filePath] = { scannedTo: newScannedTo };
7770
8244
  filesScanned++;
7771
8245
  }
8246
+ const uploadAs = wm.pendingResetUploadAs === "totals" ? "totals" : "deltas";
7772
8247
  saveWatermark(wm);
7773
- return { findings, totalToolCalls, toolCallsBySession, filesScanned, filesNew, filesSkipped };
8248
+ return {
8249
+ findings,
8250
+ totalToolCalls,
8251
+ toolCallsBySession,
8252
+ filesScanned,
8253
+ filesNew,
8254
+ filesSkipped,
8255
+ uploadAs,
8256
+ schemaFuture: false
8257
+ };
7774
8258
  }
7775
- var import_fs16, import_os15, import_path18, import_readline, PROJECTS_DIR, WATERMARK_FILE, MAX_LINE_BYTES, DESTRUCTIVE_OP_RE, LONG_OUTPUT_THRESHOLD_BYTES, FILE_TOOLS, SENSITIVE_PATH_RE, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC_RE, PRIVILEGE_ESCALATION_RE;
8259
+ var import_fs16, import_os15, import_path18, import_readline, PROJECTS_DIR, WATERMARK_FILE, MAX_LINE_BYTES, WATERMARK_SCHEMA_VERSION, LONG_OUTPUT_THRESHOLD_BYTES2;
7776
8260
  var init_scan_watermark = __esm({
7777
8261
  "src/daemon/scan-watermark.ts"() {
7778
8262
  "use strict";
@@ -7785,27 +8269,8 @@ var init_scan_watermark = __esm({
7785
8269
  PROJECTS_DIR = () => import_path18.default.join(import_os15.default.homedir(), ".claude", "projects");
7786
8270
  WATERMARK_FILE = () => import_path18.default.join(import_os15.default.homedir(), ".node9", "scan-watermark.json");
7787
8271
  MAX_LINE_BYTES = 2 * 1024 * 1024;
7788
- 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;
7789
- LONG_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
7790
- FILE_TOOLS = /* @__PURE__ */ new Set([
7791
- "read",
7792
- "read_file",
7793
- "edit",
7794
- "edit_file",
7795
- "write",
7796
- "write_file",
7797
- "multiedit",
7798
- "grep",
7799
- "grep_search",
7800
- "glob",
7801
- "list_files"
7802
- ]);
7803
- 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;
7804
- PII_EMAIL_RE = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/;
7805
- PII_SSN_RE = /\b\d{3}-\d{2}-\d{4}\b/;
7806
- PII_PHONE_RE = /\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]\d{3}[-.\s]\d{4}\b/;
7807
- 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/;
7808
- PRIVILEGE_ESCALATION_RE = /\b(sudo|su)\b\s+[a-z]|\bchmod\s+(0?777|\+x)\b|\bchown\s+root\b/i;
8272
+ WATERMARK_SCHEMA_VERSION = 2;
8273
+ LONG_OUTPUT_THRESHOLD_BYTES2 = LONG_OUTPUT_THRESHOLD_BYTES;
7809
8274
  }
7810
8275
  });
7811
8276
 
@@ -7925,6 +8390,13 @@ async function runUploadHistory(opts) {
7925
8390
  let linesParsed = 0;
7926
8391
  let linesSkipped = 0;
7927
8392
  const dailyEntries = [];
8393
+ const liveLoopCfg = getConfig().policy.loopDetection;
8394
+ const loopCfg = {
8395
+ enabled: liveLoopCfg.enabled,
8396
+ threshold: 3,
8397
+ windowSeconds: 0
8398
+ // "no window" — engine treats this as session-wide
8399
+ };
7928
8400
  for (const { filePath, sessionId, projectDir } of iterateJsonlFiles(cutoffMs)) {
7929
8401
  filesScanned++;
7930
8402
  let content;
@@ -7934,6 +8406,7 @@ async function runUploadHistory(opts) {
7934
8406
  continue;
7935
8407
  }
7936
8408
  let lineIndex = 0;
8409
+ const sessionCalls = [];
7937
8410
  for (const line of content.split("\n")) {
7938
8411
  if (!line.trim()) continue;
7939
8412
  let obj;
@@ -7950,10 +8423,38 @@ async function runUploadHistory(opts) {
7950
8423
  if (obj["type"] === "assistant" && Array.isArray(msg?.content) && msg.content.some((b) => b.type === "tool_use")) {
7951
8424
  totalToolCalls++;
7952
8425
  toolCallsBySession[sessionId] = (toolCallsBySession[sessionId] ?? 0) + 1;
8426
+ const ts = typeof obj.timestamp === "string" ? obj.timestamp : "";
8427
+ for (const block of msg.content) {
8428
+ if (!block || typeof block !== "object") continue;
8429
+ const b = block;
8430
+ if (b.type !== "tool_use") continue;
8431
+ sessionCalls.push({
8432
+ toolName: typeof b.name === "string" ? b.name : "",
8433
+ args: b.input ?? {},
8434
+ timestamp: ts,
8435
+ lineIndex
8436
+ });
8437
+ }
7953
8438
  }
7954
8439
  linesParsed++;
7955
8440
  lineIndex++;
7956
8441
  }
8442
+ if (loopCfg.enabled && sessionCalls.length > 0) {
8443
+ const loops = extractSessionLevelFindings(sessionCalls, {
8444
+ sessionId,
8445
+ project: decodeProjectDirName(projectDir),
8446
+ agent: "claude",
8447
+ loopDetection: {
8448
+ enabled: loopCfg.enabled,
8449
+ threshold: loopCfg.threshold,
8450
+ windowSeconds: loopCfg.windowSeconds
8451
+ }
8452
+ });
8453
+ for (const cf of loops) {
8454
+ const sf = toScanFinding(cf);
8455
+ if (sf) findings.push(sf);
8456
+ }
8457
+ }
7957
8458
  const fallbackWorkingDir = decodeProjectDirName(projectDir);
7958
8459
  const dailyMap = parseJSONLFile(filePath, fallbackWorkingDir);
7959
8460
  for (const entry of dailyMap.values()) {
@@ -7978,7 +8479,8 @@ async function runUploadHistory(opts) {
7978
8479
  const scanUrl = creds.apiUrl.endsWith("/policies/sync") ? creds.apiUrl.replace(/\/policies\/sync$/, "/scan/report") : `${creds.apiUrl.replace(/\/$/, "")}/scan/report`;
7979
8480
  await postJson(scanUrl, creds.apiKey, {
7980
8481
  ...summary,
7981
- sessionTotals
8482
+ sessionTotals,
8483
+ extractorVersion: CANONICAL_EXTRACTOR_VERSION
7982
8484
  });
7983
8485
  console.log(import_chalk3.default.green(` \u2713 Uploaded scanner findings`));
7984
8486
  if (dailyEntries.length > 0) {
@@ -8115,7 +8617,7 @@ function fmtTs(ts) {
8115
8617
  }
8116
8618
  }
8117
8619
  function stripTerminalEscapes(s) {
8118
- return s.replace(TERMINAL_ESCAPE_RE, "");
8620
+ return s.replace(TERMINAL_ESCAPE_RE2, "");
8119
8621
  }
8120
8622
  function preview(input, max) {
8121
8623
  const cmd = input.command ?? input.query ?? input.file_path ?? JSON.stringify(input);
@@ -9389,7 +9891,7 @@ function renderNarrativeScorecard(input) {
9389
9891
  console.log("");
9390
9892
  }
9391
9893
  function registerScanCommand(program2) {
9392
- program2.command("scan").description("Forecast: scan agent history and show what node9 would catch if installed").option("--all", "Scan all history (default: last 30 days)").option("--days <n>", "Scan last N days of history", "30").option("--top <n>", "Max findings to show per rule (default: 5)", "5").option("--drill-down", "Show all findings with full commands and session IDs").option("--compact", "Compact one-screen scorecard \u2014 for screenshots and sharing").option("--narrative", "Severity-grouped report \u2014 for video / dramatic sharing").option(
9894
+ program2.command("scan").description("Forecast: scan agent history and show what node9 would catch if installed").option("--all", "Scan all history (default: last 90 days)").option("--days <n>", "Scan last N days of history", "90").option("--top <n>", "Max findings to show per rule (default: 5)", "5").option("--drill-down", "Show all findings with full commands and session IDs").option("--compact", "Compact one-screen scorecard \u2014 for screenshots and sharing").option("--narrative", "Severity-grouped report \u2014 for video / dramatic sharing").option(
9393
9895
  "--upload-history",
9394
9896
  "Upload aggregate counts from existing JSONL sessions to the SaaS dashboard. Defaults to last 3 months; override with --since. Idempotent (safe to re-run)."
9395
9897
  ).option(
@@ -9408,7 +9910,7 @@ function registerScanCommand(program2) {
9408
9910
  const previewWidth = 70;
9409
9911
  const startDate = options.all ? null : (() => {
9410
9912
  const d = /* @__PURE__ */ new Date();
9411
- d.setDate(d.getDate() - (parseInt(options.days, 10) || 30));
9913
+ d.setDate(d.getDate() - (parseInt(options.days, 10) || 90));
9412
9914
  d.setHours(0, 0, 0, 0);
9413
9915
  return d;
9414
9916
  })();
@@ -9482,7 +9984,7 @@ function registerScanCommand(program2) {
9482
9984
  );
9483
9985
  return;
9484
9986
  }
9485
- const rangeLabel = options.all ? import_chalk4.default.dim("all time") : import_chalk4.default.dim(`last ${options.days ?? 30} days`);
9987
+ const rangeLabel = options.all ? import_chalk4.default.dim("all time") : import_chalk4.default.dim(`last ${options.days ?? 90} days`);
9486
9988
  const dateRange = scan.firstDate && scan.lastDate ? import_chalk4.default.dim(` ${fmtTs(scan.firstDate)} \u2013 ${fmtTs(scan.lastDate)}`) : "";
9487
9989
  const breakdownParts = [];
9488
9990
  if (claudeScan.sessions > 0)
@@ -9768,7 +10270,7 @@ function registerScanCommand(program2) {
9768
10270
  }
9769
10271
  );
9770
10272
  }
9771
- var import_chalk4, import_fs18, import_path20, import_os17, CLAUDE_PRICING, GEMINI_PRICING, CODE_EXTENSIONS, SELF_OUTPUT_MARKERS, FIXTURE_TOKEN_PATTERNS, TERMINAL_ESCAPE_RE, LOOP_TOOLS, LOOP_THRESHOLD, LOOP_TIMESPAN_THRESHOLD_MS, STUCK_TOOLS_MIN_WASTE, STUCK_TOOLS_LIMIT, RECURRING_SESSION_THRESHOLD, STALE_AGE_DAYS, AST_FS_REGEX_RULES, DEFAULT_RULE_NAMES, classifyRuleSeverity2, narrativeRuleLabel2;
10273
+ var import_chalk4, import_fs18, import_path20, import_os17, CLAUDE_PRICING, GEMINI_PRICING, CODE_EXTENSIONS, SELF_OUTPUT_MARKERS, FIXTURE_TOKEN_PATTERNS, TERMINAL_ESCAPE_RE2, LOOP_TOOLS, LOOP_THRESHOLD, LOOP_TIMESPAN_THRESHOLD_MS, STUCK_TOOLS_MIN_WASTE, STUCK_TOOLS_LIMIT, RECURRING_SESSION_THRESHOLD, STALE_AGE_DAYS, DEFAULT_RULE_NAMES, classifyRuleSeverity2, narrativeRuleLabel2;
9772
10274
  var init_scan = __esm({
9773
10275
  "src/cli/commands/scan.ts"() {
9774
10276
  "use strict";
@@ -9848,7 +10350,7 @@ var init_scan = __esm({
9848
10350
  // long digit sequence — fixture, not entropy
9849
10351
  /qwerty/i
9850
10352
  ];
9851
- TERMINAL_ESCAPE_RE = /\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[@-_]|[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g;
10353
+ TERMINAL_ESCAPE_RE2 = /\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[@-_]|[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g;
9852
10354
  LOOP_TOOLS = /* @__PURE__ */ new Set([
9853
10355
  "bash",
9854
10356
  "execute_bash",
@@ -9865,13 +10367,6 @@ var init_scan = __esm({
9865
10367
  STUCK_TOOLS_LIMIT = 3;
9866
10368
  RECURRING_SESSION_THRESHOLD = 3;
9867
10369
  STALE_AGE_DAYS = 30;
9868
- AST_FS_REGEX_RULES = /* @__PURE__ */ new Set([
9869
- "block-rm-rf-home",
9870
- "shield:project-jail:block-read-ssh",
9871
- "shield:project-jail:block-read-aws",
9872
- "shield:project-jail:block-read-env",
9873
- "shield:project-jail:block-read-credentials"
9874
- ]);
9875
10370
  DEFAULT_RULE_NAMES = new Set(
9876
10371
  DEFAULT_CONFIG.policy.smartRules.map((r) => r.name).filter(Boolean)
9877
10372
  );
@@ -10418,33 +10913,11 @@ function shouldRebind(now = Date.now()) {
10418
10913
  }
10419
10914
  function startActivitySocket() {
10420
10915
  bindActivitySocket();
10421
- if (process.platform !== "win32") {
10422
- try {
10423
- activitySocketWatcher = import_fs20.default.watch(import_os18.default.tmpdir(), (eventType, filename) => {
10424
- if (filename !== import_path22.default.basename(ACTIVITY_SOCKET_PATH2)) return;
10425
- if (eventType !== "rename") return;
10426
- if (import_fs20.default.existsSync(ACTIVITY_SOCKET_PATH2)) return;
10427
- attemptRebind("watch-unlink");
10428
- });
10429
- activitySocketWatcher.on("error", (err2) => {
10430
- logActivitySocket(`watcher error: ${err2.message}`);
10431
- });
10432
- activitySocketWatcher.unref();
10433
- } catch (err2) {
10434
- logActivitySocket(`failed to start watcher: ${err2.message}`);
10435
- }
10436
- }
10437
10916
  activityHealthInterval = setInterval(() => {
10438
10917
  if (!import_fs20.default.existsSync(ACTIVITY_SOCKET_PATH2)) attemptRebind("health-probe");
10439
10918
  }, ACTIVITY_HEALTH_PROBE_MS);
10440
10919
  activityHealthInterval.unref();
10441
10920
  process.on("exit", () => {
10442
- if (activitySocketWatcher) {
10443
- try {
10444
- activitySocketWatcher.close();
10445
- } catch {
10446
- }
10447
- }
10448
10921
  if (activityHealthInterval) clearInterval(activityHealthInterval);
10449
10922
  try {
10450
10923
  import_fs20.default.unlinkSync(ACTIVITY_SOCKET_PATH2);
@@ -10579,7 +11052,7 @@ function bindActivitySocket() {
10579
11052
  });
10580
11053
  activitySocketServer = unixServer;
10581
11054
  }
10582
- var import_net2, import_fs20, import_path22, import_os18, import_crypto6, homeDir, DAEMON_PID_FILE, DECISIONS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, INSIGHT_COUNTS_FILE, pending, sseClients, suggestionTracker, taintStore, insightCounts, _abandonTimer, _hadBrowserClient, _daemonServer, daemonRejectionHandlerRegistered, AUTO_DENY_MS, TRUST_DURATIONS, autoStarted, ACTIVITY_SOCKET_PATH2, ACTIVITY_RING_SIZE, activityRing, LARGE_RESPONSE_RING_SIZE, largeResponseRing, cachedScanResult, cachedScanTs, SCAN_CACHE_TTL_MS, SECRET_KEY_RE, INPUT_PRICE_PER_1M, OUTPUT_PRICE_PER_1M, BYTES_PER_TOKEN, WRITE_TOOL_NAMES, ACTIVITY_REBIND_MAX_ATTEMPTS, ACTIVITY_REBIND_WINDOW_MS, ACTIVITY_HEALTH_PROBE_MS, activitySocketServer, activitySocketWatcher, activityHealthInterval, activityRebindAttempts, activityCircuitTripped;
11055
+ var import_net2, import_fs20, import_path22, import_os18, import_crypto6, homeDir, DAEMON_PID_FILE, DECISIONS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, INSIGHT_COUNTS_FILE, pending, sseClients, suggestionTracker, taintStore, insightCounts, _abandonTimer, _hadBrowserClient, _daemonServer, daemonRejectionHandlerRegistered, AUTO_DENY_MS, TRUST_DURATIONS, autoStarted, ACTIVITY_SOCKET_PATH2, ACTIVITY_RING_SIZE, activityRing, LARGE_RESPONSE_RING_SIZE, largeResponseRing, cachedScanResult, cachedScanTs, SCAN_CACHE_TTL_MS, SECRET_KEY_RE, INPUT_PRICE_PER_1M, OUTPUT_PRICE_PER_1M, BYTES_PER_TOKEN, WRITE_TOOL_NAMES, ACTIVITY_REBIND_MAX_ATTEMPTS, ACTIVITY_REBIND_WINDOW_MS, ACTIVITY_HEALTH_PROBE_MS, activitySocketServer, activityHealthInterval, activityRebindAttempts, activityCircuitTripped;
10583
11056
  var init_state2 = __esm({
10584
11057
  "src/daemon/state.ts"() {
10585
11058
  "use strict";
@@ -10642,9 +11115,8 @@ var init_state2 = __esm({
10642
11115
  ]);
10643
11116
  ACTIVITY_REBIND_MAX_ATTEMPTS = 5;
10644
11117
  ACTIVITY_REBIND_WINDOW_MS = 6e4;
10645
- ACTIVITY_HEALTH_PROBE_MS = 3e4;
11118
+ ACTIVITY_HEALTH_PROBE_MS = 2e3;
10646
11119
  activitySocketServer = null;
10647
- activitySocketWatcher = null;
10648
11120
  activityHealthInterval = null;
10649
11121
  activityRebindAttempts = [];
10650
11122
  activityCircuitTripped = false;
@@ -10850,16 +11322,19 @@ async function pushBlastSnapshot(creds) {
10850
11322
  async function pushScanSnapshot(creds) {
10851
11323
  try {
10852
11324
  const tick = await tickScanWatcher();
11325
+ if (tick.schemaFuture) return;
10853
11326
  if (tick.findings.length === 0 && tick.totalToolCalls === 0) {
10854
11327
  return;
10855
11328
  }
10856
11329
  const summary = summarizeScan(tick.findings, {
10857
11330
  totalToolCalls: tick.totalToolCalls
10858
11331
  });
10859
- const sessionDeltas = buildSessionDeltas(tick.findings, tick.toolCallsBySession);
11332
+ const perSession = buildSessionDeltas(tick.findings, tick.toolCallsBySession);
10860
11333
  const scanUrl = creds.apiUrl.endsWith("/policies/sync") ? creds.apiUrl.replace(/\/policies\/sync$/, "/scan/report") : null;
10861
11334
  if (!scanUrl) return;
11335
+ const body = tick.uploadAs === "totals" ? { ...summary, sessionTotals: perSession, extractorVersion: CANONICAL_EXTRACTOR_VERSION } : { ...summary, sessionDeltas: perSession, extractorVersion: CANONICAL_EXTRACTOR_VERSION };
10862
11336
  const parsed = new URL(scanUrl);
11337
+ let posted = false;
10863
11338
  await new Promise((resolve) => {
10864
11339
  const req = import_https2.default.request(
10865
11340
  {
@@ -10874,6 +11349,9 @@ async function pushScanSnapshot(creds) {
10874
11349
  timeout: 1e4
10875
11350
  },
10876
11351
  (res) => {
11352
+ if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
11353
+ posted = true;
11354
+ }
10877
11355
  res.resume();
10878
11356
  res.on("end", resolve);
10879
11357
  res.on("error", () => resolve());
@@ -10884,9 +11362,12 @@ async function pushScanSnapshot(creds) {
10884
11362
  req.destroy();
10885
11363
  resolve();
10886
11364
  });
10887
- req.write(JSON.stringify({ ...summary, sessionDeltas }));
11365
+ req.write(JSON.stringify(body));
10888
11366
  req.end();
10889
11367
  });
11368
+ if (posted && tick.uploadAs === "totals") {
11369
+ markUploadComplete();
11370
+ }
10890
11371
  } catch {
10891
11372
  }
10892
11373
  }