@node9/proxy 1.18.2 → 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 +1163 -152
- package/dist/cli.mjs +1163 -152
- package/dist/index.js +333 -41
- package/dist/index.mjs +333 -41
- package/package.json +3 -1
package/dist/cli.js
CHANGED
|
@@ -444,9 +444,65 @@ function redactText(text) {
|
|
|
444
444
|
}
|
|
445
445
|
return { result, found };
|
|
446
446
|
}
|
|
447
|
+
function isCatHeredocOrLit(part) {
|
|
448
|
+
if (!part) return false;
|
|
449
|
+
const t = syntax.NodeType(part);
|
|
450
|
+
if (t === "Lit") return true;
|
|
451
|
+
if (t !== "CmdSubst") return false;
|
|
452
|
+
const stmts = part.Stmts || [];
|
|
453
|
+
if (stmts.length !== 1) return false;
|
|
454
|
+
const stmt = stmts[0];
|
|
455
|
+
const redirs = stmt.Redirs || stmt.Cmd?.Redirs || [];
|
|
456
|
+
const hasHeredoc = redirs.some((r) => r && r.Hdoc);
|
|
457
|
+
if (!hasHeredoc) return false;
|
|
458
|
+
const cmd = stmt.Cmd;
|
|
459
|
+
if (!cmd || syntax.NodeType(cmd) !== "CallExpr") return false;
|
|
460
|
+
const firstArg2 = cmd.Args?.[0]?.Parts || [];
|
|
461
|
+
if (firstArg2.length !== 1 || syntax.NodeType(firstArg2[0]) !== "Lit") return false;
|
|
462
|
+
return (firstArg2[0].Value || "").toLowerCase() === "cat";
|
|
463
|
+
}
|
|
464
|
+
function parseShared(command) {
|
|
465
|
+
const cached = astCache.get(command);
|
|
466
|
+
if (cached !== void 0) {
|
|
467
|
+
astCache.delete(command);
|
|
468
|
+
astCache.set(command, cached);
|
|
469
|
+
return cached;
|
|
470
|
+
}
|
|
471
|
+
let parsed;
|
|
472
|
+
try {
|
|
473
|
+
parsed = sharedParser.Parse(command, "cmd");
|
|
474
|
+
} catch {
|
|
475
|
+
parsed = PARSE_FAIL;
|
|
476
|
+
}
|
|
477
|
+
if (astCache.size >= AST_CACHE_MAX) {
|
|
478
|
+
const oldest = astCache.keys().next().value;
|
|
479
|
+
if (oldest !== void 0) astCache.delete(oldest);
|
|
480
|
+
}
|
|
481
|
+
astCache.set(command, parsed);
|
|
482
|
+
return parsed;
|
|
483
|
+
}
|
|
484
|
+
function cachedNormalize(command, compute) {
|
|
485
|
+
const hit = normalizeCache.get(command);
|
|
486
|
+
if (hit !== void 0) {
|
|
487
|
+
normalizeCache.delete(command);
|
|
488
|
+
normalizeCache.set(command, hit);
|
|
489
|
+
return hit;
|
|
490
|
+
}
|
|
491
|
+
const result = compute();
|
|
492
|
+
if (normalizeCache.size >= NORMALIZE_CACHE_MAX) {
|
|
493
|
+
const oldest = normalizeCache.keys().next().value;
|
|
494
|
+
if (oldest !== void 0) normalizeCache.delete(oldest);
|
|
495
|
+
}
|
|
496
|
+
normalizeCache.set(command, result);
|
|
497
|
+
return result;
|
|
498
|
+
}
|
|
447
499
|
function normalizeCommandForPolicy(command) {
|
|
500
|
+
return cachedNormalize(command, () => normalizeCommandForPolicyImpl(command));
|
|
501
|
+
}
|
|
502
|
+
function normalizeCommandForPolicyImpl(command) {
|
|
503
|
+
const f = parseShared(command);
|
|
504
|
+
if (f === PARSE_FAIL) return command;
|
|
448
505
|
try {
|
|
449
|
-
const f = sharedParser.Parse(command, "cmd");
|
|
450
506
|
const strips = [];
|
|
451
507
|
syntax.Walk(f, (node) => {
|
|
452
508
|
if (!node) return false;
|
|
@@ -468,7 +524,11 @@ function normalizeCommandForPolicy(command) {
|
|
|
468
524
|
} else if (nt === "DblQuoted") {
|
|
469
525
|
const innerParts = quotedNode.Parts || [];
|
|
470
526
|
const allLit = innerParts.length === 0 || innerParts.every((p) => syntax.NodeType(p) === "Lit");
|
|
471
|
-
if (allLit)
|
|
527
|
+
if (allLit) {
|
|
528
|
+
strips.push([next.Pos().Offset(), next.End().Offset()]);
|
|
529
|
+
} else if (innerParts.every((p) => isCatHeredocOrLit(p))) {
|
|
530
|
+
strips.push([next.Pos().Offset(), next.End().Offset()]);
|
|
531
|
+
}
|
|
472
532
|
}
|
|
473
533
|
}
|
|
474
534
|
return true;
|
|
@@ -536,6 +596,130 @@ function detectDangerousShellExec(command) {
|
|
|
536
596
|
return null;
|
|
537
597
|
}
|
|
538
598
|
}
|
|
599
|
+
function isBashTool(toolName) {
|
|
600
|
+
return BASH_TOOL_NAMES.has(toolName.toLowerCase());
|
|
601
|
+
}
|
|
602
|
+
function isProtectedHomePath(rawPath) {
|
|
603
|
+
let p = rawPath.replace(/^\$HOME[\\/]?|^\$\{HOME\}[\\/]?/, "~/");
|
|
604
|
+
let underHome = false;
|
|
605
|
+
if (p === "~" || p.startsWith("~/") || p.startsWith("~\\")) {
|
|
606
|
+
p = p.replace(/^~[\\/]?/, "");
|
|
607
|
+
underHome = true;
|
|
608
|
+
} else if (/^\/home\/[^/]+/.test(p) || /^\/root(\/|$)/.test(p)) {
|
|
609
|
+
p = p.replace(/^\/home\/[^/]+[\\/]?|^\/root[\\/]?/, "");
|
|
610
|
+
underHome = true;
|
|
611
|
+
}
|
|
612
|
+
if (!underHome) return false;
|
|
613
|
+
if (p === "" || p === "." || p === "./") return true;
|
|
614
|
+
for (const safe of HOME_CACHE_ALLOWLIST) {
|
|
615
|
+
if (p === safe || p.startsWith(safe + "/") || p.startsWith(safe + "\\")) {
|
|
616
|
+
return false;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
return true;
|
|
620
|
+
}
|
|
621
|
+
function extractLiteralArgs(callExpr) {
|
|
622
|
+
const args = callExpr.Args || [];
|
|
623
|
+
if (args.length === 0) return { name: "", flags: [], paths: [] };
|
|
624
|
+
const litFromWord = (w) => {
|
|
625
|
+
const parts = w?.Parts || [];
|
|
626
|
+
let s = "";
|
|
627
|
+
for (const p of parts) {
|
|
628
|
+
const t = syntax.NodeType(p);
|
|
629
|
+
if (t === "Lit") s += (p.Value ?? "").replace(/\\(.)/g, "$1");
|
|
630
|
+
else if (t === "SglQuoted") s += p.Value ?? "";
|
|
631
|
+
else if (t === "DblQuoted") {
|
|
632
|
+
const inner = p.Parts || [];
|
|
633
|
+
if (!inner.every((ip) => syntax.NodeType(ip) === "Lit")) return null;
|
|
634
|
+
s += inner.map((ip) => ip.Value ?? "").join("");
|
|
635
|
+
} else {
|
|
636
|
+
return null;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
return s;
|
|
640
|
+
};
|
|
641
|
+
const name = (litFromWord(args[0]) || "").toLowerCase();
|
|
642
|
+
const flags = [];
|
|
643
|
+
const paths = [];
|
|
644
|
+
for (let i = 1; i < args.length; i++) {
|
|
645
|
+
const v = litFromWord(args[i]);
|
|
646
|
+
if (v === null) continue;
|
|
647
|
+
if (v.startsWith("-")) flags.push(v);
|
|
648
|
+
else paths.push(v);
|
|
649
|
+
}
|
|
650
|
+
return { name, flags, paths };
|
|
651
|
+
}
|
|
652
|
+
function analyzeFsOperation(command) {
|
|
653
|
+
if (!FS_OP_PRESCREEN_RE.test(command)) return null;
|
|
654
|
+
if (fsOpCache.has(command)) {
|
|
655
|
+
const hit = fsOpCache.get(command) ?? null;
|
|
656
|
+
fsOpCache.delete(command);
|
|
657
|
+
fsOpCache.set(command, hit);
|
|
658
|
+
return hit;
|
|
659
|
+
}
|
|
660
|
+
const computed = analyzeFsOperationImpl(command);
|
|
661
|
+
if (fsOpCache.size >= FS_OP_CACHE_MAX) {
|
|
662
|
+
const oldest = fsOpCache.keys().next().value;
|
|
663
|
+
if (oldest !== void 0) fsOpCache.delete(oldest);
|
|
664
|
+
}
|
|
665
|
+
fsOpCache.set(command, computed);
|
|
666
|
+
return computed;
|
|
667
|
+
}
|
|
668
|
+
function analyzeFsOperationImpl(command) {
|
|
669
|
+
const f = parseShared(command);
|
|
670
|
+
if (f === PARSE_FAIL) return null;
|
|
671
|
+
let result = null;
|
|
672
|
+
try {
|
|
673
|
+
syntax.Walk(f, (node) => {
|
|
674
|
+
if (!node || result) return false;
|
|
675
|
+
const n = node;
|
|
676
|
+
if (syntax.NodeType(n) !== "CallExpr") return true;
|
|
677
|
+
const { name, flags, paths } = extractLiteralArgs(n);
|
|
678
|
+
if (!name) return true;
|
|
679
|
+
if (name === "rm") {
|
|
680
|
+
const flagStr = flags.join("").toLowerCase();
|
|
681
|
+
const hasR = /[r]/.test(flagStr) || flags.includes("--recursive");
|
|
682
|
+
const hasF = /[f]/.test(flagStr) || flags.includes("--force");
|
|
683
|
+
if (hasR && hasF) {
|
|
684
|
+
for (const p of paths) {
|
|
685
|
+
if (isProtectedHomePath(p)) {
|
|
686
|
+
result = {
|
|
687
|
+
ruleName: "block-rm-rf-home",
|
|
688
|
+
verdict: "block",
|
|
689
|
+
reason: "Recursive delete of home directory is irreversible",
|
|
690
|
+
path: p
|
|
691
|
+
};
|
|
692
|
+
return false;
|
|
693
|
+
}
|
|
694
|
+
if (p === "/" || /^\/+$/.test(p)) {
|
|
695
|
+
result = {
|
|
696
|
+
ruleName: "block-rm-rf-home",
|
|
697
|
+
verdict: "block",
|
|
698
|
+
reason: "Recursive delete of root is catastrophic",
|
|
699
|
+
path: p
|
|
700
|
+
};
|
|
701
|
+
return false;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
if (FS_READ_TOOLS.has(name)) {
|
|
707
|
+
for (const p of paths) {
|
|
708
|
+
for (const sp of SENSITIVE_PATH_RULES) {
|
|
709
|
+
if (sp.match(p)) {
|
|
710
|
+
result = { ruleName: sp.rule, verdict: "block", reason: sp.reason, path: p };
|
|
711
|
+
return false;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
return true;
|
|
717
|
+
});
|
|
718
|
+
return result;
|
|
719
|
+
} catch {
|
|
720
|
+
return null;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
539
723
|
function analyzeShellCommand(command) {
|
|
540
724
|
const actions = [];
|
|
541
725
|
const paths = [];
|
|
@@ -825,10 +1009,18 @@ function getNestedValue(obj, path49) {
|
|
|
825
1009
|
function evaluateSmartConditions(args, rule) {
|
|
826
1010
|
if (!rule.conditions || rule.conditions.length === 0) return true;
|
|
827
1011
|
const mode = rule.conditionMode ?? "all";
|
|
1012
|
+
const fieldCache = /* @__PURE__ */ new Map();
|
|
1013
|
+
const resolveField = (field) => {
|
|
1014
|
+
if (fieldCache.has(field)) return fieldCache.get(field) ?? null;
|
|
1015
|
+
const rawVal = getNestedValue(args, field);
|
|
1016
|
+
const rawStr = rawVal !== null && rawVal !== void 0 ? String(rawVal) : null;
|
|
1017
|
+
const stripped = field === "command" && rawStr !== null ? normalizeCommandForPolicy(rawStr) : rawStr;
|
|
1018
|
+
const val = stripped !== null ? stripped.replace(/\s+/g, " ").trim() : null;
|
|
1019
|
+
fieldCache.set(field, val);
|
|
1020
|
+
return val;
|
|
1021
|
+
};
|
|
828
1022
|
const results = rule.conditions.map((cond) => {
|
|
829
|
-
const
|
|
830
|
-
const normalized = rawVal !== null && rawVal !== void 0 ? String(rawVal).replace(/\s+/g, " ").trim() : null;
|
|
831
|
-
const val = cond.field === "command" && normalized !== null ? normalizeCommandForPolicy(normalized) : normalized;
|
|
1023
|
+
const val = resolveField(cond.field);
|
|
832
1024
|
switch (cond.op) {
|
|
833
1025
|
case "exists":
|
|
834
1026
|
return val !== null && val !== "";
|
|
@@ -879,6 +1071,43 @@ function isSqlTool(toolName, toolInspection) {
|
|
|
879
1071
|
const fieldName = toolInspection[matchingPattern];
|
|
880
1072
|
return fieldName === "sql" || fieldName === "query";
|
|
881
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
|
+
}
|
|
882
1111
|
async function evaluatePolicy(config, toolName, args, context = {}, hooks = {}) {
|
|
883
1112
|
const { agent, cwd, activeEnvironment } = context;
|
|
884
1113
|
const { checkProvenance: checkProvenance2, isTrustedHost: isTrustedHost2 } = hooks;
|
|
@@ -894,9 +1123,27 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
894
1123
|
}
|
|
895
1124
|
}
|
|
896
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
|
+
}
|
|
897
1144
|
if (config.policy.smartRules.length > 0) {
|
|
898
1145
|
const matchedRule = config.policy.smartRules.find(
|
|
899
|
-
(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)
|
|
900
1147
|
);
|
|
901
1148
|
if (matchedRule) {
|
|
902
1149
|
if (matchedRule.verdict === "allow")
|
|
@@ -954,41 +1201,8 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
954
1201
|
tier: 3
|
|
955
1202
|
};
|
|
956
1203
|
}
|
|
957
|
-
const
|
|
958
|
-
if (
|
|
959
|
-
const sinks = pipeAnalysis.sinkTargets;
|
|
960
|
-
const allTrusted = sinks.length > 0 && sinks.every((host) => isTrustedHost2 ? isTrustedHost2(host) : false);
|
|
961
|
-
if (pipeAnalysis.risk === "critical") {
|
|
962
|
-
if (allTrusted) {
|
|
963
|
-
return {
|
|
964
|
-
decision: "review",
|
|
965
|
-
blockedByLabel: "Node9: Pipe-Chain to Trusted Host (obfuscated)",
|
|
966
|
-
reason: `Obfuscated pipe to trusted host(s): ${sinks.join(", ")} \u2014 requires approval`,
|
|
967
|
-
tier: 3
|
|
968
|
-
};
|
|
969
|
-
}
|
|
970
|
-
return {
|
|
971
|
-
decision: "block",
|
|
972
|
-
blockedByLabel: "Node9: Pipe-Chain Exfiltration (critical)",
|
|
973
|
-
reason: `Sensitive file piped through obfuscator to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
|
|
974
|
-
tier: 3
|
|
975
|
-
};
|
|
976
|
-
}
|
|
977
|
-
if (allTrusted) {
|
|
978
|
-
return {
|
|
979
|
-
decision: "allow",
|
|
980
|
-
blockedByLabel: "Node9: Pipe-Chain to Trusted Host",
|
|
981
|
-
reason: `Sensitive file piped to trusted host(s): ${sinks.join(", ")}`,
|
|
982
|
-
tier: 3
|
|
983
|
-
};
|
|
984
|
-
}
|
|
985
|
-
return {
|
|
986
|
-
decision: "review",
|
|
987
|
-
blockedByLabel: "Node9: Pipe-Chain Exfiltration (high)",
|
|
988
|
-
reason: `Sensitive file piped to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
|
|
989
|
-
tier: 3
|
|
990
|
-
};
|
|
991
|
-
}
|
|
1204
|
+
const ptVerdict = pipeChainVerdict(shellCommand, isTrustedHost2);
|
|
1205
|
+
if (ptVerdict) return ptVerdict;
|
|
992
1206
|
const firstToken = analyzed.actions[0] ?? "";
|
|
993
1207
|
if (["ssh", "scp", "rsync"].includes(firstToken)) {
|
|
994
1208
|
const rawTokens = shellCommand.trim().split(/\s+/);
|
|
@@ -1310,7 +1524,323 @@ function summarizeBlast(result, opts = {}) {
|
|
|
1310
1524
|
}))
|
|
1311
1525
|
};
|
|
1312
1526
|
}
|
|
1313
|
-
|
|
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;
|
|
1314
1844
|
var init_dist = __esm({
|
|
1315
1845
|
"packages/policy-engine/dist/index.mjs"() {
|
|
1316
1846
|
"use strict";
|
|
@@ -1784,6 +2314,83 @@ var init_dist = __esm({
|
|
|
1784
2314
|
]);
|
|
1785
2315
|
SHELL_INTERPRETERS = /* @__PURE__ */ new Set(["bash", "sh", "zsh", "fish", "dash", "ksh"]);
|
|
1786
2316
|
DOWNLOAD_CMDS = /* @__PURE__ */ new Set(["curl", "wget"]);
|
|
2317
|
+
NORMALIZE_CACHE_MAX = 5e3;
|
|
2318
|
+
normalizeCache = /* @__PURE__ */ new Map();
|
|
2319
|
+
AST_CACHE_MAX = 5e3;
|
|
2320
|
+
astCache = /* @__PURE__ */ new Map();
|
|
2321
|
+
PARSE_FAIL = /* @__PURE__ */ Symbol("parse-fail");
|
|
2322
|
+
FS_READ_TOOLS = /* @__PURE__ */ new Set([
|
|
2323
|
+
"cat",
|
|
2324
|
+
"less",
|
|
2325
|
+
"head",
|
|
2326
|
+
"tail",
|
|
2327
|
+
"bat",
|
|
2328
|
+
"more",
|
|
2329
|
+
"open",
|
|
2330
|
+
"print",
|
|
2331
|
+
"nano",
|
|
2332
|
+
"vim",
|
|
2333
|
+
"vi",
|
|
2334
|
+
"emacs",
|
|
2335
|
+
"code",
|
|
2336
|
+
"type"
|
|
2337
|
+
]);
|
|
2338
|
+
FS_OP_PRESCREEN_RE = /(?:^|[\s|;&(`\n])(?:rm|cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\b/;
|
|
2339
|
+
HOME_CACHE_ALLOWLIST = [
|
|
2340
|
+
".cache",
|
|
2341
|
+
".npm/_npx",
|
|
2342
|
+
".npm/_cacache",
|
|
2343
|
+
".cargo/registry",
|
|
2344
|
+
".gradle/caches",
|
|
2345
|
+
".gradle/.tmp",
|
|
2346
|
+
".m2/repository",
|
|
2347
|
+
".pnpm-store",
|
|
2348
|
+
".yarn/cache",
|
|
2349
|
+
".yarn/.cache",
|
|
2350
|
+
".cache/pip",
|
|
2351
|
+
".local/share/Trash",
|
|
2352
|
+
".rustup/downloads"
|
|
2353
|
+
];
|
|
2354
|
+
SENSITIVE_PATH_RULES = [
|
|
2355
|
+
{
|
|
2356
|
+
rule: "shield:project-jail:block-read-ssh",
|
|
2357
|
+
reason: "Reading SSH private keys is blocked by project-jail shield",
|
|
2358
|
+
match: (p) => /(^|[\\/])\.ssh[\\/]/i.test(p)
|
|
2359
|
+
},
|
|
2360
|
+
{
|
|
2361
|
+
rule: "shield:project-jail:block-read-aws",
|
|
2362
|
+
reason: "Reading AWS credentials is blocked by project-jail shield",
|
|
2363
|
+
match: (p) => /(^|[\\/])\.aws[\\/]/i.test(p)
|
|
2364
|
+
},
|
|
2365
|
+
{
|
|
2366
|
+
rule: "shield:project-jail:block-read-env",
|
|
2367
|
+
reason: "Reading .env files is blocked by project-jail shield",
|
|
2368
|
+
match: (p) => /(?:^|[\\/])\.env(?:\.local|\.production|\.staging)?$/i.test(p)
|
|
2369
|
+
},
|
|
2370
|
+
{
|
|
2371
|
+
rule: "shield:project-jail:block-read-credentials",
|
|
2372
|
+
reason: "Reading credential files is blocked by project-jail shield",
|
|
2373
|
+
match: (p) => /(?:credentials\.json|\.netrc|\.npmrc|\.docker[\\/]config\.json|gcloud[\\/]credentials)$/i.test(
|
|
2374
|
+
p
|
|
2375
|
+
)
|
|
2376
|
+
}
|
|
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
|
+
]);
|
|
2392
|
+
FS_OP_CACHE_MAX = 5e3;
|
|
2393
|
+
fsOpCache = /* @__PURE__ */ new Map();
|
|
1787
2394
|
SOURCE_COMMANDS = /* @__PURE__ */ new Set([
|
|
1788
2395
|
"cat",
|
|
1789
2396
|
"head",
|
|
@@ -2638,6 +3245,31 @@ var init_dist = __esm({
|
|
|
2638
3245
|
};
|
|
2639
3246
|
LOOP_THRESHOLD_FOR_WASTE = 3;
|
|
2640
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;
|
|
2641
3273
|
}
|
|
2642
3274
|
});
|
|
2643
3275
|
|
|
@@ -5559,9 +6191,7 @@ function removeNode9McpServer(servers) {
|
|
|
5559
6191
|
return true;
|
|
5560
6192
|
}
|
|
5561
6193
|
function printDaemonTip() {
|
|
5562
|
-
console.log(
|
|
5563
|
-
import_chalk.default.cyan("\n \u{1F4A1} Node9 will protect you automatically using Native OS popups.") + import_chalk.default.white("\n To view your history or manage persistent rules, run:") + import_chalk.default.green("\n node9 daemon --openui")
|
|
5564
|
-
);
|
|
6194
|
+
console.log(import_chalk.default.cyan("\n \u{1F4A1} Node9 will protect you automatically using Native OS popups."));
|
|
5565
6195
|
}
|
|
5566
6196
|
function fullPathCommand(subcommand) {
|
|
5567
6197
|
if (process.env.NODE9_TESTING === "1") return `node9 ${subcommand}`;
|
|
@@ -6651,7 +7281,8 @@ function buildScanSummary(agents) {
|
|
|
6651
7281
|
timestamp: f.timestamp,
|
|
6652
7282
|
project: f.project,
|
|
6653
7283
|
sessionId: f.sessionId,
|
|
6654
|
-
agent: f.agent
|
|
7284
|
+
agent: f.agent,
|
|
7285
|
+
kind: f.kind
|
|
6655
7286
|
}))
|
|
6656
7287
|
);
|
|
6657
7288
|
const byVerdict = {
|
|
@@ -6669,10 +7300,7 @@ function buildScanSummary(agents) {
|
|
|
6669
7300
|
costUSD: a.scan.totalCostUSD
|
|
6670
7301
|
})).filter((s) => s.sessions > 0 || s.findings > 0);
|
|
6671
7302
|
const sections = buildSections(allFindings);
|
|
6672
|
-
const wastedIters = allLoops.reduce(
|
|
6673
|
-
(sum, l) => sum + Math.max(0, l.count - LOOP_THRESHOLD_FOR_WASTE),
|
|
6674
|
-
0
|
|
6675
|
-
);
|
|
7303
|
+
const wastedIters = allLoops.filter((l) => l.kind !== "long-iteration").reduce((sum, l) => sum + Math.max(0, l.count - LOOP_THRESHOLD_FOR_WASTE), 0);
|
|
6676
7304
|
const loopWastedUSD = wastedIters * COST_PER_LOOP_ITER_USD;
|
|
6677
7305
|
return {
|
|
6678
7306
|
stats,
|
|
@@ -7288,24 +7916,76 @@ var init_costSync = __esm({
|
|
|
7288
7916
|
// src/daemon/scan-watermark.ts
|
|
7289
7917
|
var scan_watermark_exports = {};
|
|
7290
7918
|
__export(scan_watermark_exports, {
|
|
7919
|
+
WATERMARK_SCHEMA_VERSION: () => WATERMARK_SCHEMA_VERSION,
|
|
7291
7920
|
extractFindingsFromLine: () => extractFindingsFromLine,
|
|
7292
7921
|
loadWatermark: () => loadWatermark,
|
|
7922
|
+
markUploadComplete: () => markUploadComplete,
|
|
7293
7923
|
saveWatermark: () => saveWatermark,
|
|
7294
7924
|
scanDelta: () => scanDelta,
|
|
7295
7925
|
tickScanWatcher: () => tickScanWatcher
|
|
7296
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
|
+
}
|
|
7297
7935
|
function loadWatermark() {
|
|
7936
|
+
let raw;
|
|
7298
7937
|
try {
|
|
7299
|
-
|
|
7300
|
-
const parsed = JSON.parse(raw);
|
|
7301
|
-
if (typeof parsed.createdAt === "string" && parsed.files && typeof parsed.files === "object") {
|
|
7302
|
-
return parsed;
|
|
7303
|
-
}
|
|
7938
|
+
raw = import_fs16.default.readFileSync(WATERMARK_FILE(), "utf-8");
|
|
7304
7939
|
} catch {
|
|
7940
|
+
return { status: "fresh", wm: freshWatermark() };
|
|
7305
7941
|
}
|
|
7306
|
-
|
|
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 };
|
|
7307
7986
|
}
|
|
7308
7987
|
function saveWatermark(wm) {
|
|
7988
|
+
if (wm.schemaVersion > WATERMARK_SCHEMA_VERSION) return;
|
|
7309
7989
|
const target = WATERMARK_FILE();
|
|
7310
7990
|
const dir = import_path18.default.dirname(target);
|
|
7311
7991
|
if (!import_fs16.default.existsSync(dir)) import_fs16.default.mkdirSync(dir, { recursive: true });
|
|
@@ -7398,6 +8078,16 @@ function extractFindingsFromLine(line, sessionId, lineIndex) {
|
|
|
7398
8078
|
}
|
|
7399
8079
|
}
|
|
7400
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
|
+
};
|
|
7401
8091
|
const message = line.message;
|
|
7402
8092
|
if (message && typeof message === "object") {
|
|
7403
8093
|
const content = message.content;
|
|
@@ -7408,73 +8098,105 @@ function extractFindingsFromLine(line, sessionId, lineIndex) {
|
|
|
7408
8098
|
if (b.type === "tool_result") {
|
|
7409
8099
|
const c = b.content;
|
|
7410
8100
|
const len = typeof c === "string" ? c.length : Array.isArray(c) ? JSON.stringify(c).length : 0;
|
|
7411
|
-
if (len >
|
|
8101
|
+
if (len > LONG_OUTPUT_THRESHOLD_BYTES2) {
|
|
7412
8102
|
findings.push({
|
|
7413
8103
|
type: "long-output-redacted",
|
|
7414
8104
|
sessionId,
|
|
7415
8105
|
lineIndex
|
|
7416
8106
|
});
|
|
7417
8107
|
}
|
|
8108
|
+
continue;
|
|
7418
8109
|
}
|
|
7419
8110
|
if (b.type !== "tool_use") continue;
|
|
7420
|
-
const toolName = typeof b.name === "string" ? b.name
|
|
7421
|
-
const input = b.input;
|
|
7422
|
-
|
|
7423
|
-
|
|
7424
|
-
|
|
7425
|
-
|
|
7426
|
-
|
|
7427
|
-
|
|
7428
|
-
|
|
7429
|
-
|
|
7430
|
-
|
|
7431
|
-
}
|
|
7432
|
-
if (toolName !== "bash" && toolName !== "execute_bash") continue;
|
|
7433
|
-
const command = input && typeof input.command === "string" ? input.command : "";
|
|
7434
|
-
if (!command) continue;
|
|
7435
|
-
const verdict = detectDangerousShellExec(command);
|
|
7436
|
-
if (verdict) {
|
|
7437
|
-
findings.push({ type: "eval-of-remote", sessionId, lineIndex });
|
|
7438
|
-
}
|
|
7439
|
-
const pipe = analyzePipeChain(command);
|
|
7440
|
-
if (pipe.isPipeline && pipe.risk === "critical") {
|
|
7441
|
-
findings.push({ type: "pipe-to-shell", sessionId, lineIndex });
|
|
7442
|
-
}
|
|
7443
|
-
if (DESTRUCTIVE_OP_RE.test(command)) {
|
|
7444
|
-
findings.push({ type: "destructive-op", sessionId, lineIndex });
|
|
7445
|
-
}
|
|
7446
|
-
if (PRIVILEGE_ESCALATION_RE.test(command)) {
|
|
7447
|
-
findings.push({
|
|
7448
|
-
type: "privilege-escalation",
|
|
7449
|
-
sessionId,
|
|
7450
|
-
lineIndex
|
|
7451
|
-
});
|
|
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);
|
|
7452
8122
|
}
|
|
7453
8123
|
}
|
|
7454
8124
|
}
|
|
7455
8125
|
}
|
|
7456
8126
|
return findings;
|
|
7457
8127
|
}
|
|
7458
|
-
function
|
|
7459
|
-
const
|
|
7460
|
-
if (
|
|
7461
|
-
if (
|
|
7462
|
-
if (
|
|
7463
|
-
|
|
7464
|
-
|
|
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);
|
|
7465
8135
|
}
|
|
7466
8136
|
async function tickScanWatcher() {
|
|
7467
8137
|
if (process.env.NODE9_SCAN_DISABLE === "1") {
|
|
7468
|
-
return
|
|
7469
|
-
|
|
7470
|
-
|
|
7471
|
-
|
|
7472
|
-
|
|
7473
|
-
|
|
7474
|
-
|
|
7475
|
-
};
|
|
8138
|
+
return emptyTick("deltas");
|
|
8139
|
+
}
|
|
8140
|
+
const state = loadWatermark();
|
|
8141
|
+
if (state.status === "schema-future") {
|
|
8142
|
+
if (process.env.NODE9_DEBUG === "1") {
|
|
8143
|
+
process.stderr.write("[node9] watermark schema is from a newer daemon \u2014 skipping tick.\n");
|
|
8144
|
+
}
|
|
8145
|
+
return { ...emptyTick("deltas"), schemaFuture: true };
|
|
8146
|
+
}
|
|
8147
|
+
if (state.status === "extractor-stale") {
|
|
8148
|
+
if (process.env.NODE9_SKIP_WATERMARK_RESET === "1") {
|
|
8149
|
+
const acknowledged = readRawWatermarkPreservingOffsets();
|
|
8150
|
+
if (acknowledged) {
|
|
8151
|
+
saveWatermark(acknowledged);
|
|
8152
|
+
}
|
|
8153
|
+
process.stderr.write(
|
|
8154
|
+
"[node9] Extractor upgrade acknowledged via NODE9_SKIP_WATERMARK_RESET.\n Existing verdicts not refreshed \u2014 run `node9 scan --upload-history`\n to backfill them through the new pipeline.\n"
|
|
8155
|
+
);
|
|
8156
|
+
return runActualTick(loadWatermark().wm);
|
|
8157
|
+
}
|
|
8158
|
+
process.stderr.write(
|
|
8159
|
+
"[node9] Detector upgrade detected \u2014 re-scanning history through the new\n pipeline. Expect a one-time SaaS payload spike on this tick.\n Set NODE9_SKIP_WATERMARK_RESET=1 to skip.\n"
|
|
8160
|
+
);
|
|
8161
|
+
}
|
|
8162
|
+
return runActualTick(state.wm);
|
|
8163
|
+
}
|
|
8164
|
+
function emptyTick(uploadAs) {
|
|
8165
|
+
return {
|
|
8166
|
+
findings: [],
|
|
8167
|
+
totalToolCalls: 0,
|
|
8168
|
+
toolCallsBySession: {},
|
|
8169
|
+
filesScanned: 0,
|
|
8170
|
+
filesNew: 0,
|
|
8171
|
+
filesSkipped: 0,
|
|
8172
|
+
uploadAs,
|
|
8173
|
+
schemaFuture: false
|
|
8174
|
+
};
|
|
8175
|
+
}
|
|
8176
|
+
function readRawWatermarkPreservingOffsets() {
|
|
8177
|
+
let raw;
|
|
8178
|
+
try {
|
|
8179
|
+
raw = import_fs16.default.readFileSync(WATERMARK_FILE(), "utf-8");
|
|
8180
|
+
} catch {
|
|
8181
|
+
return null;
|
|
8182
|
+
}
|
|
8183
|
+
let parsed;
|
|
8184
|
+
try {
|
|
8185
|
+
parsed = JSON.parse(raw);
|
|
8186
|
+
} catch {
|
|
8187
|
+
return null;
|
|
7476
8188
|
}
|
|
7477
|
-
|
|
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) {
|
|
7478
8200
|
const watermarkCreatedAt = new Date(wm.createdAt).getTime();
|
|
7479
8201
|
const findings = [];
|
|
7480
8202
|
let totalToolCalls = 0;
|
|
@@ -7521,10 +8243,20 @@ async function tickScanWatcher() {
|
|
|
7521
8243
|
wm.files[filePath] = { scannedTo: newScannedTo };
|
|
7522
8244
|
filesScanned++;
|
|
7523
8245
|
}
|
|
8246
|
+
const uploadAs = wm.pendingResetUploadAs === "totals" ? "totals" : "deltas";
|
|
7524
8247
|
saveWatermark(wm);
|
|
7525
|
-
return {
|
|
8248
|
+
return {
|
|
8249
|
+
findings,
|
|
8250
|
+
totalToolCalls,
|
|
8251
|
+
toolCallsBySession,
|
|
8252
|
+
filesScanned,
|
|
8253
|
+
filesNew,
|
|
8254
|
+
filesSkipped,
|
|
8255
|
+
uploadAs,
|
|
8256
|
+
schemaFuture: false
|
|
8257
|
+
};
|
|
7526
8258
|
}
|
|
7527
|
-
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;
|
|
7528
8260
|
var init_scan_watermark = __esm({
|
|
7529
8261
|
"src/daemon/scan-watermark.ts"() {
|
|
7530
8262
|
"use strict";
|
|
@@ -7537,27 +8269,8 @@ var init_scan_watermark = __esm({
|
|
|
7537
8269
|
PROJECTS_DIR = () => import_path18.default.join(import_os15.default.homedir(), ".claude", "projects");
|
|
7538
8270
|
WATERMARK_FILE = () => import_path18.default.join(import_os15.default.homedir(), ".node9", "scan-watermark.json");
|
|
7539
8271
|
MAX_LINE_BYTES = 2 * 1024 * 1024;
|
|
7540
|
-
|
|
7541
|
-
|
|
7542
|
-
FILE_TOOLS = /* @__PURE__ */ new Set([
|
|
7543
|
-
"read",
|
|
7544
|
-
"read_file",
|
|
7545
|
-
"edit",
|
|
7546
|
-
"edit_file",
|
|
7547
|
-
"write",
|
|
7548
|
-
"write_file",
|
|
7549
|
-
"multiedit",
|
|
7550
|
-
"grep",
|
|
7551
|
-
"grep_search",
|
|
7552
|
-
"glob",
|
|
7553
|
-
"list_files"
|
|
7554
|
-
]);
|
|
7555
|
-
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;
|
|
7556
|
-
PII_EMAIL_RE = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/;
|
|
7557
|
-
PII_SSN_RE = /\b\d{3}-\d{2}-\d{4}\b/;
|
|
7558
|
-
PII_PHONE_RE = /\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]\d{3}[-.\s]\d{4}\b/;
|
|
7559
|
-
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/;
|
|
7560
|
-
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;
|
|
7561
8274
|
}
|
|
7562
8275
|
});
|
|
7563
8276
|
|
|
@@ -7677,6 +8390,13 @@ async function runUploadHistory(opts) {
|
|
|
7677
8390
|
let linesParsed = 0;
|
|
7678
8391
|
let linesSkipped = 0;
|
|
7679
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
|
+
};
|
|
7680
8400
|
for (const { filePath, sessionId, projectDir } of iterateJsonlFiles(cutoffMs)) {
|
|
7681
8401
|
filesScanned++;
|
|
7682
8402
|
let content;
|
|
@@ -7686,6 +8406,7 @@ async function runUploadHistory(opts) {
|
|
|
7686
8406
|
continue;
|
|
7687
8407
|
}
|
|
7688
8408
|
let lineIndex = 0;
|
|
8409
|
+
const sessionCalls = [];
|
|
7689
8410
|
for (const line of content.split("\n")) {
|
|
7690
8411
|
if (!line.trim()) continue;
|
|
7691
8412
|
let obj;
|
|
@@ -7702,10 +8423,38 @@ async function runUploadHistory(opts) {
|
|
|
7702
8423
|
if (obj["type"] === "assistant" && Array.isArray(msg?.content) && msg.content.some((b) => b.type === "tool_use")) {
|
|
7703
8424
|
totalToolCalls++;
|
|
7704
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
|
+
}
|
|
7705
8438
|
}
|
|
7706
8439
|
linesParsed++;
|
|
7707
8440
|
lineIndex++;
|
|
7708
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
|
+
}
|
|
7709
8458
|
const fallbackWorkingDir = decodeProjectDirName(projectDir);
|
|
7710
8459
|
const dailyMap = parseJSONLFile(filePath, fallbackWorkingDir);
|
|
7711
8460
|
for (const entry of dailyMap.values()) {
|
|
@@ -7730,7 +8479,8 @@ async function runUploadHistory(opts) {
|
|
|
7730
8479
|
const scanUrl = creds.apiUrl.endsWith("/policies/sync") ? creds.apiUrl.replace(/\/policies\/sync$/, "/scan/report") : `${creds.apiUrl.replace(/\/$/, "")}/scan/report`;
|
|
7731
8480
|
await postJson(scanUrl, creds.apiKey, {
|
|
7732
8481
|
...summary,
|
|
7733
|
-
sessionTotals
|
|
8482
|
+
sessionTotals,
|
|
8483
|
+
extractorVersion: CANONICAL_EXTRACTOR_VERSION
|
|
7734
8484
|
});
|
|
7735
8485
|
console.log(import_chalk3.default.green(` \u2713 Uploaded scanner findings`));
|
|
7736
8486
|
if (dailyEntries.length > 0) {
|
|
@@ -7833,6 +8583,20 @@ function geminiModelPrice(model) {
|
|
|
7833
8583
|
if (base.includes("flash")) return GEMINI_PRICING["gemini-2.0-flash"];
|
|
7834
8584
|
return null;
|
|
7835
8585
|
}
|
|
8586
|
+
function isNode9SelfOutput(text) {
|
|
8587
|
+
let hits = 0;
|
|
8588
|
+
for (const re of SELF_OUTPUT_MARKERS) {
|
|
8589
|
+
if (re.test(text)) hits++;
|
|
8590
|
+
if (hits >= 2) return true;
|
|
8591
|
+
}
|
|
8592
|
+
return false;
|
|
8593
|
+
}
|
|
8594
|
+
function looksLikeFixtureToken(sample) {
|
|
8595
|
+
for (const re of FIXTURE_TOKEN_PATTERNS) {
|
|
8596
|
+
if (re.test(sample)) return true;
|
|
8597
|
+
}
|
|
8598
|
+
return false;
|
|
8599
|
+
}
|
|
7836
8600
|
function num(n) {
|
|
7837
8601
|
return n.toLocaleString();
|
|
7838
8602
|
}
|
|
@@ -7853,7 +8617,7 @@ function fmtTs(ts) {
|
|
|
7853
8617
|
}
|
|
7854
8618
|
}
|
|
7855
8619
|
function stripTerminalEscapes(s) {
|
|
7856
|
-
return s.replace(
|
|
8620
|
+
return s.replace(TERMINAL_ESCAPE_RE2, "");
|
|
7857
8621
|
}
|
|
7858
8622
|
function preview(input, max) {
|
|
7859
8623
|
const cmd = input.command ?? input.query ?? input.file_path ?? JSON.stringify(input);
|
|
@@ -7896,6 +8660,45 @@ function buildRecurringPatternSet(findings) {
|
|
|
7896
8660
|
}
|
|
7897
8661
|
return recurring;
|
|
7898
8662
|
}
|
|
8663
|
+
function pushFsOpAstFinding(command, toolName, input, timestamp, projLabel, sessionId, agent, result) {
|
|
8664
|
+
const fsVerdict = analyzeFsOperation(command);
|
|
8665
|
+
if (!fsVerdict) return false;
|
|
8666
|
+
const synthRule = {
|
|
8667
|
+
name: fsVerdict.ruleName,
|
|
8668
|
+
tool: "bash",
|
|
8669
|
+
conditions: [],
|
|
8670
|
+
verdict: fsVerdict.verdict,
|
|
8671
|
+
reason: fsVerdict.reason
|
|
8672
|
+
};
|
|
8673
|
+
const isShieldRule = fsVerdict.ruleName.startsWith("shield:");
|
|
8674
|
+
const synthSource = isShieldRule ? {
|
|
8675
|
+
shieldName: "project-jail",
|
|
8676
|
+
shieldLabel: "project-jail (AST)",
|
|
8677
|
+
sourceType: "shield",
|
|
8678
|
+
rule: synthRule
|
|
8679
|
+
} : {
|
|
8680
|
+
shieldName: "",
|
|
8681
|
+
shieldLabel: "default (AST)",
|
|
8682
|
+
sourceType: "default",
|
|
8683
|
+
rule: synthRule
|
|
8684
|
+
};
|
|
8685
|
+
const inputPreview = preview(input, 120);
|
|
8686
|
+
const isDupe = result.findings.some(
|
|
8687
|
+
(f) => f.source.rule.name === synthRule.name && preview(f.input, 120) === inputPreview && f.project === projLabel
|
|
8688
|
+
);
|
|
8689
|
+
if (!isDupe) {
|
|
8690
|
+
result.findings.push({
|
|
8691
|
+
source: synthSource,
|
|
8692
|
+
toolName,
|
|
8693
|
+
input,
|
|
8694
|
+
timestamp,
|
|
8695
|
+
project: projLabel,
|
|
8696
|
+
sessionId,
|
|
8697
|
+
agent
|
|
8698
|
+
});
|
|
8699
|
+
}
|
|
8700
|
+
return true;
|
|
8701
|
+
}
|
|
7899
8702
|
function isStaleFinding(timestamp, now = Date.now()) {
|
|
7900
8703
|
if (!timestamp) return false;
|
|
7901
8704
|
const t = Date.parse(timestamp);
|
|
@@ -7929,15 +8732,24 @@ function detectLoops(calls, project, sessionId, agent) {
|
|
|
7929
8732
|
const entry = counts.get(key) ?? {
|
|
7930
8733
|
count: 0,
|
|
7931
8734
|
timestamp: call.timestamp,
|
|
8735
|
+
firstTs: null,
|
|
8736
|
+
lastTs: null,
|
|
7932
8737
|
input: call.input,
|
|
7933
8738
|
toolName: call.toolName
|
|
7934
8739
|
};
|
|
7935
8740
|
entry.count++;
|
|
8741
|
+
const t = call.timestamp ? Date.parse(call.timestamp) : NaN;
|
|
8742
|
+
if (!Number.isNaN(t)) {
|
|
8743
|
+
if (entry.firstTs === null || t < entry.firstTs) entry.firstTs = t;
|
|
8744
|
+
if (entry.lastTs === null || t > entry.lastTs) entry.lastTs = t;
|
|
8745
|
+
}
|
|
7936
8746
|
counts.set(key, entry);
|
|
7937
8747
|
}
|
|
7938
8748
|
const findings = [];
|
|
7939
8749
|
for (const [, entry] of counts) {
|
|
7940
8750
|
if (entry.count >= LOOP_THRESHOLD) {
|
|
8751
|
+
const span = entry.firstTs !== null && entry.lastTs !== null ? entry.lastTs - entry.firstTs : 0;
|
|
8752
|
+
const kind = span >= LOOP_TIMESPAN_THRESHOLD_MS ? "long-iteration" : "loop";
|
|
7941
8753
|
findings.push({
|
|
7942
8754
|
toolName: entry.toolName,
|
|
7943
8755
|
commandPreview: preview(entry.input, 80),
|
|
@@ -7945,7 +8757,8 @@ function detectLoops(calls, project, sessionId, agent) {
|
|
|
7945
8757
|
timestamp: entry.timestamp,
|
|
7946
8758
|
project,
|
|
7947
8759
|
sessionId,
|
|
7948
|
-
agent
|
|
8760
|
+
agent,
|
|
8761
|
+
kind
|
|
7949
8762
|
});
|
|
7950
8763
|
}
|
|
7951
8764
|
}
|
|
@@ -8164,8 +8977,10 @@ function scanClaudeHistory(startDate, onProgress, onLine) {
|
|
|
8164
8977
|
}
|
|
8165
8978
|
const resultText = typeof block.content === "string" ? block.content : Array.isArray(block.content) ? block.content.map((c) => c.text ?? "").join("\n") : null;
|
|
8166
8979
|
if (!resultText) continue;
|
|
8980
|
+
if (isNode9SelfOutput(resultText)) continue;
|
|
8167
8981
|
const dlpMatch = scanArgs({ text: resultText });
|
|
8168
8982
|
if (dlpMatch) {
|
|
8983
|
+
if (looksLikeFixtureToken(dlpMatch.redactedSample)) continue;
|
|
8169
8984
|
if (firstDlpTs === null) firstDlpTs = entry.timestamp ?? null;
|
|
8170
8985
|
const isDupe = result.dlpFindings.some(
|
|
8171
8986
|
(f) => f.patternName === dlpMatch.patternName && f.redactedSample === dlpMatch.redactedSample && f.project === projLabel
|
|
@@ -8236,11 +9051,26 @@ function scanClaudeHistory(startDate, onProgress, onLine) {
|
|
|
8236
9051
|
});
|
|
8237
9052
|
}
|
|
8238
9053
|
}
|
|
8239
|
-
let
|
|
9054
|
+
let astFsMatched = false;
|
|
9055
|
+
const astRanForBash = toolNameLower === "bash" || toolNameLower === "execute_bash";
|
|
9056
|
+
if (astRanForBash) {
|
|
9057
|
+
astFsMatched = pushFsOpAstFinding(
|
|
9058
|
+
String(input.command ?? ""),
|
|
9059
|
+
toolName,
|
|
9060
|
+
input,
|
|
9061
|
+
entry.timestamp ?? "",
|
|
9062
|
+
projLabel,
|
|
9063
|
+
sessionId,
|
|
9064
|
+
"claude",
|
|
9065
|
+
result
|
|
9066
|
+
);
|
|
9067
|
+
}
|
|
9068
|
+
let ruleMatched = astFsMatched;
|
|
8240
9069
|
for (const source of ruleSources) {
|
|
8241
9070
|
const { rule } = source;
|
|
8242
9071
|
if (rule.verdict === "allow") continue;
|
|
8243
9072
|
if (rule.tool && !matchesPattern(toolNameLower, rule.tool)) continue;
|
|
9073
|
+
if (astRanForBash && rule.name && AST_FS_REGEX_RULES.has(rule.name)) continue;
|
|
8244
9074
|
if (!evaluateSmartConditions(input, rule)) continue;
|
|
8245
9075
|
const inputPreview = preview(input, 120);
|
|
8246
9076
|
const isDupe = result.findings.some(
|
|
@@ -8436,11 +9266,26 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
|
|
|
8436
9266
|
});
|
|
8437
9267
|
}
|
|
8438
9268
|
}
|
|
8439
|
-
let
|
|
9269
|
+
let astFsMatched = false;
|
|
9270
|
+
const astRanForBash = toolNameLower === "run_shell_command" || toolNameLower === "shell";
|
|
9271
|
+
if (astRanForBash) {
|
|
9272
|
+
astFsMatched = pushFsOpAstFinding(
|
|
9273
|
+
String(input.command ?? ""),
|
|
9274
|
+
toolName,
|
|
9275
|
+
input,
|
|
9276
|
+
msg.timestamp ?? "",
|
|
9277
|
+
projLabel,
|
|
9278
|
+
sessionId,
|
|
9279
|
+
"gemini",
|
|
9280
|
+
result
|
|
9281
|
+
);
|
|
9282
|
+
}
|
|
9283
|
+
let ruleMatched = astFsMatched;
|
|
8440
9284
|
for (const source of ruleSources) {
|
|
8441
9285
|
const { rule } = source;
|
|
8442
9286
|
if (rule.verdict === "allow") continue;
|
|
8443
9287
|
if (rule.tool && !matchesPattern(toolNameLower, rule.tool)) continue;
|
|
9288
|
+
if (astRanForBash && rule.name && AST_FS_REGEX_RULES.has(rule.name)) continue;
|
|
8444
9289
|
if (!evaluateSmartConditions(input, rule)) continue;
|
|
8445
9290
|
const inputPreview = preview(input, 120);
|
|
8446
9291
|
const isDupe = result.findings.some(
|
|
@@ -8658,12 +9503,27 @@ function scanCodexHistory(startDate, onProgress, onLine) {
|
|
|
8658
9503
|
});
|
|
8659
9504
|
}
|
|
8660
9505
|
}
|
|
8661
|
-
let
|
|
9506
|
+
let astFsMatched = false;
|
|
9507
|
+
const astRanForBash = toolNameLower === "exec_command" || toolNameLower === "bash";
|
|
9508
|
+
if (astRanForBash) {
|
|
9509
|
+
astFsMatched = pushFsOpAstFinding(
|
|
9510
|
+
String(input["command"] ?? ""),
|
|
9511
|
+
toolName,
|
|
9512
|
+
input,
|
|
9513
|
+
ts,
|
|
9514
|
+
projLabel,
|
|
9515
|
+
sessionId,
|
|
9516
|
+
"codex",
|
|
9517
|
+
result
|
|
9518
|
+
);
|
|
9519
|
+
}
|
|
9520
|
+
let ruleMatched = astFsMatched;
|
|
8662
9521
|
for (const source of ruleSources) {
|
|
8663
9522
|
const { rule } = source;
|
|
8664
9523
|
if (rule.verdict === "allow") continue;
|
|
8665
9524
|
if (rule.tool && !matchesPattern(toolNameLower === "exec_command" ? "bash" : toolNameLower, rule.tool))
|
|
8666
9525
|
continue;
|
|
9526
|
+
if (astRanForBash && rule.name && AST_FS_REGEX_RULES.has(rule.name)) continue;
|
|
8667
9527
|
if (!evaluateSmartConditions(input, rule)) continue;
|
|
8668
9528
|
const inputPreview = preview(input, 120);
|
|
8669
9529
|
const isDupe = result.findings.some(
|
|
@@ -8863,15 +9723,22 @@ function renderCompactScorecard(input) {
|
|
|
8863
9723
|
import_chalk4.default.red("\u{1F6D1} ") + import_chalk4.default.red.bold(String(blockedCount).padEnd(4)) + import_chalk4.default.dim("would have blocked".padEnd(20)) + import_chalk4.default.dim(`(${topBlocked})`)
|
|
8864
9724
|
);
|
|
8865
9725
|
}
|
|
8866
|
-
|
|
8867
|
-
|
|
9726
|
+
const realLoops = scan.loopFindings.filter((l) => l.kind !== "long-iteration");
|
|
9727
|
+
const longIterations = scan.loopFindings.filter((l) => l.kind === "long-iteration");
|
|
9728
|
+
if (realLoops.length > 0) {
|
|
9729
|
+
const wastedCalls = realLoops.reduce((s, l) => s + Math.max(0, l.count - 1), 0);
|
|
8868
9730
|
const wastePct = scan.totalToolCalls > 0 ? Math.round(wastedCalls / scan.totalToolCalls * 100) : 0;
|
|
8869
9731
|
const wasteParts = [];
|
|
8870
9732
|
if (wastePct > 0) wasteParts.push(`${wastePct}% wasted`);
|
|
8871
9733
|
if (summary.loopWastedUSD > 0) wasteParts.push("~" + fmtCost(summary.loopWastedUSD));
|
|
8872
9734
|
const wasteSummary = wasteParts.length ? `(${wasteParts.join(" \xB7 ")})` : "";
|
|
8873
9735
|
console.log(
|
|
8874
|
-
import_chalk4.default.yellow("\u{1F501} ") + import_chalk4.default.yellow.bold(String(
|
|
9736
|
+
import_chalk4.default.yellow("\u{1F501} ") + import_chalk4.default.yellow.bold(String(realLoops.length).padEnd(4)) + import_chalk4.default.dim("agent loops".padEnd(20)) + import_chalk4.default.dim(wasteSummary)
|
|
9737
|
+
);
|
|
9738
|
+
}
|
|
9739
|
+
if (longIterations.length > 0) {
|
|
9740
|
+
console.log(
|
|
9741
|
+
import_chalk4.default.dim("\u{1F4C2} ") + import_chalk4.default.dim.bold(String(longIterations.length).padEnd(4)) + import_chalk4.default.dim("long iterations".padEnd(20)) + import_chalk4.default.dim("(deep work \u2014 not waste)")
|
|
8875
9742
|
);
|
|
8876
9743
|
}
|
|
8877
9744
|
if (reviewCount > 0) {
|
|
@@ -9024,7 +9891,7 @@ function renderNarrativeScorecard(input) {
|
|
|
9024
9891
|
console.log("");
|
|
9025
9892
|
}
|
|
9026
9893
|
function registerScanCommand(program2) {
|
|
9027
|
-
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(
|
|
9028
9895
|
"--upload-history",
|
|
9029
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)."
|
|
9030
9897
|
).option(
|
|
@@ -9043,7 +9910,7 @@ function registerScanCommand(program2) {
|
|
|
9043
9910
|
const previewWidth = 70;
|
|
9044
9911
|
const startDate = options.all ? null : (() => {
|
|
9045
9912
|
const d = /* @__PURE__ */ new Date();
|
|
9046
|
-
d.setDate(d.getDate() - (parseInt(options.days, 10) ||
|
|
9913
|
+
d.setDate(d.getDate() - (parseInt(options.days, 10) || 90));
|
|
9047
9914
|
d.setHours(0, 0, 0, 0);
|
|
9048
9915
|
return d;
|
|
9049
9916
|
})();
|
|
@@ -9117,7 +9984,7 @@ function registerScanCommand(program2) {
|
|
|
9117
9984
|
);
|
|
9118
9985
|
return;
|
|
9119
9986
|
}
|
|
9120
|
-
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`);
|
|
9121
9988
|
const dateRange = scan.firstDate && scan.lastDate ? import_chalk4.default.dim(` ${fmtTs(scan.firstDate)} \u2013 ${fmtTs(scan.lastDate)}`) : "";
|
|
9122
9989
|
const breakdownParts = [];
|
|
9123
9990
|
if (claudeScan.sessions > 0)
|
|
@@ -9403,7 +10270,7 @@ function registerScanCommand(program2) {
|
|
|
9403
10270
|
}
|
|
9404
10271
|
);
|
|
9405
10272
|
}
|
|
9406
|
-
var import_chalk4, import_fs18, import_path20, import_os17, CLAUDE_PRICING, GEMINI_PRICING, CODE_EXTENSIONS,
|
|
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;
|
|
9407
10274
|
var init_scan = __esm({
|
|
9408
10275
|
"src/cli/commands/scan.ts"() {
|
|
9409
10276
|
"use strict";
|
|
@@ -9414,6 +10281,7 @@ var init_scan = __esm({
|
|
|
9414
10281
|
init_shields();
|
|
9415
10282
|
init_config();
|
|
9416
10283
|
init_policy();
|
|
10284
|
+
init_dist();
|
|
9417
10285
|
init_dlp();
|
|
9418
10286
|
init_dist();
|
|
9419
10287
|
init_scan_summary();
|
|
@@ -9466,7 +10334,23 @@ var init_scan = __esm({
|
|
|
9466
10334
|
".vue",
|
|
9467
10335
|
".svelte"
|
|
9468
10336
|
]);
|
|
9469
|
-
|
|
10337
|
+
SELF_OUTPUT_MARKERS = [
|
|
10338
|
+
/redactedSample:\s*['"]/,
|
|
10339
|
+
/patternName:\s*['"]/,
|
|
10340
|
+
/\bseverity:\s*['"](?:block|review|allow)['"]/,
|
|
10341
|
+
/NODE9 SECURITY ALERT/
|
|
10342
|
+
];
|
|
10343
|
+
FIXTURE_TOKEN_PATTERNS = [
|
|
10344
|
+
/(.)\1{5,}/,
|
|
10345
|
+
// 6+ repeated characters (aaaaaa, 000000)
|
|
10346
|
+
/(?:EXAMPLE|FAKE|DUMMY|PLACEHOLDER|XXXXX)/i,
|
|
10347
|
+
/abcdefghijklmn/i,
|
|
10348
|
+
// long alpha sequence — fixture, not entropy
|
|
10349
|
+
/1234567890/,
|
|
10350
|
+
// long digit sequence — fixture, not entropy
|
|
10351
|
+
/qwerty/i
|
|
10352
|
+
];
|
|
10353
|
+
TERMINAL_ESCAPE_RE2 = /\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[@-_]|[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g;
|
|
9470
10354
|
LOOP_TOOLS = /* @__PURE__ */ new Set([
|
|
9471
10355
|
"bash",
|
|
9472
10356
|
"execute_bash",
|
|
@@ -9478,6 +10362,7 @@ var init_scan = __esm({
|
|
|
9478
10362
|
"multiedit"
|
|
9479
10363
|
]);
|
|
9480
10364
|
LOOP_THRESHOLD = 3;
|
|
10365
|
+
LOOP_TIMESPAN_THRESHOLD_MS = 10 * 60 * 1e3;
|
|
9481
10366
|
STUCK_TOOLS_MIN_WASTE = 5;
|
|
9482
10367
|
STUCK_TOOLS_LIMIT = 3;
|
|
9483
10368
|
RECURRING_SESSION_THRESHOLD = 3;
|
|
@@ -10004,7 +10889,86 @@ function abandonPending() {
|
|
|
10004
10889
|
}, 200);
|
|
10005
10890
|
}
|
|
10006
10891
|
}
|
|
10892
|
+
function logActivitySocket(msg) {
|
|
10893
|
+
try {
|
|
10894
|
+
import_fs20.default.appendFileSync(
|
|
10895
|
+
import_path22.default.join(homeDir, ".node9", "hook-debug.log"),
|
|
10896
|
+
`[${(/* @__PURE__ */ new Date()).toISOString()}] [activity-socket] ${msg}
|
|
10897
|
+
`
|
|
10898
|
+
);
|
|
10899
|
+
} catch {
|
|
10900
|
+
}
|
|
10901
|
+
}
|
|
10902
|
+
function shouldRebind(now = Date.now()) {
|
|
10903
|
+
if (activityCircuitTripped) return false;
|
|
10904
|
+
activityRebindAttempts = activityRebindAttempts.filter(
|
|
10905
|
+
(t) => now - t < ACTIVITY_REBIND_WINDOW_MS
|
|
10906
|
+
);
|
|
10907
|
+
activityRebindAttempts.push(now);
|
|
10908
|
+
if (activityRebindAttempts.length > ACTIVITY_REBIND_MAX_ATTEMPTS) {
|
|
10909
|
+
activityCircuitTripped = true;
|
|
10910
|
+
return false;
|
|
10911
|
+
}
|
|
10912
|
+
return true;
|
|
10913
|
+
}
|
|
10007
10914
|
function startActivitySocket() {
|
|
10915
|
+
bindActivitySocket();
|
|
10916
|
+
if (process.platform !== "win32") {
|
|
10917
|
+
try {
|
|
10918
|
+
activitySocketWatcher = import_fs20.default.watch(import_os18.default.tmpdir(), (eventType, filename) => {
|
|
10919
|
+
if (filename !== import_path22.default.basename(ACTIVITY_SOCKET_PATH2)) return;
|
|
10920
|
+
if (eventType !== "rename") return;
|
|
10921
|
+
if (import_fs20.default.existsSync(ACTIVITY_SOCKET_PATH2)) return;
|
|
10922
|
+
attemptRebind("watch-unlink");
|
|
10923
|
+
});
|
|
10924
|
+
activitySocketWatcher.on("error", (err2) => {
|
|
10925
|
+
logActivitySocket(`watcher error: ${err2.message}`);
|
|
10926
|
+
});
|
|
10927
|
+
activitySocketWatcher.unref();
|
|
10928
|
+
} catch (err2) {
|
|
10929
|
+
logActivitySocket(`failed to start watcher: ${err2.message}`);
|
|
10930
|
+
}
|
|
10931
|
+
}
|
|
10932
|
+
activityHealthInterval = setInterval(() => {
|
|
10933
|
+
if (!import_fs20.default.existsSync(ACTIVITY_SOCKET_PATH2)) attemptRebind("health-probe");
|
|
10934
|
+
}, ACTIVITY_HEALTH_PROBE_MS);
|
|
10935
|
+
activityHealthInterval.unref();
|
|
10936
|
+
process.on("exit", () => {
|
|
10937
|
+
if (activitySocketWatcher) {
|
|
10938
|
+
try {
|
|
10939
|
+
activitySocketWatcher.close();
|
|
10940
|
+
} catch {
|
|
10941
|
+
}
|
|
10942
|
+
}
|
|
10943
|
+
if (activityHealthInterval) clearInterval(activityHealthInterval);
|
|
10944
|
+
try {
|
|
10945
|
+
import_fs20.default.unlinkSync(ACTIVITY_SOCKET_PATH2);
|
|
10946
|
+
} catch {
|
|
10947
|
+
}
|
|
10948
|
+
});
|
|
10949
|
+
}
|
|
10950
|
+
function attemptRebind(reason) {
|
|
10951
|
+
if (!shouldRebind()) {
|
|
10952
|
+
logActivitySocket(
|
|
10953
|
+
`circuit breaker tripped after ${ACTIVITY_REBIND_MAX_ATTEMPTS} attempts in ${ACTIVITY_REBIND_WINDOW_MS}ms \u2014 flight recorder down`
|
|
10954
|
+
);
|
|
10955
|
+
broadcast("flight-recorder-down", {
|
|
10956
|
+
reason: "rebind-loop",
|
|
10957
|
+
message: "Activity socket repeatedly disappearing \u2014 run: node9 daemon restart"
|
|
10958
|
+
});
|
|
10959
|
+
return;
|
|
10960
|
+
}
|
|
10961
|
+
logActivitySocket(`rebinding (reason: ${reason}, attempt ${activityRebindAttempts.length})`);
|
|
10962
|
+
if (activitySocketServer) {
|
|
10963
|
+
try {
|
|
10964
|
+
activitySocketServer.close();
|
|
10965
|
+
} catch {
|
|
10966
|
+
}
|
|
10967
|
+
activitySocketServer = null;
|
|
10968
|
+
}
|
|
10969
|
+
bindActivitySocket();
|
|
10970
|
+
}
|
|
10971
|
+
function bindActivitySocket() {
|
|
10008
10972
|
try {
|
|
10009
10973
|
import_fs20.default.unlinkSync(ACTIVITY_SOCKET_PATH2);
|
|
10010
10974
|
} catch {
|
|
@@ -10102,15 +11066,15 @@ function startActivitySocket() {
|
|
|
10102
11066
|
socket.on("error", () => {
|
|
10103
11067
|
});
|
|
10104
11068
|
});
|
|
10105
|
-
unixServer.
|
|
10106
|
-
|
|
10107
|
-
|
|
10108
|
-
|
|
10109
|
-
|
|
10110
|
-
}
|
|
11069
|
+
unixServer.on("error", (err2) => {
|
|
11070
|
+
logActivitySocket(`server error: ${err2.message}`);
|
|
11071
|
+
});
|
|
11072
|
+
unixServer.listen(ACTIVITY_SOCKET_PATH2, () => {
|
|
11073
|
+
logActivitySocket(`bound to ${ACTIVITY_SOCKET_PATH2}`);
|
|
10111
11074
|
});
|
|
11075
|
+
activitySocketServer = unixServer;
|
|
10112
11076
|
}
|
|
10113
|
-
var import_net2, import_fs20, import_path22, import_os18, import_crypto6, homeDir, DAEMON_PID_FILE, DECISIONS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, INSIGHT_COUNTS_FILE, pending, sseClients, suggestionTracker, taintStore, insightCounts, _abandonTimer, _hadBrowserClient, _daemonServer, daemonRejectionHandlerRegistered, AUTO_DENY_MS, TRUST_DURATIONS, autoStarted, ACTIVITY_SOCKET_PATH2, ACTIVITY_RING_SIZE, activityRing, LARGE_RESPONSE_RING_SIZE, largeResponseRing, cachedScanResult, cachedScanTs, SCAN_CACHE_TTL_MS, SECRET_KEY_RE, INPUT_PRICE_PER_1M, OUTPUT_PRICE_PER_1M, BYTES_PER_TOKEN, WRITE_TOOL_NAMES;
|
|
11077
|
+
var import_net2, import_fs20, import_path22, import_os18, import_crypto6, homeDir, DAEMON_PID_FILE, DECISIONS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, INSIGHT_COUNTS_FILE, pending, sseClients, suggestionTracker, taintStore, insightCounts, _abandonTimer, _hadBrowserClient, _daemonServer, daemonRejectionHandlerRegistered, AUTO_DENY_MS, TRUST_DURATIONS, autoStarted, ACTIVITY_SOCKET_PATH2, ACTIVITY_RING_SIZE, activityRing, LARGE_RESPONSE_RING_SIZE, largeResponseRing, cachedScanResult, cachedScanTs, SCAN_CACHE_TTL_MS, SECRET_KEY_RE, INPUT_PRICE_PER_1M, OUTPUT_PRICE_PER_1M, BYTES_PER_TOKEN, WRITE_TOOL_NAMES, ACTIVITY_REBIND_MAX_ATTEMPTS, ACTIVITY_REBIND_WINDOW_MS, ACTIVITY_HEALTH_PROBE_MS, activitySocketServer, activitySocketWatcher, activityHealthInterval, activityRebindAttempts, activityCircuitTripped;
|
|
10114
11078
|
var init_state2 = __esm({
|
|
10115
11079
|
"src/daemon/state.ts"() {
|
|
10116
11080
|
"use strict";
|
|
@@ -10171,6 +11135,14 @@ var init_state2 = __esm({
|
|
|
10171
11135
|
"notebook_edit",
|
|
10172
11136
|
"notebookedit"
|
|
10173
11137
|
]);
|
|
11138
|
+
ACTIVITY_REBIND_MAX_ATTEMPTS = 5;
|
|
11139
|
+
ACTIVITY_REBIND_WINDOW_MS = 6e4;
|
|
11140
|
+
ACTIVITY_HEALTH_PROBE_MS = 3e4;
|
|
11141
|
+
activitySocketServer = null;
|
|
11142
|
+
activitySocketWatcher = null;
|
|
11143
|
+
activityHealthInterval = null;
|
|
11144
|
+
activityRebindAttempts = [];
|
|
11145
|
+
activityCircuitTripped = false;
|
|
10174
11146
|
}
|
|
10175
11147
|
});
|
|
10176
11148
|
|
|
@@ -10373,16 +11345,19 @@ async function pushBlastSnapshot(creds) {
|
|
|
10373
11345
|
async function pushScanSnapshot(creds) {
|
|
10374
11346
|
try {
|
|
10375
11347
|
const tick = await tickScanWatcher();
|
|
11348
|
+
if (tick.schemaFuture) return;
|
|
10376
11349
|
if (tick.findings.length === 0 && tick.totalToolCalls === 0) {
|
|
10377
11350
|
return;
|
|
10378
11351
|
}
|
|
10379
11352
|
const summary = summarizeScan(tick.findings, {
|
|
10380
11353
|
totalToolCalls: tick.totalToolCalls
|
|
10381
11354
|
});
|
|
10382
|
-
const
|
|
11355
|
+
const perSession = buildSessionDeltas(tick.findings, tick.toolCallsBySession);
|
|
10383
11356
|
const scanUrl = creds.apiUrl.endsWith("/policies/sync") ? creds.apiUrl.replace(/\/policies\/sync$/, "/scan/report") : null;
|
|
10384
11357
|
if (!scanUrl) return;
|
|
11358
|
+
const body = tick.uploadAs === "totals" ? { ...summary, sessionTotals: perSession, extractorVersion: CANONICAL_EXTRACTOR_VERSION } : { ...summary, sessionDeltas: perSession, extractorVersion: CANONICAL_EXTRACTOR_VERSION };
|
|
10385
11359
|
const parsed = new URL(scanUrl);
|
|
11360
|
+
let posted = false;
|
|
10386
11361
|
await new Promise((resolve) => {
|
|
10387
11362
|
const req = import_https2.default.request(
|
|
10388
11363
|
{
|
|
@@ -10397,6 +11372,9 @@ async function pushScanSnapshot(creds) {
|
|
|
10397
11372
|
timeout: 1e4
|
|
10398
11373
|
},
|
|
10399
11374
|
(res) => {
|
|
11375
|
+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
|
11376
|
+
posted = true;
|
|
11377
|
+
}
|
|
10400
11378
|
res.resume();
|
|
10401
11379
|
res.on("end", resolve);
|
|
10402
11380
|
res.on("error", () => resolve());
|
|
@@ -10407,9 +11385,12 @@ async function pushScanSnapshot(creds) {
|
|
|
10407
11385
|
req.destroy();
|
|
10408
11386
|
resolve();
|
|
10409
11387
|
});
|
|
10410
|
-
req.write(JSON.stringify(
|
|
11388
|
+
req.write(JSON.stringify(body));
|
|
10411
11389
|
req.end();
|
|
10412
11390
|
});
|
|
11391
|
+
if (posted && tick.uploadAs === "totals") {
|
|
11392
|
+
markUploadComplete();
|
|
11393
|
+
}
|
|
10413
11394
|
} catch {
|
|
10414
11395
|
}
|
|
10415
11396
|
}
|
|
@@ -12312,6 +13293,8 @@ async function startTail(options = {}) {
|
|
|
12312
13293
|
let initialReplayDone = false;
|
|
12313
13294
|
const activityPending = /* @__PURE__ */ new Map();
|
|
12314
13295
|
const orphanedResults = /* @__PURE__ */ new Map();
|
|
13296
|
+
let lastActivityFromDaemon = Date.now();
|
|
13297
|
+
let stallWarned = false;
|
|
12315
13298
|
const authToken = getInternalToken() ?? "";
|
|
12316
13299
|
const approvalQueue = [];
|
|
12317
13300
|
let cardActive = false;
|
|
@@ -12538,6 +13521,24 @@ async function startTail(options = {}) {
|
|
|
12538
13521
|
console.log(import_chalk29.default.dim("\n\u{1F6F0}\uFE0F Disconnected."));
|
|
12539
13522
|
process.exit(0);
|
|
12540
13523
|
});
|
|
13524
|
+
const STALL_THRESHOLD_MS = 6e4;
|
|
13525
|
+
const stallWatchdog = setInterval(() => {
|
|
13526
|
+
if (stallWarned) return;
|
|
13527
|
+
if (Date.now() - lastActivityFromDaemon < STALL_THRESHOLD_MS) return;
|
|
13528
|
+
try {
|
|
13529
|
+
const auditMtime = import_fs44.default.statSync(auditLog).mtimeMs;
|
|
13530
|
+
if (Date.now() - auditMtime >= STALL_THRESHOLD_MS) return;
|
|
13531
|
+
console.log("");
|
|
13532
|
+
console.log(
|
|
13533
|
+
import_chalk29.default.yellow(
|
|
13534
|
+
"\u26A0\uFE0F Tail appears stalled \u2014 hooks are firing but no events are arriving. Try: node9 daemon restart"
|
|
13535
|
+
)
|
|
13536
|
+
);
|
|
13537
|
+
stallWarned = true;
|
|
13538
|
+
} catch {
|
|
13539
|
+
}
|
|
13540
|
+
}, STALL_THRESHOLD_MS / 2);
|
|
13541
|
+
stallWatchdog.unref();
|
|
12541
13542
|
const sseUrl = `http://127.0.0.1:${port}/events?capabilities=input`;
|
|
12542
13543
|
const req = import_http2.default.get(
|
|
12543
13544
|
sseUrl,
|
|
@@ -12583,7 +13584,18 @@ async function startTail(options = {}) {
|
|
|
12583
13584
|
}
|
|
12584
13585
|
);
|
|
12585
13586
|
function handleMessage(event, rawData) {
|
|
13587
|
+
lastActivityFromDaemon = Date.now();
|
|
12586
13588
|
if (event === "csrf") return;
|
|
13589
|
+
if (event === "flight-recorder-down") {
|
|
13590
|
+
try {
|
|
13591
|
+
const parsed = JSON.parse(rawData);
|
|
13592
|
+
const msg = parsed.message ?? "Flight recorder is down \u2014 run: node9 daemon restart";
|
|
13593
|
+
console.log("");
|
|
13594
|
+
console.log(import_chalk29.default.bgRed.white.bold(` \u26A0\uFE0F ${msg} `));
|
|
13595
|
+
} catch {
|
|
13596
|
+
}
|
|
13597
|
+
return;
|
|
13598
|
+
}
|
|
12587
13599
|
if (event === "init") {
|
|
12588
13600
|
try {
|
|
12589
13601
|
const parsed = JSON.parse(rawData);
|
|
@@ -16166,7 +17178,6 @@ function registerInitCommand(program2) {
|
|
|
16166
17178
|
console.log(import_chalk15.default.green.bold(`\u{1F6E1}\uFE0F Node9 is protecting ${agentList}!`));
|
|
16167
17179
|
console.log("");
|
|
16168
17180
|
console.log(import_chalk15.default.white(" Watch live: ") + import_chalk15.default.cyan("node9 tail"));
|
|
16169
|
-
console.log(import_chalk15.default.white(" Local UI: ") + import_chalk15.default.cyan("node9 daemon --openui"));
|
|
16170
17181
|
console.log("");
|
|
16171
17182
|
console.log(import_chalk15.default.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
16172
17183
|
console.log(
|