@node9/proxy 1.18.3 → 1.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +635 -131
- package/dist/cli.mjs +635 -131
- package/dist/index.js +255 -36
- package/dist/index.mjs +255 -36
- package/package.json +3 -1
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
|
|
1147
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 >
|
|
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
|
|
7669
|
-
const input = b.input;
|
|
7670
|
-
|
|
7671
|
-
|
|
7672
|
-
|
|
7673
|
-
|
|
7674
|
-
|
|
7675
|
-
|
|
7676
|
-
|
|
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
|
|
7707
|
-
const
|
|
7708
|
-
if (
|
|
7709
|
-
if (
|
|
7710
|
-
if (
|
|
7711
|
-
|
|
7712
|
-
|
|
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
|
-
|
|
7718
|
-
|
|
7719
|
-
|
|
7720
|
-
|
|
7721
|
-
|
|
7722
|
-
|
|
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
|
+
);
|
|
7724
8161
|
}
|
|
7725
|
-
|
|
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;
|
|
8182
|
+
}
|
|
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 {
|
|
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,
|
|
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
|
-
|
|
7789
|
-
|
|
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(
|
|
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
|
|
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) ||
|
|
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 ??
|
|
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,
|
|
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
|
-
|
|
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
|
);
|
|
@@ -10850,16 +11345,19 @@ async function pushBlastSnapshot(creds) {
|
|
|
10850
11345
|
async function pushScanSnapshot(creds) {
|
|
10851
11346
|
try {
|
|
10852
11347
|
const tick = await tickScanWatcher();
|
|
11348
|
+
if (tick.schemaFuture) return;
|
|
10853
11349
|
if (tick.findings.length === 0 && tick.totalToolCalls === 0) {
|
|
10854
11350
|
return;
|
|
10855
11351
|
}
|
|
10856
11352
|
const summary = summarizeScan(tick.findings, {
|
|
10857
11353
|
totalToolCalls: tick.totalToolCalls
|
|
10858
11354
|
});
|
|
10859
|
-
const
|
|
11355
|
+
const perSession = buildSessionDeltas(tick.findings, tick.toolCallsBySession);
|
|
10860
11356
|
const scanUrl = creds.apiUrl.endsWith("/policies/sync") ? creds.apiUrl.replace(/\/policies\/sync$/, "/scan/report") : null;
|
|
10861
11357
|
if (!scanUrl) return;
|
|
11358
|
+
const body = tick.uploadAs === "totals" ? { ...summary, sessionTotals: perSession, extractorVersion: CANONICAL_EXTRACTOR_VERSION } : { ...summary, sessionDeltas: perSession, extractorVersion: CANONICAL_EXTRACTOR_VERSION };
|
|
10862
11359
|
const parsed = new URL(scanUrl);
|
|
11360
|
+
let posted = false;
|
|
10863
11361
|
await new Promise((resolve) => {
|
|
10864
11362
|
const req = import_https2.default.request(
|
|
10865
11363
|
{
|
|
@@ -10874,6 +11372,9 @@ async function pushScanSnapshot(creds) {
|
|
|
10874
11372
|
timeout: 1e4
|
|
10875
11373
|
},
|
|
10876
11374
|
(res) => {
|
|
11375
|
+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
|
11376
|
+
posted = true;
|
|
11377
|
+
}
|
|
10877
11378
|
res.resume();
|
|
10878
11379
|
res.on("end", resolve);
|
|
10879
11380
|
res.on("error", () => resolve());
|
|
@@ -10884,9 +11385,12 @@ async function pushScanSnapshot(creds) {
|
|
|
10884
11385
|
req.destroy();
|
|
10885
11386
|
resolve();
|
|
10886
11387
|
});
|
|
10887
|
-
req.write(JSON.stringify(
|
|
11388
|
+
req.write(JSON.stringify(body));
|
|
10888
11389
|
req.end();
|
|
10889
11390
|
});
|
|
11391
|
+
if (posted && tick.uploadAs === "totals") {
|
|
11392
|
+
markUploadComplete();
|
|
11393
|
+
}
|
|
10890
11394
|
} catch {
|
|
10891
11395
|
}
|
|
10892
11396
|
}
|