@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 +637 -156
- package/dist/cli.mjs +637 -156
- package/dist/index.js +255 -36
- package/dist/index.mjs +255 -36
- package/package.json +3 -1
package/dist/cli.mjs
CHANGED
|
@@ -580,6 +580,9 @@ function detectDangerousShellExec(command) {
|
|
|
580
580
|
return null;
|
|
581
581
|
}
|
|
582
582
|
}
|
|
583
|
+
function isBashTool(toolName) {
|
|
584
|
+
return BASH_TOOL_NAMES.has(toolName.toLowerCase());
|
|
585
|
+
}
|
|
583
586
|
function isProtectedHomePath(rawPath) {
|
|
584
587
|
let p = rawPath.replace(/^\$HOME[\\/]?|^\$\{HOME\}[\\/]?/, "~/");
|
|
585
588
|
let underHome = false;
|
|
@@ -1052,6 +1055,43 @@ function isSqlTool(toolName, toolInspection) {
|
|
|
1052
1055
|
const fieldName = toolInspection[matchingPattern];
|
|
1053
1056
|
return fieldName === "sql" || fieldName === "query";
|
|
1054
1057
|
}
|
|
1058
|
+
function pipeChainVerdict(command, isTrustedHost2) {
|
|
1059
|
+
const pipeAnalysis = analyzePipeChain(command);
|
|
1060
|
+
if (!pipeAnalysis.isPipeline) return null;
|
|
1061
|
+
if (pipeAnalysis.risk !== "critical" && pipeAnalysis.risk !== "high") return null;
|
|
1062
|
+
const sinks = pipeAnalysis.sinkTargets;
|
|
1063
|
+
const allTrusted = sinks.length > 0 && sinks.every((host) => isTrustedHost2 ? isTrustedHost2(host) : false);
|
|
1064
|
+
if (pipeAnalysis.risk === "critical") {
|
|
1065
|
+
if (allTrusted) {
|
|
1066
|
+
return {
|
|
1067
|
+
decision: "review",
|
|
1068
|
+
blockedByLabel: "Node9: Pipe-Chain to Trusted Host (obfuscated)",
|
|
1069
|
+
reason: `Obfuscated pipe to trusted host(s): ${sinks.join(", ")} \u2014 requires approval`,
|
|
1070
|
+
tier: 3
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
return {
|
|
1074
|
+
decision: "block",
|
|
1075
|
+
blockedByLabel: "Node9: Pipe-Chain Exfiltration (critical)",
|
|
1076
|
+
reason: `Sensitive file piped through obfuscator to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
|
|
1077
|
+
tier: 3
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
if (allTrusted) {
|
|
1081
|
+
return {
|
|
1082
|
+
decision: "allow",
|
|
1083
|
+
blockedByLabel: "Node9: Pipe-Chain to Trusted Host",
|
|
1084
|
+
reason: `Sensitive file piped to trusted host(s): ${sinks.join(", ")}`,
|
|
1085
|
+
tier: 3
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
return {
|
|
1089
|
+
decision: "review",
|
|
1090
|
+
blockedByLabel: "Node9: Pipe-Chain Exfiltration (high)",
|
|
1091
|
+
reason: `Sensitive file piped to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
|
|
1092
|
+
tier: 3
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1055
1095
|
async function evaluatePolicy(config, toolName, args, context = {}, hooks = {}) {
|
|
1056
1096
|
const { agent, cwd, activeEnvironment } = context;
|
|
1057
1097
|
const { checkProvenance: checkProvenance2, isTrustedHost: isTrustedHost2 } = hooks;
|
|
@@ -1067,9 +1107,27 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
1067
1107
|
}
|
|
1068
1108
|
}
|
|
1069
1109
|
if (wouldBeIgnored) return { decision: "allow" };
|
|
1110
|
+
const bashCommand = agent !== "Terminal" && isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : null;
|
|
1111
|
+
if (bashCommand !== null) {
|
|
1112
|
+
const pipeVerdict = pipeChainVerdict(bashCommand, isTrustedHost2);
|
|
1113
|
+
if (pipeVerdict) return pipeVerdict;
|
|
1114
|
+
const fsVerdict = analyzeFsOperation(bashCommand);
|
|
1115
|
+
if (fsVerdict) {
|
|
1116
|
+
const isShieldRule = fsVerdict.ruleName.startsWith("shield:");
|
|
1117
|
+
const labelPrefix = isShieldRule ? "project-jail (AST)" : "Node9 (AST)";
|
|
1118
|
+
return {
|
|
1119
|
+
decision: fsVerdict.verdict,
|
|
1120
|
+
blockedByLabel: `${labelPrefix}: ${fsVerdict.ruleName}`,
|
|
1121
|
+
reason: fsVerdict.reason,
|
|
1122
|
+
tier: 2,
|
|
1123
|
+
ruleName: fsVerdict.ruleName,
|
|
1124
|
+
ruleDescription: fsVerdict.reason
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1070
1128
|
if (config.policy.smartRules.length > 0) {
|
|
1071
1129
|
const matchedRule = config.policy.smartRules.find(
|
|
1072
|
-
(rule) => matchesPattern(toolName, rule.tool) && evaluateSmartConditions(args, rule)
|
|
1130
|
+
(rule) => matchesPattern(toolName, rule.tool) && !(bashCommand !== null && rule.name && AST_FS_REGEX_RULES.has(rule.name)) && evaluateSmartConditions(args, rule)
|
|
1073
1131
|
);
|
|
1074
1132
|
if (matchedRule) {
|
|
1075
1133
|
if (matchedRule.verdict === "allow")
|
|
@@ -1127,41 +1185,8 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
1127
1185
|
tier: 3
|
|
1128
1186
|
};
|
|
1129
1187
|
}
|
|
1130
|
-
const
|
|
1131
|
-
if (
|
|
1132
|
-
const sinks = pipeAnalysis.sinkTargets;
|
|
1133
|
-
const allTrusted = sinks.length > 0 && sinks.every((host) => isTrustedHost2 ? isTrustedHost2(host) : false);
|
|
1134
|
-
if (pipeAnalysis.risk === "critical") {
|
|
1135
|
-
if (allTrusted) {
|
|
1136
|
-
return {
|
|
1137
|
-
decision: "review",
|
|
1138
|
-
blockedByLabel: "Node9: Pipe-Chain to Trusted Host (obfuscated)",
|
|
1139
|
-
reason: `Obfuscated pipe to trusted host(s): ${sinks.join(", ")} \u2014 requires approval`,
|
|
1140
|
-
tier: 3
|
|
1141
|
-
};
|
|
1142
|
-
}
|
|
1143
|
-
return {
|
|
1144
|
-
decision: "block",
|
|
1145
|
-
blockedByLabel: "Node9: Pipe-Chain Exfiltration (critical)",
|
|
1146
|
-
reason: `Sensitive file piped through obfuscator to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
|
|
1147
|
-
tier: 3
|
|
1148
|
-
};
|
|
1149
|
-
}
|
|
1150
|
-
if (allTrusted) {
|
|
1151
|
-
return {
|
|
1152
|
-
decision: "allow",
|
|
1153
|
-
blockedByLabel: "Node9: Pipe-Chain to Trusted Host",
|
|
1154
|
-
reason: `Sensitive file piped to trusted host(s): ${sinks.join(", ")}`,
|
|
1155
|
-
tier: 3
|
|
1156
|
-
};
|
|
1157
|
-
}
|
|
1158
|
-
return {
|
|
1159
|
-
decision: "review",
|
|
1160
|
-
blockedByLabel: "Node9: Pipe-Chain Exfiltration (high)",
|
|
1161
|
-
reason: `Sensitive file piped to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
|
|
1162
|
-
tier: 3
|
|
1163
|
-
};
|
|
1164
|
-
}
|
|
1188
|
+
const ptVerdict = pipeChainVerdict(shellCommand, isTrustedHost2);
|
|
1189
|
+
if (ptVerdict) return ptVerdict;
|
|
1165
1190
|
const firstToken = analyzed.actions[0] ?? "";
|
|
1166
1191
|
if (["ssh", "scp", "rsync"].includes(firstToken)) {
|
|
1167
1192
|
const rawTokens = shellCommand.trim().split(/\s+/);
|
|
@@ -1483,7 +1508,323 @@ function summarizeBlast(result, opts = {}) {
|
|
|
1483
1508
|
}))
|
|
1484
1509
|
};
|
|
1485
1510
|
}
|
|
1486
|
-
|
|
1511
|
+
function detectPii(text) {
|
|
1512
|
+
const found = /* @__PURE__ */ new Set();
|
|
1513
|
+
if (/@/.test(text) && PII_EMAIL_RE.test(text)) found.add("Email");
|
|
1514
|
+
if (/-/.test(text) && PII_SSN_RE.test(text)) found.add("SSN");
|
|
1515
|
+
if (PII_PHONE_RE.test(text)) found.add("Phone");
|
|
1516
|
+
if (PII_CC_RE.test(text)) found.add("Credit Card");
|
|
1517
|
+
return [...found];
|
|
1518
|
+
}
|
|
1519
|
+
function extractCanonicalFindings(call, ctx) {
|
|
1520
|
+
const out = [];
|
|
1521
|
+
const ts = call.timestamp;
|
|
1522
|
+
const toolNameLower = call.toolName.toLowerCase();
|
|
1523
|
+
const command = typeof call.args.command === "string" ? call.args.command : null;
|
|
1524
|
+
const isBash = isBashTool(call.toolName) && command !== null;
|
|
1525
|
+
if (call.outputBytes !== void 0 && call.outputBytes > LONG_OUTPUT_THRESHOLD_BYTES) {
|
|
1526
|
+
out.push(
|
|
1527
|
+
makeFinding({
|
|
1528
|
+
type: "long-output-redacted",
|
|
1529
|
+
ruleName: "long-output-redacted",
|
|
1530
|
+
verdict: "review",
|
|
1531
|
+
severity: "medium",
|
|
1532
|
+
reason: `Tool output exceeded ${LONG_OUTPUT_THRESHOLD_BYTES} bytes and was redacted`,
|
|
1533
|
+
toolName: call.toolName,
|
|
1534
|
+
ctx,
|
|
1535
|
+
ts,
|
|
1536
|
+
sourceType: "engine"
|
|
1537
|
+
})
|
|
1538
|
+
);
|
|
1539
|
+
}
|
|
1540
|
+
if (ctx.dlpEnabled) {
|
|
1541
|
+
const dlp = scanArgs(call.args);
|
|
1542
|
+
if (dlp) {
|
|
1543
|
+
out.push(
|
|
1544
|
+
makeFinding({
|
|
1545
|
+
type: "dlp",
|
|
1546
|
+
ruleName: `dlp:${dlp.patternName}`,
|
|
1547
|
+
patternName: dlp.patternName,
|
|
1548
|
+
verdict: dlp.severity === "block" ? "block" : "review",
|
|
1549
|
+
severity: dlp.severity === "block" ? "critical" : "medium",
|
|
1550
|
+
reason: `${dlp.patternName} detected in ${dlp.fieldPath}`,
|
|
1551
|
+
toolName: call.toolName,
|
|
1552
|
+
ctx,
|
|
1553
|
+
ts,
|
|
1554
|
+
sourceType: "engine",
|
|
1555
|
+
input: call.args,
|
|
1556
|
+
redactedSample: dlp.redactedSample
|
|
1557
|
+
})
|
|
1558
|
+
);
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
for (const value of stringValues(call.args)) {
|
|
1562
|
+
const piiHits = detectPii(value);
|
|
1563
|
+
for (const pattern of piiHits) {
|
|
1564
|
+
out.push(
|
|
1565
|
+
makeFinding({
|
|
1566
|
+
type: "pii",
|
|
1567
|
+
ruleName: `pii:${pattern.toLowerCase().replace(/\s+/g, "-")}`,
|
|
1568
|
+
patternName: pattern,
|
|
1569
|
+
verdict: "review",
|
|
1570
|
+
severity: "medium",
|
|
1571
|
+
reason: `${pattern} pattern detected in tool input`,
|
|
1572
|
+
toolName: call.toolName,
|
|
1573
|
+
ctx,
|
|
1574
|
+
ts,
|
|
1575
|
+
sourceType: "engine"
|
|
1576
|
+
})
|
|
1577
|
+
);
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
if (FILE_TOOLS.has(toolNameLower)) {
|
|
1581
|
+
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 || "";
|
|
1582
|
+
if (filePath && SENSITIVE_PATH_RE.test(filePath)) {
|
|
1583
|
+
out.push(
|
|
1584
|
+
makeFinding({
|
|
1585
|
+
type: "sensitive-file-read",
|
|
1586
|
+
ruleName: "sensitive-file-read",
|
|
1587
|
+
verdict: "review",
|
|
1588
|
+
severity: "critical",
|
|
1589
|
+
reason: `Sensitive file path read via ${call.toolName}`,
|
|
1590
|
+
toolName: call.toolName,
|
|
1591
|
+
ctx,
|
|
1592
|
+
ts,
|
|
1593
|
+
sourceType: "engine",
|
|
1594
|
+
subjectPath: filePath
|
|
1595
|
+
})
|
|
1596
|
+
);
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
if (!isBash || command === null) {
|
|
1600
|
+
return out;
|
|
1601
|
+
}
|
|
1602
|
+
const fsVerdict = analyzeFsOperation(command);
|
|
1603
|
+
if (fsVerdict) {
|
|
1604
|
+
const isShield = fsVerdict.ruleName.startsWith("shield:");
|
|
1605
|
+
out.push(
|
|
1606
|
+
makeFinding({
|
|
1607
|
+
type: "ast-fs-op",
|
|
1608
|
+
ruleName: fsVerdict.ruleName,
|
|
1609
|
+
verdict: fsVerdict.verdict,
|
|
1610
|
+
severity: classifyRuleSeverity(fsVerdict.ruleName, fsVerdict.verdict),
|
|
1611
|
+
reason: fsVerdict.reason,
|
|
1612
|
+
toolName: call.toolName,
|
|
1613
|
+
ctx,
|
|
1614
|
+
ts,
|
|
1615
|
+
sourceType: isShield ? "shield" : "engine",
|
|
1616
|
+
shieldLabel: isShield ? "project-jail (AST)" : "Node9 (AST)",
|
|
1617
|
+
subjectPath: fsVerdict.path,
|
|
1618
|
+
input: call.args
|
|
1619
|
+
})
|
|
1620
|
+
);
|
|
1621
|
+
}
|
|
1622
|
+
for (const source of ctx.rules) {
|
|
1623
|
+
const r = source.rule;
|
|
1624
|
+
if (r.verdict === "allow") continue;
|
|
1625
|
+
if (r.tool && !matchesPattern(toolNameLower, r.tool)) continue;
|
|
1626
|
+
if (r.name && AST_FS_REGEX_RULES.has(r.name)) continue;
|
|
1627
|
+
if (!evaluateSmartConditions(call.args, r)) continue;
|
|
1628
|
+
out.push(
|
|
1629
|
+
makeFinding({
|
|
1630
|
+
type: "smart-rule",
|
|
1631
|
+
ruleName: r.name ?? r.tool,
|
|
1632
|
+
verdict: r.verdict === "block" ? "block" : "review",
|
|
1633
|
+
severity: classifyRuleSeverity(r.name ?? r.tool, r.verdict),
|
|
1634
|
+
reason: r.reason ?? `Smart rule ${r.name ?? r.tool} fired`,
|
|
1635
|
+
toolName: call.toolName,
|
|
1636
|
+
ctx,
|
|
1637
|
+
ts,
|
|
1638
|
+
sourceType: source.sourceType,
|
|
1639
|
+
shieldLabel: source.shieldLabel,
|
|
1640
|
+
input: call.args
|
|
1641
|
+
})
|
|
1642
|
+
);
|
|
1643
|
+
break;
|
|
1644
|
+
}
|
|
1645
|
+
const evalVerdict = detectDangerousShellExec(command);
|
|
1646
|
+
if (evalVerdict) {
|
|
1647
|
+
out.push(
|
|
1648
|
+
makeFinding({
|
|
1649
|
+
type: "eval-of-remote",
|
|
1650
|
+
ruleName: "eval-of-remote",
|
|
1651
|
+
verdict: evalVerdict,
|
|
1652
|
+
severity: classifyRuleSeverity("eval-remote", evalVerdict),
|
|
1653
|
+
reason: evalVerdict === "block" ? "Eval of remote download is a near-certain supply-chain attack" : "Eval of dynamic content (variable / subshell) requires approval",
|
|
1654
|
+
toolName: call.toolName,
|
|
1655
|
+
ctx,
|
|
1656
|
+
ts,
|
|
1657
|
+
sourceType: "engine",
|
|
1658
|
+
input: call.args
|
|
1659
|
+
})
|
|
1660
|
+
);
|
|
1661
|
+
}
|
|
1662
|
+
const pipe = analyzePipeChain(command);
|
|
1663
|
+
if (pipe.isPipeline && pipe.risk === "critical") {
|
|
1664
|
+
out.push(
|
|
1665
|
+
makeFinding({
|
|
1666
|
+
type: "pipe-to-shell",
|
|
1667
|
+
ruleName: "pipe-to-shell",
|
|
1668
|
+
verdict: "block",
|
|
1669
|
+
severity: "critical",
|
|
1670
|
+
reason: `Sensitive file piped through obfuscator to network sink: ${pipe.sourceFiles.join(", ")} \u2192 ${pipe.sinkTargets.join(", ")}`,
|
|
1671
|
+
toolName: call.toolName,
|
|
1672
|
+
ctx,
|
|
1673
|
+
ts,
|
|
1674
|
+
sourceType: "engine",
|
|
1675
|
+
input: call.args
|
|
1676
|
+
})
|
|
1677
|
+
);
|
|
1678
|
+
}
|
|
1679
|
+
if (DESTRUCTIVE_OP_RE.test(command)) {
|
|
1680
|
+
out.push(
|
|
1681
|
+
makeFinding({
|
|
1682
|
+
type: "destructive-op",
|
|
1683
|
+
ruleName: "destructive-op",
|
|
1684
|
+
verdict: "review",
|
|
1685
|
+
severity: "high",
|
|
1686
|
+
reason: "Destructive operation pattern detected",
|
|
1687
|
+
toolName: call.toolName,
|
|
1688
|
+
ctx,
|
|
1689
|
+
ts,
|
|
1690
|
+
sourceType: "engine",
|
|
1691
|
+
input: call.args
|
|
1692
|
+
})
|
|
1693
|
+
);
|
|
1694
|
+
}
|
|
1695
|
+
if (PRIVILEGE_ESCALATION_RE.test(command)) {
|
|
1696
|
+
out.push(
|
|
1697
|
+
makeFinding({
|
|
1698
|
+
type: "privilege-escalation",
|
|
1699
|
+
ruleName: "privilege-escalation",
|
|
1700
|
+
verdict: "review",
|
|
1701
|
+
severity: "high",
|
|
1702
|
+
reason: "Privilege-escalation pattern detected",
|
|
1703
|
+
toolName: call.toolName,
|
|
1704
|
+
ctx,
|
|
1705
|
+
ts,
|
|
1706
|
+
sourceType: "engine",
|
|
1707
|
+
input: call.args
|
|
1708
|
+
})
|
|
1709
|
+
);
|
|
1710
|
+
}
|
|
1711
|
+
return out;
|
|
1712
|
+
}
|
|
1713
|
+
function extractSessionLevelFindings(calls, ctx) {
|
|
1714
|
+
if (!ctx.loopDetection.enabled || calls.length === 0) return [];
|
|
1715
|
+
const out = [];
|
|
1716
|
+
const seenLoopKeys = /* @__PURE__ */ new Set();
|
|
1717
|
+
const ONE_YEAR_MS = 365 * 24 * 60 * 60 * 1e3;
|
|
1718
|
+
const windowMs = ctx.loopDetection.windowSeconds <= 0 ? ONE_YEAR_MS : ctx.loopDetection.windowSeconds * 1e3;
|
|
1719
|
+
let records = [];
|
|
1720
|
+
let syntheticTs = 0;
|
|
1721
|
+
for (let i = 0; i < calls.length; i++) {
|
|
1722
|
+
const call = calls[i];
|
|
1723
|
+
const parsed = new Date(call.timestamp).getTime();
|
|
1724
|
+
const now = Number.isFinite(parsed) ? parsed : ++syntheticTs;
|
|
1725
|
+
const verdict = evaluateLoopWindow(
|
|
1726
|
+
records,
|
|
1727
|
+
call.toolName,
|
|
1728
|
+
call.args,
|
|
1729
|
+
ctx.loopDetection.threshold,
|
|
1730
|
+
windowMs,
|
|
1731
|
+
now
|
|
1732
|
+
);
|
|
1733
|
+
records = verdict.nextRecords;
|
|
1734
|
+
if (!verdict.looping) continue;
|
|
1735
|
+
const last = records[records.length - 1];
|
|
1736
|
+
const key = `${last.t}|${last.h}`;
|
|
1737
|
+
if (seenLoopKeys.has(key)) continue;
|
|
1738
|
+
seenLoopKeys.add(key);
|
|
1739
|
+
out.push({
|
|
1740
|
+
type: "loop",
|
|
1741
|
+
ruleName: "loop",
|
|
1742
|
+
verdict: "review",
|
|
1743
|
+
severity: "medium",
|
|
1744
|
+
reason: `Tool called ${verdict.count} times with identical args within window`,
|
|
1745
|
+
toolName: call.toolName,
|
|
1746
|
+
agent: ctx.agent,
|
|
1747
|
+
sessionId: ctx.sessionId,
|
|
1748
|
+
project: ctx.project,
|
|
1749
|
+
lineIndex: call.lineIndex,
|
|
1750
|
+
sourceType: "engine",
|
|
1751
|
+
firstSeenAt: call.timestamp,
|
|
1752
|
+
lastSeenAt: call.timestamp,
|
|
1753
|
+
occurrenceCount: 1,
|
|
1754
|
+
loopCount: verdict.count,
|
|
1755
|
+
loopKind: "loop",
|
|
1756
|
+
commandPreview: previewArgs(call.args, DEDUPE_PREVIEW_LEN),
|
|
1757
|
+
costUsd: verdict.count * COST_PER_LOOP_ITER_USD
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
return out;
|
|
1761
|
+
}
|
|
1762
|
+
function toScanFinding(c) {
|
|
1763
|
+
const typeMap = {
|
|
1764
|
+
"smart-rule": null,
|
|
1765
|
+
"ast-fs-op": null,
|
|
1766
|
+
dlp: "dlp",
|
|
1767
|
+
pii: "pii",
|
|
1768
|
+
"sensitive-file-read": "sensitive-file-read",
|
|
1769
|
+
"privilege-escalation": "privilege-escalation",
|
|
1770
|
+
"destructive-op": "destructive-op",
|
|
1771
|
+
"pipe-to-shell": "pipe-to-shell",
|
|
1772
|
+
"eval-of-remote": "eval-of-remote",
|
|
1773
|
+
loop: "loop",
|
|
1774
|
+
"long-output-redacted": "long-output-redacted"
|
|
1775
|
+
};
|
|
1776
|
+
const sfType = typeMap[c.type];
|
|
1777
|
+
if (sfType === null) return null;
|
|
1778
|
+
return {
|
|
1779
|
+
sessionId: c.sessionId,
|
|
1780
|
+
type: sfType,
|
|
1781
|
+
...c.patternName && { patternName: c.patternName },
|
|
1782
|
+
lineIndex: c.lineIndex
|
|
1783
|
+
};
|
|
1784
|
+
}
|
|
1785
|
+
function previewArgs(input, max) {
|
|
1786
|
+
const cmd = input.command ?? input.query ?? input.file_path ?? JSON.stringify(input);
|
|
1787
|
+
const s = String(cmd).replace(TERMINAL_ESCAPE_RE, "").replace(/\s+/g, " ").trim();
|
|
1788
|
+
return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
|
|
1789
|
+
}
|
|
1790
|
+
function makeFinding(args) {
|
|
1791
|
+
const f = {
|
|
1792
|
+
type: args.type,
|
|
1793
|
+
ruleName: args.ruleName,
|
|
1794
|
+
verdict: args.verdict,
|
|
1795
|
+
severity: args.severity,
|
|
1796
|
+
reason: args.reason,
|
|
1797
|
+
toolName: args.toolName,
|
|
1798
|
+
agent: args.ctx.agent,
|
|
1799
|
+
sessionId: args.ctx.sessionId,
|
|
1800
|
+
project: args.ctx.project,
|
|
1801
|
+
lineIndex: args.ctx.lineIndex,
|
|
1802
|
+
sourceType: args.sourceType,
|
|
1803
|
+
firstSeenAt: args.ts,
|
|
1804
|
+
lastSeenAt: args.ts,
|
|
1805
|
+
occurrenceCount: 1
|
|
1806
|
+
};
|
|
1807
|
+
if (args.shieldLabel) f.shieldLabel = args.shieldLabel;
|
|
1808
|
+
if (args.subjectPath) f.subjectPath = args.subjectPath;
|
|
1809
|
+
if (args.input) f.input = args.input;
|
|
1810
|
+
if (args.patternName) f.patternName = args.patternName;
|
|
1811
|
+
if (args.redactedSample) f.redactedSample = args.redactedSample;
|
|
1812
|
+
return f;
|
|
1813
|
+
}
|
|
1814
|
+
function* stringValues(obj, depth = 0) {
|
|
1815
|
+
if (depth > 6) return;
|
|
1816
|
+
if (typeof obj === "string") {
|
|
1817
|
+
if (obj.length > 0) yield obj;
|
|
1818
|
+
return;
|
|
1819
|
+
}
|
|
1820
|
+
if (!obj || typeof obj !== "object") return;
|
|
1821
|
+
if (Array.isArray(obj)) {
|
|
1822
|
+
for (const v of obj) yield* stringValues(v, depth + 1);
|
|
1823
|
+
return;
|
|
1824
|
+
}
|
|
1825
|
+
for (const v of Object.values(obj)) yield* stringValues(v, depth + 1);
|
|
1826
|
+
}
|
|
1827
|
+
var 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;
|
|
1487
1828
|
var init_dist = __esm({
|
|
1488
1829
|
"packages/policy-engine/dist/index.mjs"() {
|
|
1489
1830
|
"use strict";
|
|
@@ -2012,6 +2353,20 @@ var init_dist = __esm({
|
|
|
2012
2353
|
)
|
|
2013
2354
|
}
|
|
2014
2355
|
];
|
|
2356
|
+
BASH_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
2357
|
+
"bash",
|
|
2358
|
+
"execute_bash",
|
|
2359
|
+
"run_shell_command",
|
|
2360
|
+
"shell",
|
|
2361
|
+
"exec_command"
|
|
2362
|
+
]);
|
|
2363
|
+
AST_FS_REGEX_RULES = /* @__PURE__ */ new Set([
|
|
2364
|
+
"block-rm-rf-home",
|
|
2365
|
+
"shield:project-jail:block-read-ssh",
|
|
2366
|
+
"shield:project-jail:block-read-aws",
|
|
2367
|
+
"shield:project-jail:block-read-env",
|
|
2368
|
+
"shield:project-jail:block-read-credentials"
|
|
2369
|
+
]);
|
|
2015
2370
|
FS_OP_CACHE_MAX = 5e3;
|
|
2016
2371
|
fsOpCache = /* @__PURE__ */ new Map();
|
|
2017
2372
|
SOURCE_COMMANDS = /* @__PURE__ */ new Set([
|
|
@@ -2868,6 +3223,31 @@ var init_dist = __esm({
|
|
|
2868
3223
|
};
|
|
2869
3224
|
LOOP_THRESHOLD_FOR_WASTE = 3;
|
|
2870
3225
|
COST_PER_LOOP_ITER_USD = 6e-3;
|
|
3226
|
+
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;
|
|
3227
|
+
PRIVILEGE_ESCALATION_RE = /\b(sudo|su)\b\s+[a-z]|\bchmod\s+(0?777|\+x)\b|\bchown\s+root\b/i;
|
|
3228
|
+
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;
|
|
3229
|
+
FILE_TOOLS = /* @__PURE__ */ new Set([
|
|
3230
|
+
"read",
|
|
3231
|
+
"read_file",
|
|
3232
|
+
"edit",
|
|
3233
|
+
"edit_file",
|
|
3234
|
+
"write",
|
|
3235
|
+
"write_file",
|
|
3236
|
+
"multiedit",
|
|
3237
|
+
"grep",
|
|
3238
|
+
"grep_search",
|
|
3239
|
+
"glob",
|
|
3240
|
+
"list_files"
|
|
3241
|
+
]);
|
|
3242
|
+
PII_EMAIL_RE = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/;
|
|
3243
|
+
PII_SSN_RE = /\b\d{3}-\d{2}-\d{4}\b/;
|
|
3244
|
+
PII_PHONE_RE = /\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]\d{3}[-.\s]\d{4}\b/;
|
|
3245
|
+
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/;
|
|
3246
|
+
LONG_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
|
|
3247
|
+
CANONICAL_EXTRACTOR_VERSION = "canonical-v1";
|
|
3248
|
+
DEDUPE_PREVIEW_LEN = 120;
|
|
3249
|
+
TERMINAL_ESCAPE_RE = // eslint-disable-next-line no-control-regex
|
|
3250
|
+
/\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[@-_]|[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g;
|
|
2871
3251
|
}
|
|
2872
3252
|
});
|
|
2873
3253
|
|
|
@@ -7511,8 +7891,10 @@ var init_costSync = __esm({
|
|
|
7511
7891
|
// src/daemon/scan-watermark.ts
|
|
7512
7892
|
var scan_watermark_exports = {};
|
|
7513
7893
|
__export(scan_watermark_exports, {
|
|
7894
|
+
WATERMARK_SCHEMA_VERSION: () => WATERMARK_SCHEMA_VERSION,
|
|
7514
7895
|
extractFindingsFromLine: () => extractFindingsFromLine,
|
|
7515
7896
|
loadWatermark: () => loadWatermark,
|
|
7897
|
+
markUploadComplete: () => markUploadComplete,
|
|
7516
7898
|
saveWatermark: () => saveWatermark,
|
|
7517
7899
|
scanDelta: () => scanDelta,
|
|
7518
7900
|
tickScanWatcher: () => tickScanWatcher
|
|
@@ -7521,18 +7903,68 @@ import fs16 from "fs";
|
|
|
7521
7903
|
import os15 from "os";
|
|
7522
7904
|
import path18 from "path";
|
|
7523
7905
|
import readline from "readline";
|
|
7906
|
+
function freshWatermark() {
|
|
7907
|
+
return {
|
|
7908
|
+
schemaVersion: WATERMARK_SCHEMA_VERSION,
|
|
7909
|
+
extractorVersion: CANONICAL_EXTRACTOR_VERSION,
|
|
7910
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7911
|
+
files: {}
|
|
7912
|
+
};
|
|
7913
|
+
}
|
|
7524
7914
|
function loadWatermark() {
|
|
7915
|
+
let raw;
|
|
7525
7916
|
try {
|
|
7526
|
-
|
|
7527
|
-
const parsed = JSON.parse(raw);
|
|
7528
|
-
if (typeof parsed.createdAt === "string" && parsed.files && typeof parsed.files === "object") {
|
|
7529
|
-
return parsed;
|
|
7530
|
-
}
|
|
7917
|
+
raw = fs16.readFileSync(WATERMARK_FILE(), "utf-8");
|
|
7531
7918
|
} catch {
|
|
7919
|
+
return { status: "fresh", wm: freshWatermark() };
|
|
7532
7920
|
}
|
|
7533
|
-
|
|
7921
|
+
let parsed;
|
|
7922
|
+
try {
|
|
7923
|
+
parsed = JSON.parse(raw);
|
|
7924
|
+
} catch {
|
|
7925
|
+
return { status: "fresh", wm: freshWatermark() };
|
|
7926
|
+
}
|
|
7927
|
+
if (typeof parsed.createdAt !== "string" || !parsed.files || typeof parsed.files !== "object") {
|
|
7928
|
+
return { status: "fresh", wm: freshWatermark() };
|
|
7929
|
+
}
|
|
7930
|
+
const fileSchemaVersion = typeof parsed.schemaVersion === "number" ? parsed.schemaVersion : 1;
|
|
7931
|
+
if (fileSchemaVersion > WATERMARK_SCHEMA_VERSION) {
|
|
7932
|
+
const wm2 = {
|
|
7933
|
+
schemaVersion: fileSchemaVersion,
|
|
7934
|
+
extractorVersion: typeof parsed.extractorVersion === "string" ? parsed.extractorVersion : CANONICAL_EXTRACTOR_VERSION,
|
|
7935
|
+
createdAt: parsed.createdAt,
|
|
7936
|
+
files: parsed.files
|
|
7937
|
+
};
|
|
7938
|
+
return { status: "schema-future", wm: wm2 };
|
|
7939
|
+
}
|
|
7940
|
+
const fileExtractorVersion = typeof parsed.extractorVersion === "string" ? parsed.extractorVersion : "";
|
|
7941
|
+
if (fileExtractorVersion !== CANONICAL_EXTRACTOR_VERSION) {
|
|
7942
|
+
const filesIn = parsed.files;
|
|
7943
|
+
const filesOut = {};
|
|
7944
|
+
for (const [k, v] of Object.entries(filesIn)) {
|
|
7945
|
+
filesOut[k] = { scannedTo: 0 };
|
|
7946
|
+
void v;
|
|
7947
|
+
}
|
|
7948
|
+
const wm2 = {
|
|
7949
|
+
schemaVersion: WATERMARK_SCHEMA_VERSION,
|
|
7950
|
+
extractorVersion: CANONICAL_EXTRACTOR_VERSION,
|
|
7951
|
+
pendingResetUploadAs: "totals",
|
|
7952
|
+
createdAt: parsed.createdAt,
|
|
7953
|
+
files: filesOut
|
|
7954
|
+
};
|
|
7955
|
+
return { status: "extractor-stale", wm: wm2 };
|
|
7956
|
+
}
|
|
7957
|
+
const wm = {
|
|
7958
|
+
schemaVersion: WATERMARK_SCHEMA_VERSION,
|
|
7959
|
+
extractorVersion: CANONICAL_EXTRACTOR_VERSION,
|
|
7960
|
+
...parsed.pendingResetUploadAs === "totals" && { pendingResetUploadAs: "totals" },
|
|
7961
|
+
createdAt: parsed.createdAt,
|
|
7962
|
+
files: parsed.files
|
|
7963
|
+
};
|
|
7964
|
+
return { status: "current", wm };
|
|
7534
7965
|
}
|
|
7535
7966
|
function saveWatermark(wm) {
|
|
7967
|
+
if (wm.schemaVersion > WATERMARK_SCHEMA_VERSION) return;
|
|
7536
7968
|
const target = WATERMARK_FILE();
|
|
7537
7969
|
const dir = path18.dirname(target);
|
|
7538
7970
|
if (!fs16.existsSync(dir)) fs16.mkdirSync(dir, { recursive: true });
|
|
@@ -7625,6 +8057,16 @@ function extractFindingsFromLine(line, sessionId, lineIndex) {
|
|
|
7625
8057
|
}
|
|
7626
8058
|
}
|
|
7627
8059
|
}
|
|
8060
|
+
const ctx = {
|
|
8061
|
+
sessionId,
|
|
8062
|
+
lineIndex,
|
|
8063
|
+
project: "",
|
|
8064
|
+
agent: "claude",
|
|
8065
|
+
rules: [],
|
|
8066
|
+
toolInspection: { bash: "command", execute_bash: "command" },
|
|
8067
|
+
dlpEnabled: false
|
|
8068
|
+
// line-level DLP runs above already
|
|
8069
|
+
};
|
|
7628
8070
|
const message = line.message;
|
|
7629
8071
|
if (message && typeof message === "object") {
|
|
7630
8072
|
const content = message.content;
|
|
@@ -7635,73 +8077,105 @@ function extractFindingsFromLine(line, sessionId, lineIndex) {
|
|
|
7635
8077
|
if (b.type === "tool_result") {
|
|
7636
8078
|
const c = b.content;
|
|
7637
8079
|
const len = typeof c === "string" ? c.length : Array.isArray(c) ? JSON.stringify(c).length : 0;
|
|
7638
|
-
if (len >
|
|
8080
|
+
if (len > LONG_OUTPUT_THRESHOLD_BYTES2) {
|
|
7639
8081
|
findings.push({
|
|
7640
8082
|
type: "long-output-redacted",
|
|
7641
8083
|
sessionId,
|
|
7642
8084
|
lineIndex
|
|
7643
8085
|
});
|
|
7644
8086
|
}
|
|
8087
|
+
continue;
|
|
7645
8088
|
}
|
|
7646
8089
|
if (b.type !== "tool_use") continue;
|
|
7647
|
-
const toolName = typeof b.name === "string" ? b.name
|
|
7648
|
-
const input = b.input;
|
|
7649
|
-
|
|
7650
|
-
|
|
7651
|
-
|
|
7652
|
-
|
|
7653
|
-
|
|
7654
|
-
|
|
7655
|
-
|
|
7656
|
-
|
|
7657
|
-
|
|
7658
|
-
}
|
|
7659
|
-
if (toolName !== "bash" && toolName !== "execute_bash") continue;
|
|
7660
|
-
const command = input && typeof input.command === "string" ? input.command : "";
|
|
7661
|
-
if (!command) continue;
|
|
7662
|
-
const verdict = detectDangerousShellExec(command);
|
|
7663
|
-
if (verdict) {
|
|
7664
|
-
findings.push({ type: "eval-of-remote", sessionId, lineIndex });
|
|
7665
|
-
}
|
|
7666
|
-
const pipe = analyzePipeChain(command);
|
|
7667
|
-
if (pipe.isPipeline && pipe.risk === "critical") {
|
|
7668
|
-
findings.push({ type: "pipe-to-shell", sessionId, lineIndex });
|
|
7669
|
-
}
|
|
7670
|
-
if (DESTRUCTIVE_OP_RE.test(command)) {
|
|
7671
|
-
findings.push({ type: "destructive-op", sessionId, lineIndex });
|
|
7672
|
-
}
|
|
7673
|
-
if (PRIVILEGE_ESCALATION_RE.test(command)) {
|
|
7674
|
-
findings.push({
|
|
7675
|
-
type: "privilege-escalation",
|
|
7676
|
-
sessionId,
|
|
7677
|
-
lineIndex
|
|
7678
|
-
});
|
|
8090
|
+
const toolName = typeof b.name === "string" ? b.name : "";
|
|
8091
|
+
const input = b.input ?? {};
|
|
8092
|
+
const call = {
|
|
8093
|
+
toolName,
|
|
8094
|
+
args: input,
|
|
8095
|
+
timestamp: typeof obj.timestamp === "string" ? obj.timestamp : ""
|
|
8096
|
+
};
|
|
8097
|
+
const canonical = extractCanonicalFindings(call, ctx);
|
|
8098
|
+
for (const cf of canonical) {
|
|
8099
|
+
const sf = toScanFinding(cf);
|
|
8100
|
+
if (sf) findings.push(sf);
|
|
7679
8101
|
}
|
|
7680
8102
|
}
|
|
7681
8103
|
}
|
|
7682
8104
|
}
|
|
7683
8105
|
return findings;
|
|
7684
8106
|
}
|
|
7685
|
-
function
|
|
7686
|
-
const
|
|
7687
|
-
if (
|
|
7688
|
-
if (
|
|
7689
|
-
if (
|
|
7690
|
-
|
|
7691
|
-
|
|
8107
|
+
function markUploadComplete() {
|
|
8108
|
+
const state = loadWatermark();
|
|
8109
|
+
if (state.status === "schema-future") return;
|
|
8110
|
+
if (state.status === "extractor-stale") return;
|
|
8111
|
+
if (!state.wm.pendingResetUploadAs) return;
|
|
8112
|
+
delete state.wm.pendingResetUploadAs;
|
|
8113
|
+
saveWatermark(state.wm);
|
|
7692
8114
|
}
|
|
7693
8115
|
async function tickScanWatcher() {
|
|
7694
8116
|
if (process.env.NODE9_SCAN_DISABLE === "1") {
|
|
7695
|
-
return
|
|
7696
|
-
|
|
7697
|
-
|
|
7698
|
-
|
|
7699
|
-
|
|
7700
|
-
|
|
7701
|
-
|
|
7702
|
-
};
|
|
8117
|
+
return emptyTick("deltas");
|
|
8118
|
+
}
|
|
8119
|
+
const state = loadWatermark();
|
|
8120
|
+
if (state.status === "schema-future") {
|
|
8121
|
+
if (process.env.NODE9_DEBUG === "1") {
|
|
8122
|
+
process.stderr.write("[node9] watermark schema is from a newer daemon \u2014 skipping tick.\n");
|
|
8123
|
+
}
|
|
8124
|
+
return { ...emptyTick("deltas"), schemaFuture: true };
|
|
8125
|
+
}
|
|
8126
|
+
if (state.status === "extractor-stale") {
|
|
8127
|
+
if (process.env.NODE9_SKIP_WATERMARK_RESET === "1") {
|
|
8128
|
+
const acknowledged = readRawWatermarkPreservingOffsets();
|
|
8129
|
+
if (acknowledged) {
|
|
8130
|
+
saveWatermark(acknowledged);
|
|
8131
|
+
}
|
|
8132
|
+
process.stderr.write(
|
|
8133
|
+
"[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"
|
|
8134
|
+
);
|
|
8135
|
+
return runActualTick(loadWatermark().wm);
|
|
8136
|
+
}
|
|
8137
|
+
process.stderr.write(
|
|
8138
|
+
"[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"
|
|
8139
|
+
);
|
|
8140
|
+
}
|
|
8141
|
+
return runActualTick(state.wm);
|
|
8142
|
+
}
|
|
8143
|
+
function emptyTick(uploadAs) {
|
|
8144
|
+
return {
|
|
8145
|
+
findings: [],
|
|
8146
|
+
totalToolCalls: 0,
|
|
8147
|
+
toolCallsBySession: {},
|
|
8148
|
+
filesScanned: 0,
|
|
8149
|
+
filesNew: 0,
|
|
8150
|
+
filesSkipped: 0,
|
|
8151
|
+
uploadAs,
|
|
8152
|
+
schemaFuture: false
|
|
8153
|
+
};
|
|
8154
|
+
}
|
|
8155
|
+
function readRawWatermarkPreservingOffsets() {
|
|
8156
|
+
let raw;
|
|
8157
|
+
try {
|
|
8158
|
+
raw = fs16.readFileSync(WATERMARK_FILE(), "utf-8");
|
|
8159
|
+
} catch {
|
|
8160
|
+
return null;
|
|
7703
8161
|
}
|
|
7704
|
-
|
|
8162
|
+
let parsed;
|
|
8163
|
+
try {
|
|
8164
|
+
parsed = JSON.parse(raw);
|
|
8165
|
+
} catch {
|
|
8166
|
+
return null;
|
|
8167
|
+
}
|
|
8168
|
+
if (typeof parsed.createdAt !== "string" || !parsed.files || typeof parsed.files !== "object") {
|
|
8169
|
+
return null;
|
|
8170
|
+
}
|
|
8171
|
+
return {
|
|
8172
|
+
schemaVersion: WATERMARK_SCHEMA_VERSION,
|
|
8173
|
+
extractorVersion: CANONICAL_EXTRACTOR_VERSION,
|
|
8174
|
+
createdAt: parsed.createdAt,
|
|
8175
|
+
files: parsed.files
|
|
8176
|
+
};
|
|
8177
|
+
}
|
|
8178
|
+
async function runActualTick(wm) {
|
|
7705
8179
|
const watermarkCreatedAt = new Date(wm.createdAt).getTime();
|
|
7706
8180
|
const findings = [];
|
|
7707
8181
|
let totalToolCalls = 0;
|
|
@@ -7748,10 +8222,20 @@ async function tickScanWatcher() {
|
|
|
7748
8222
|
wm.files[filePath] = { scannedTo: newScannedTo };
|
|
7749
8223
|
filesScanned++;
|
|
7750
8224
|
}
|
|
8225
|
+
const uploadAs = wm.pendingResetUploadAs === "totals" ? "totals" : "deltas";
|
|
7751
8226
|
saveWatermark(wm);
|
|
7752
|
-
return {
|
|
8227
|
+
return {
|
|
8228
|
+
findings,
|
|
8229
|
+
totalToolCalls,
|
|
8230
|
+
toolCallsBySession,
|
|
8231
|
+
filesScanned,
|
|
8232
|
+
filesNew,
|
|
8233
|
+
filesSkipped,
|
|
8234
|
+
uploadAs,
|
|
8235
|
+
schemaFuture: false
|
|
8236
|
+
};
|
|
7753
8237
|
}
|
|
7754
|
-
var PROJECTS_DIR, WATERMARK_FILE, MAX_LINE_BYTES,
|
|
8238
|
+
var PROJECTS_DIR, WATERMARK_FILE, MAX_LINE_BYTES, WATERMARK_SCHEMA_VERSION, LONG_OUTPUT_THRESHOLD_BYTES2;
|
|
7755
8239
|
var init_scan_watermark = __esm({
|
|
7756
8240
|
"src/daemon/scan-watermark.ts"() {
|
|
7757
8241
|
"use strict";
|
|
@@ -7760,27 +8244,8 @@ var init_scan_watermark = __esm({
|
|
|
7760
8244
|
PROJECTS_DIR = () => path18.join(os15.homedir(), ".claude", "projects");
|
|
7761
8245
|
WATERMARK_FILE = () => path18.join(os15.homedir(), ".node9", "scan-watermark.json");
|
|
7762
8246
|
MAX_LINE_BYTES = 2 * 1024 * 1024;
|
|
7763
|
-
|
|
7764
|
-
|
|
7765
|
-
FILE_TOOLS = /* @__PURE__ */ new Set([
|
|
7766
|
-
"read",
|
|
7767
|
-
"read_file",
|
|
7768
|
-
"edit",
|
|
7769
|
-
"edit_file",
|
|
7770
|
-
"write",
|
|
7771
|
-
"write_file",
|
|
7772
|
-
"multiedit",
|
|
7773
|
-
"grep",
|
|
7774
|
-
"grep_search",
|
|
7775
|
-
"glob",
|
|
7776
|
-
"list_files"
|
|
7777
|
-
]);
|
|
7778
|
-
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;
|
|
7779
|
-
PII_EMAIL_RE = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/;
|
|
7780
|
-
PII_SSN_RE = /\b\d{3}-\d{2}-\d{4}\b/;
|
|
7781
|
-
PII_PHONE_RE = /\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]\d{3}[-.\s]\d{4}\b/;
|
|
7782
|
-
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/;
|
|
7783
|
-
PRIVILEGE_ESCALATION_RE = /\b(sudo|su)\b\s+[a-z]|\bchmod\s+(0?777|\+x)\b|\bchown\s+root\b/i;
|
|
8247
|
+
WATERMARK_SCHEMA_VERSION = 2;
|
|
8248
|
+
LONG_OUTPUT_THRESHOLD_BYTES2 = LONG_OUTPUT_THRESHOLD_BYTES;
|
|
7784
8249
|
}
|
|
7785
8250
|
});
|
|
7786
8251
|
|
|
@@ -7905,6 +8370,13 @@ async function runUploadHistory(opts) {
|
|
|
7905
8370
|
let linesParsed = 0;
|
|
7906
8371
|
let linesSkipped = 0;
|
|
7907
8372
|
const dailyEntries = [];
|
|
8373
|
+
const liveLoopCfg = getConfig().policy.loopDetection;
|
|
8374
|
+
const loopCfg = {
|
|
8375
|
+
enabled: liveLoopCfg.enabled,
|
|
8376
|
+
threshold: 3,
|
|
8377
|
+
windowSeconds: 0
|
|
8378
|
+
// "no window" — engine treats this as session-wide
|
|
8379
|
+
};
|
|
7908
8380
|
for (const { filePath, sessionId, projectDir } of iterateJsonlFiles(cutoffMs)) {
|
|
7909
8381
|
filesScanned++;
|
|
7910
8382
|
let content;
|
|
@@ -7914,6 +8386,7 @@ async function runUploadHistory(opts) {
|
|
|
7914
8386
|
continue;
|
|
7915
8387
|
}
|
|
7916
8388
|
let lineIndex = 0;
|
|
8389
|
+
const sessionCalls = [];
|
|
7917
8390
|
for (const line of content.split("\n")) {
|
|
7918
8391
|
if (!line.trim()) continue;
|
|
7919
8392
|
let obj;
|
|
@@ -7930,10 +8403,38 @@ async function runUploadHistory(opts) {
|
|
|
7930
8403
|
if (obj["type"] === "assistant" && Array.isArray(msg?.content) && msg.content.some((b) => b.type === "tool_use")) {
|
|
7931
8404
|
totalToolCalls++;
|
|
7932
8405
|
toolCallsBySession[sessionId] = (toolCallsBySession[sessionId] ?? 0) + 1;
|
|
8406
|
+
const ts = typeof obj.timestamp === "string" ? obj.timestamp : "";
|
|
8407
|
+
for (const block of msg.content) {
|
|
8408
|
+
if (!block || typeof block !== "object") continue;
|
|
8409
|
+
const b = block;
|
|
8410
|
+
if (b.type !== "tool_use") continue;
|
|
8411
|
+
sessionCalls.push({
|
|
8412
|
+
toolName: typeof b.name === "string" ? b.name : "",
|
|
8413
|
+
args: b.input ?? {},
|
|
8414
|
+
timestamp: ts,
|
|
8415
|
+
lineIndex
|
|
8416
|
+
});
|
|
8417
|
+
}
|
|
7933
8418
|
}
|
|
7934
8419
|
linesParsed++;
|
|
7935
8420
|
lineIndex++;
|
|
7936
8421
|
}
|
|
8422
|
+
if (loopCfg.enabled && sessionCalls.length > 0) {
|
|
8423
|
+
const loops = extractSessionLevelFindings(sessionCalls, {
|
|
8424
|
+
sessionId,
|
|
8425
|
+
project: decodeProjectDirName(projectDir),
|
|
8426
|
+
agent: "claude",
|
|
8427
|
+
loopDetection: {
|
|
8428
|
+
enabled: loopCfg.enabled,
|
|
8429
|
+
threshold: loopCfg.threshold,
|
|
8430
|
+
windowSeconds: loopCfg.windowSeconds
|
|
8431
|
+
}
|
|
8432
|
+
});
|
|
8433
|
+
for (const cf of loops) {
|
|
8434
|
+
const sf = toScanFinding(cf);
|
|
8435
|
+
if (sf) findings.push(sf);
|
|
8436
|
+
}
|
|
8437
|
+
}
|
|
7937
8438
|
const fallbackWorkingDir = decodeProjectDirName(projectDir);
|
|
7938
8439
|
const dailyMap = parseJSONLFile(filePath, fallbackWorkingDir);
|
|
7939
8440
|
for (const entry of dailyMap.values()) {
|
|
@@ -7958,7 +8459,8 @@ async function runUploadHistory(opts) {
|
|
|
7958
8459
|
const scanUrl = creds.apiUrl.endsWith("/policies/sync") ? creds.apiUrl.replace(/\/policies\/sync$/, "/scan/report") : `${creds.apiUrl.replace(/\/$/, "")}/scan/report`;
|
|
7959
8460
|
await postJson(scanUrl, creds.apiKey, {
|
|
7960
8461
|
...summary,
|
|
7961
|
-
sessionTotals
|
|
8462
|
+
sessionTotals,
|
|
8463
|
+
extractorVersion: CANONICAL_EXTRACTOR_VERSION
|
|
7962
8464
|
});
|
|
7963
8465
|
console.log(chalk3.green(` \u2713 Uploaded scanner findings`));
|
|
7964
8466
|
if (dailyEntries.length > 0) {
|
|
@@ -8094,7 +8596,7 @@ function fmtTs(ts) {
|
|
|
8094
8596
|
}
|
|
8095
8597
|
}
|
|
8096
8598
|
function stripTerminalEscapes(s) {
|
|
8097
|
-
return s.replace(
|
|
8599
|
+
return s.replace(TERMINAL_ESCAPE_RE2, "");
|
|
8098
8600
|
}
|
|
8099
8601
|
function preview(input, max) {
|
|
8100
8602
|
const cmd = input.command ?? input.query ?? input.file_path ?? JSON.stringify(input);
|
|
@@ -9368,7 +9870,7 @@ function renderNarrativeScorecard(input) {
|
|
|
9368
9870
|
console.log("");
|
|
9369
9871
|
}
|
|
9370
9872
|
function registerScanCommand(program2) {
|
|
9371
|
-
program2.command("scan").description("Forecast: scan agent history and show what node9 would catch if installed").option("--all", "Scan all history (default: last
|
|
9873
|
+
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(
|
|
9372
9874
|
"--upload-history",
|
|
9373
9875
|
"Upload aggregate counts from existing JSONL sessions to the SaaS dashboard. Defaults to last 3 months; override with --since. Idempotent (safe to re-run)."
|
|
9374
9876
|
).option(
|
|
@@ -9387,7 +9889,7 @@ function registerScanCommand(program2) {
|
|
|
9387
9889
|
const previewWidth = 70;
|
|
9388
9890
|
const startDate = options.all ? null : (() => {
|
|
9389
9891
|
const d = /* @__PURE__ */ new Date();
|
|
9390
|
-
d.setDate(d.getDate() - (parseInt(options.days, 10) ||
|
|
9892
|
+
d.setDate(d.getDate() - (parseInt(options.days, 10) || 90));
|
|
9391
9893
|
d.setHours(0, 0, 0, 0);
|
|
9392
9894
|
return d;
|
|
9393
9895
|
})();
|
|
@@ -9461,7 +9963,7 @@ function registerScanCommand(program2) {
|
|
|
9461
9963
|
);
|
|
9462
9964
|
return;
|
|
9463
9965
|
}
|
|
9464
|
-
const rangeLabel = options.all ? chalk4.dim("all time") : chalk4.dim(`last ${options.days ??
|
|
9966
|
+
const rangeLabel = options.all ? chalk4.dim("all time") : chalk4.dim(`last ${options.days ?? 90} days`);
|
|
9465
9967
|
const dateRange = scan.firstDate && scan.lastDate ? chalk4.dim(` ${fmtTs(scan.firstDate)} \u2013 ${fmtTs(scan.lastDate)}`) : "";
|
|
9466
9968
|
const breakdownParts = [];
|
|
9467
9969
|
if (claudeScan.sessions > 0)
|
|
@@ -9747,7 +10249,7 @@ function registerScanCommand(program2) {
|
|
|
9747
10249
|
}
|
|
9748
10250
|
);
|
|
9749
10251
|
}
|
|
9750
|
-
var CLAUDE_PRICING, GEMINI_PRICING, CODE_EXTENSIONS, SELF_OUTPUT_MARKERS, FIXTURE_TOKEN_PATTERNS,
|
|
10252
|
+
var 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;
|
|
9751
10253
|
var init_scan = __esm({
|
|
9752
10254
|
"src/cli/commands/scan.ts"() {
|
|
9753
10255
|
"use strict";
|
|
@@ -9823,7 +10325,7 @@ var init_scan = __esm({
|
|
|
9823
10325
|
// long digit sequence — fixture, not entropy
|
|
9824
10326
|
/qwerty/i
|
|
9825
10327
|
];
|
|
9826
|
-
|
|
10328
|
+
TERMINAL_ESCAPE_RE2 = /\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[@-_]|[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g;
|
|
9827
10329
|
LOOP_TOOLS = /* @__PURE__ */ new Set([
|
|
9828
10330
|
"bash",
|
|
9829
10331
|
"execute_bash",
|
|
@@ -9840,13 +10342,6 @@ var init_scan = __esm({
|
|
|
9840
10342
|
STUCK_TOOLS_LIMIT = 3;
|
|
9841
10343
|
RECURRING_SESSION_THRESHOLD = 3;
|
|
9842
10344
|
STALE_AGE_DAYS = 30;
|
|
9843
|
-
AST_FS_REGEX_RULES = /* @__PURE__ */ new Set([
|
|
9844
|
-
"block-rm-rf-home",
|
|
9845
|
-
"shield:project-jail:block-read-ssh",
|
|
9846
|
-
"shield:project-jail:block-read-aws",
|
|
9847
|
-
"shield:project-jail:block-read-env",
|
|
9848
|
-
"shield:project-jail:block-read-credentials"
|
|
9849
|
-
]);
|
|
9850
10345
|
DEFAULT_RULE_NAMES = new Set(
|
|
9851
10346
|
DEFAULT_CONFIG.policy.smartRules.map((r) => r.name).filter(Boolean)
|
|
9852
10347
|
);
|
|
@@ -10398,33 +10893,11 @@ function shouldRebind(now = Date.now()) {
|
|
|
10398
10893
|
}
|
|
10399
10894
|
function startActivitySocket() {
|
|
10400
10895
|
bindActivitySocket();
|
|
10401
|
-
if (process.platform !== "win32") {
|
|
10402
|
-
try {
|
|
10403
|
-
activitySocketWatcher = fs20.watch(os18.tmpdir(), (eventType, filename) => {
|
|
10404
|
-
if (filename !== path22.basename(ACTIVITY_SOCKET_PATH2)) return;
|
|
10405
|
-
if (eventType !== "rename") return;
|
|
10406
|
-
if (fs20.existsSync(ACTIVITY_SOCKET_PATH2)) return;
|
|
10407
|
-
attemptRebind("watch-unlink");
|
|
10408
|
-
});
|
|
10409
|
-
activitySocketWatcher.on("error", (err2) => {
|
|
10410
|
-
logActivitySocket(`watcher error: ${err2.message}`);
|
|
10411
|
-
});
|
|
10412
|
-
activitySocketWatcher.unref();
|
|
10413
|
-
} catch (err2) {
|
|
10414
|
-
logActivitySocket(`failed to start watcher: ${err2.message}`);
|
|
10415
|
-
}
|
|
10416
|
-
}
|
|
10417
10896
|
activityHealthInterval = setInterval(() => {
|
|
10418
10897
|
if (!fs20.existsSync(ACTIVITY_SOCKET_PATH2)) attemptRebind("health-probe");
|
|
10419
10898
|
}, ACTIVITY_HEALTH_PROBE_MS);
|
|
10420
10899
|
activityHealthInterval.unref();
|
|
10421
10900
|
process.on("exit", () => {
|
|
10422
|
-
if (activitySocketWatcher) {
|
|
10423
|
-
try {
|
|
10424
|
-
activitySocketWatcher.close();
|
|
10425
|
-
} catch {
|
|
10426
|
-
}
|
|
10427
|
-
}
|
|
10428
10901
|
if (activityHealthInterval) clearInterval(activityHealthInterval);
|
|
10429
10902
|
try {
|
|
10430
10903
|
fs20.unlinkSync(ACTIVITY_SOCKET_PATH2);
|
|
@@ -10559,7 +11032,7 @@ function bindActivitySocket() {
|
|
|
10559
11032
|
});
|
|
10560
11033
|
activitySocketServer = unixServer;
|
|
10561
11034
|
}
|
|
10562
|
-
var 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,
|
|
11035
|
+
var 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;
|
|
10563
11036
|
var init_state2 = __esm({
|
|
10564
11037
|
"src/daemon/state.ts"() {
|
|
10565
11038
|
"use strict";
|
|
@@ -10617,9 +11090,8 @@ var init_state2 = __esm({
|
|
|
10617
11090
|
]);
|
|
10618
11091
|
ACTIVITY_REBIND_MAX_ATTEMPTS = 5;
|
|
10619
11092
|
ACTIVITY_REBIND_WINDOW_MS = 6e4;
|
|
10620
|
-
ACTIVITY_HEALTH_PROBE_MS =
|
|
11093
|
+
ACTIVITY_HEALTH_PROBE_MS = 2e3;
|
|
10621
11094
|
activitySocketServer = null;
|
|
10622
|
-
activitySocketWatcher = null;
|
|
10623
11095
|
activityHealthInterval = null;
|
|
10624
11096
|
activityRebindAttempts = [];
|
|
10625
11097
|
activityCircuitTripped = false;
|
|
@@ -10829,16 +11301,19 @@ async function pushBlastSnapshot(creds) {
|
|
|
10829
11301
|
async function pushScanSnapshot(creds) {
|
|
10830
11302
|
try {
|
|
10831
11303
|
const tick = await tickScanWatcher();
|
|
11304
|
+
if (tick.schemaFuture) return;
|
|
10832
11305
|
if (tick.findings.length === 0 && tick.totalToolCalls === 0) {
|
|
10833
11306
|
return;
|
|
10834
11307
|
}
|
|
10835
11308
|
const summary = summarizeScan(tick.findings, {
|
|
10836
11309
|
totalToolCalls: tick.totalToolCalls
|
|
10837
11310
|
});
|
|
10838
|
-
const
|
|
11311
|
+
const perSession = buildSessionDeltas(tick.findings, tick.toolCallsBySession);
|
|
10839
11312
|
const scanUrl = creds.apiUrl.endsWith("/policies/sync") ? creds.apiUrl.replace(/\/policies\/sync$/, "/scan/report") : null;
|
|
10840
11313
|
if (!scanUrl) return;
|
|
11314
|
+
const body = tick.uploadAs === "totals" ? { ...summary, sessionTotals: perSession, extractorVersion: CANONICAL_EXTRACTOR_VERSION } : { ...summary, sessionDeltas: perSession, extractorVersion: CANONICAL_EXTRACTOR_VERSION };
|
|
10841
11315
|
const parsed = new URL(scanUrl);
|
|
11316
|
+
let posted = false;
|
|
10842
11317
|
await new Promise((resolve) => {
|
|
10843
11318
|
const req = https2.request(
|
|
10844
11319
|
{
|
|
@@ -10853,6 +11328,9 @@ async function pushScanSnapshot(creds) {
|
|
|
10853
11328
|
timeout: 1e4
|
|
10854
11329
|
},
|
|
10855
11330
|
(res) => {
|
|
11331
|
+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
|
11332
|
+
posted = true;
|
|
11333
|
+
}
|
|
10856
11334
|
res.resume();
|
|
10857
11335
|
res.on("end", resolve);
|
|
10858
11336
|
res.on("error", () => resolve());
|
|
@@ -10863,9 +11341,12 @@ async function pushScanSnapshot(creds) {
|
|
|
10863
11341
|
req.destroy();
|
|
10864
11342
|
resolve();
|
|
10865
11343
|
});
|
|
10866
|
-
req.write(JSON.stringify(
|
|
11344
|
+
req.write(JSON.stringify(body));
|
|
10867
11345
|
req.end();
|
|
10868
11346
|
});
|
|
11347
|
+
if (posted && tick.uploadAs === "totals") {
|
|
11348
|
+
markUploadComplete();
|
|
11349
|
+
}
|
|
10869
11350
|
} catch {
|
|
10870
11351
|
}
|
|
10871
11352
|
}
|