@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.mjs
CHANGED
|
@@ -428,9 +428,65 @@ function redactText(text) {
|
|
|
428
428
|
}
|
|
429
429
|
return { result, found };
|
|
430
430
|
}
|
|
431
|
+
function isCatHeredocOrLit(part) {
|
|
432
|
+
if (!part) return false;
|
|
433
|
+
const t = syntax.NodeType(part);
|
|
434
|
+
if (t === "Lit") return true;
|
|
435
|
+
if (t !== "CmdSubst") return false;
|
|
436
|
+
const stmts = part.Stmts || [];
|
|
437
|
+
if (stmts.length !== 1) return false;
|
|
438
|
+
const stmt = stmts[0];
|
|
439
|
+
const redirs = stmt.Redirs || stmt.Cmd?.Redirs || [];
|
|
440
|
+
const hasHeredoc = redirs.some((r) => r && r.Hdoc);
|
|
441
|
+
if (!hasHeredoc) return false;
|
|
442
|
+
const cmd = stmt.Cmd;
|
|
443
|
+
if (!cmd || syntax.NodeType(cmd) !== "CallExpr") return false;
|
|
444
|
+
const firstArg2 = cmd.Args?.[0]?.Parts || [];
|
|
445
|
+
if (firstArg2.length !== 1 || syntax.NodeType(firstArg2[0]) !== "Lit") return false;
|
|
446
|
+
return (firstArg2[0].Value || "").toLowerCase() === "cat";
|
|
447
|
+
}
|
|
448
|
+
function parseShared(command) {
|
|
449
|
+
const cached = astCache.get(command);
|
|
450
|
+
if (cached !== void 0) {
|
|
451
|
+
astCache.delete(command);
|
|
452
|
+
astCache.set(command, cached);
|
|
453
|
+
return cached;
|
|
454
|
+
}
|
|
455
|
+
let parsed;
|
|
456
|
+
try {
|
|
457
|
+
parsed = sharedParser.Parse(command, "cmd");
|
|
458
|
+
} catch {
|
|
459
|
+
parsed = PARSE_FAIL;
|
|
460
|
+
}
|
|
461
|
+
if (astCache.size >= AST_CACHE_MAX) {
|
|
462
|
+
const oldest = astCache.keys().next().value;
|
|
463
|
+
if (oldest !== void 0) astCache.delete(oldest);
|
|
464
|
+
}
|
|
465
|
+
astCache.set(command, parsed);
|
|
466
|
+
return parsed;
|
|
467
|
+
}
|
|
468
|
+
function cachedNormalize(command, compute) {
|
|
469
|
+
const hit = normalizeCache.get(command);
|
|
470
|
+
if (hit !== void 0) {
|
|
471
|
+
normalizeCache.delete(command);
|
|
472
|
+
normalizeCache.set(command, hit);
|
|
473
|
+
return hit;
|
|
474
|
+
}
|
|
475
|
+
const result = compute();
|
|
476
|
+
if (normalizeCache.size >= NORMALIZE_CACHE_MAX) {
|
|
477
|
+
const oldest = normalizeCache.keys().next().value;
|
|
478
|
+
if (oldest !== void 0) normalizeCache.delete(oldest);
|
|
479
|
+
}
|
|
480
|
+
normalizeCache.set(command, result);
|
|
481
|
+
return result;
|
|
482
|
+
}
|
|
431
483
|
function normalizeCommandForPolicy(command) {
|
|
484
|
+
return cachedNormalize(command, () => normalizeCommandForPolicyImpl(command));
|
|
485
|
+
}
|
|
486
|
+
function normalizeCommandForPolicyImpl(command) {
|
|
487
|
+
const f = parseShared(command);
|
|
488
|
+
if (f === PARSE_FAIL) return command;
|
|
432
489
|
try {
|
|
433
|
-
const f = sharedParser.Parse(command, "cmd");
|
|
434
490
|
const strips = [];
|
|
435
491
|
syntax.Walk(f, (node) => {
|
|
436
492
|
if (!node) return false;
|
|
@@ -452,7 +508,11 @@ function normalizeCommandForPolicy(command) {
|
|
|
452
508
|
} else if (nt === "DblQuoted") {
|
|
453
509
|
const innerParts = quotedNode.Parts || [];
|
|
454
510
|
const allLit = innerParts.length === 0 || innerParts.every((p) => syntax.NodeType(p) === "Lit");
|
|
455
|
-
if (allLit)
|
|
511
|
+
if (allLit) {
|
|
512
|
+
strips.push([next.Pos().Offset(), next.End().Offset()]);
|
|
513
|
+
} else if (innerParts.every((p) => isCatHeredocOrLit(p))) {
|
|
514
|
+
strips.push([next.Pos().Offset(), next.End().Offset()]);
|
|
515
|
+
}
|
|
456
516
|
}
|
|
457
517
|
}
|
|
458
518
|
return true;
|
|
@@ -520,6 +580,130 @@ function detectDangerousShellExec(command) {
|
|
|
520
580
|
return null;
|
|
521
581
|
}
|
|
522
582
|
}
|
|
583
|
+
function isBashTool(toolName) {
|
|
584
|
+
return BASH_TOOL_NAMES.has(toolName.toLowerCase());
|
|
585
|
+
}
|
|
586
|
+
function isProtectedHomePath(rawPath) {
|
|
587
|
+
let p = rawPath.replace(/^\$HOME[\\/]?|^\$\{HOME\}[\\/]?/, "~/");
|
|
588
|
+
let underHome = false;
|
|
589
|
+
if (p === "~" || p.startsWith("~/") || p.startsWith("~\\")) {
|
|
590
|
+
p = p.replace(/^~[\\/]?/, "");
|
|
591
|
+
underHome = true;
|
|
592
|
+
} else if (/^\/home\/[^/]+/.test(p) || /^\/root(\/|$)/.test(p)) {
|
|
593
|
+
p = p.replace(/^\/home\/[^/]+[\\/]?|^\/root[\\/]?/, "");
|
|
594
|
+
underHome = true;
|
|
595
|
+
}
|
|
596
|
+
if (!underHome) return false;
|
|
597
|
+
if (p === "" || p === "." || p === "./") return true;
|
|
598
|
+
for (const safe of HOME_CACHE_ALLOWLIST) {
|
|
599
|
+
if (p === safe || p.startsWith(safe + "/") || p.startsWith(safe + "\\")) {
|
|
600
|
+
return false;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return true;
|
|
604
|
+
}
|
|
605
|
+
function extractLiteralArgs(callExpr) {
|
|
606
|
+
const args = callExpr.Args || [];
|
|
607
|
+
if (args.length === 0) return { name: "", flags: [], paths: [] };
|
|
608
|
+
const litFromWord = (w) => {
|
|
609
|
+
const parts = w?.Parts || [];
|
|
610
|
+
let s = "";
|
|
611
|
+
for (const p of parts) {
|
|
612
|
+
const t = syntax.NodeType(p);
|
|
613
|
+
if (t === "Lit") s += (p.Value ?? "").replace(/\\(.)/g, "$1");
|
|
614
|
+
else if (t === "SglQuoted") s += p.Value ?? "";
|
|
615
|
+
else if (t === "DblQuoted") {
|
|
616
|
+
const inner = p.Parts || [];
|
|
617
|
+
if (!inner.every((ip) => syntax.NodeType(ip) === "Lit")) return null;
|
|
618
|
+
s += inner.map((ip) => ip.Value ?? "").join("");
|
|
619
|
+
} else {
|
|
620
|
+
return null;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
return s;
|
|
624
|
+
};
|
|
625
|
+
const name = (litFromWord(args[0]) || "").toLowerCase();
|
|
626
|
+
const flags = [];
|
|
627
|
+
const paths = [];
|
|
628
|
+
for (let i = 1; i < args.length; i++) {
|
|
629
|
+
const v = litFromWord(args[i]);
|
|
630
|
+
if (v === null) continue;
|
|
631
|
+
if (v.startsWith("-")) flags.push(v);
|
|
632
|
+
else paths.push(v);
|
|
633
|
+
}
|
|
634
|
+
return { name, flags, paths };
|
|
635
|
+
}
|
|
636
|
+
function analyzeFsOperation(command) {
|
|
637
|
+
if (!FS_OP_PRESCREEN_RE.test(command)) return null;
|
|
638
|
+
if (fsOpCache.has(command)) {
|
|
639
|
+
const hit = fsOpCache.get(command) ?? null;
|
|
640
|
+
fsOpCache.delete(command);
|
|
641
|
+
fsOpCache.set(command, hit);
|
|
642
|
+
return hit;
|
|
643
|
+
}
|
|
644
|
+
const computed = analyzeFsOperationImpl(command);
|
|
645
|
+
if (fsOpCache.size >= FS_OP_CACHE_MAX) {
|
|
646
|
+
const oldest = fsOpCache.keys().next().value;
|
|
647
|
+
if (oldest !== void 0) fsOpCache.delete(oldest);
|
|
648
|
+
}
|
|
649
|
+
fsOpCache.set(command, computed);
|
|
650
|
+
return computed;
|
|
651
|
+
}
|
|
652
|
+
function analyzeFsOperationImpl(command) {
|
|
653
|
+
const f = parseShared(command);
|
|
654
|
+
if (f === PARSE_FAIL) return null;
|
|
655
|
+
let result = null;
|
|
656
|
+
try {
|
|
657
|
+
syntax.Walk(f, (node) => {
|
|
658
|
+
if (!node || result) return false;
|
|
659
|
+
const n = node;
|
|
660
|
+
if (syntax.NodeType(n) !== "CallExpr") return true;
|
|
661
|
+
const { name, flags, paths } = extractLiteralArgs(n);
|
|
662
|
+
if (!name) return true;
|
|
663
|
+
if (name === "rm") {
|
|
664
|
+
const flagStr = flags.join("").toLowerCase();
|
|
665
|
+
const hasR = /[r]/.test(flagStr) || flags.includes("--recursive");
|
|
666
|
+
const hasF = /[f]/.test(flagStr) || flags.includes("--force");
|
|
667
|
+
if (hasR && hasF) {
|
|
668
|
+
for (const p of paths) {
|
|
669
|
+
if (isProtectedHomePath(p)) {
|
|
670
|
+
result = {
|
|
671
|
+
ruleName: "block-rm-rf-home",
|
|
672
|
+
verdict: "block",
|
|
673
|
+
reason: "Recursive delete of home directory is irreversible",
|
|
674
|
+
path: p
|
|
675
|
+
};
|
|
676
|
+
return false;
|
|
677
|
+
}
|
|
678
|
+
if (p === "/" || /^\/+$/.test(p)) {
|
|
679
|
+
result = {
|
|
680
|
+
ruleName: "block-rm-rf-home",
|
|
681
|
+
verdict: "block",
|
|
682
|
+
reason: "Recursive delete of root is catastrophic",
|
|
683
|
+
path: p
|
|
684
|
+
};
|
|
685
|
+
return false;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
if (FS_READ_TOOLS.has(name)) {
|
|
691
|
+
for (const p of paths) {
|
|
692
|
+
for (const sp of SENSITIVE_PATH_RULES) {
|
|
693
|
+
if (sp.match(p)) {
|
|
694
|
+
result = { ruleName: sp.rule, verdict: "block", reason: sp.reason, path: p };
|
|
695
|
+
return false;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
return true;
|
|
701
|
+
});
|
|
702
|
+
return result;
|
|
703
|
+
} catch {
|
|
704
|
+
return null;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
523
707
|
function analyzeShellCommand(command) {
|
|
524
708
|
const actions = [];
|
|
525
709
|
const paths = [];
|
|
@@ -809,10 +993,18 @@ function getNestedValue(obj, path49) {
|
|
|
809
993
|
function evaluateSmartConditions(args, rule) {
|
|
810
994
|
if (!rule.conditions || rule.conditions.length === 0) return true;
|
|
811
995
|
const mode = rule.conditionMode ?? "all";
|
|
996
|
+
const fieldCache = /* @__PURE__ */ new Map();
|
|
997
|
+
const resolveField = (field) => {
|
|
998
|
+
if (fieldCache.has(field)) return fieldCache.get(field) ?? null;
|
|
999
|
+
const rawVal = getNestedValue(args, field);
|
|
1000
|
+
const rawStr = rawVal !== null && rawVal !== void 0 ? String(rawVal) : null;
|
|
1001
|
+
const stripped = field === "command" && rawStr !== null ? normalizeCommandForPolicy(rawStr) : rawStr;
|
|
1002
|
+
const val = stripped !== null ? stripped.replace(/\s+/g, " ").trim() : null;
|
|
1003
|
+
fieldCache.set(field, val);
|
|
1004
|
+
return val;
|
|
1005
|
+
};
|
|
812
1006
|
const results = rule.conditions.map((cond) => {
|
|
813
|
-
const
|
|
814
|
-
const normalized = rawVal !== null && rawVal !== void 0 ? String(rawVal).replace(/\s+/g, " ").trim() : null;
|
|
815
|
-
const val = cond.field === "command" && normalized !== null ? normalizeCommandForPolicy(normalized) : normalized;
|
|
1007
|
+
const val = resolveField(cond.field);
|
|
816
1008
|
switch (cond.op) {
|
|
817
1009
|
case "exists":
|
|
818
1010
|
return val !== null && val !== "";
|
|
@@ -863,6 +1055,43 @@ function isSqlTool(toolName, toolInspection) {
|
|
|
863
1055
|
const fieldName = toolInspection[matchingPattern];
|
|
864
1056
|
return fieldName === "sql" || fieldName === "query";
|
|
865
1057
|
}
|
|
1058
|
+
function pipeChainVerdict(command, isTrustedHost2) {
|
|
1059
|
+
const pipeAnalysis = analyzePipeChain(command);
|
|
1060
|
+
if (!pipeAnalysis.isPipeline) return null;
|
|
1061
|
+
if (pipeAnalysis.risk !== "critical" && pipeAnalysis.risk !== "high") return null;
|
|
1062
|
+
const sinks = pipeAnalysis.sinkTargets;
|
|
1063
|
+
const allTrusted = sinks.length > 0 && sinks.every((host) => isTrustedHost2 ? isTrustedHost2(host) : false);
|
|
1064
|
+
if (pipeAnalysis.risk === "critical") {
|
|
1065
|
+
if (allTrusted) {
|
|
1066
|
+
return {
|
|
1067
|
+
decision: "review",
|
|
1068
|
+
blockedByLabel: "Node9: Pipe-Chain to Trusted Host (obfuscated)",
|
|
1069
|
+
reason: `Obfuscated pipe to trusted host(s): ${sinks.join(", ")} \u2014 requires approval`,
|
|
1070
|
+
tier: 3
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
return {
|
|
1074
|
+
decision: "block",
|
|
1075
|
+
blockedByLabel: "Node9: Pipe-Chain Exfiltration (critical)",
|
|
1076
|
+
reason: `Sensitive file piped through obfuscator to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
|
|
1077
|
+
tier: 3
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
if (allTrusted) {
|
|
1081
|
+
return {
|
|
1082
|
+
decision: "allow",
|
|
1083
|
+
blockedByLabel: "Node9: Pipe-Chain to Trusted Host",
|
|
1084
|
+
reason: `Sensitive file piped to trusted host(s): ${sinks.join(", ")}`,
|
|
1085
|
+
tier: 3
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
return {
|
|
1089
|
+
decision: "review",
|
|
1090
|
+
blockedByLabel: "Node9: Pipe-Chain Exfiltration (high)",
|
|
1091
|
+
reason: `Sensitive file piped to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
|
|
1092
|
+
tier: 3
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
866
1095
|
async function evaluatePolicy(config, toolName, args, context = {}, hooks = {}) {
|
|
867
1096
|
const { agent, cwd, activeEnvironment } = context;
|
|
868
1097
|
const { checkProvenance: checkProvenance2, isTrustedHost: isTrustedHost2 } = hooks;
|
|
@@ -878,9 +1107,27 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
878
1107
|
}
|
|
879
1108
|
}
|
|
880
1109
|
if (wouldBeIgnored) return { decision: "allow" };
|
|
1110
|
+
const bashCommand = agent !== "Terminal" && isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : null;
|
|
1111
|
+
if (bashCommand !== null) {
|
|
1112
|
+
const pipeVerdict = pipeChainVerdict(bashCommand, isTrustedHost2);
|
|
1113
|
+
if (pipeVerdict) return pipeVerdict;
|
|
1114
|
+
const fsVerdict = analyzeFsOperation(bashCommand);
|
|
1115
|
+
if (fsVerdict) {
|
|
1116
|
+
const isShieldRule = fsVerdict.ruleName.startsWith("shield:");
|
|
1117
|
+
const labelPrefix = isShieldRule ? "project-jail (AST)" : "Node9 (AST)";
|
|
1118
|
+
return {
|
|
1119
|
+
decision: fsVerdict.verdict,
|
|
1120
|
+
blockedByLabel: `${labelPrefix}: ${fsVerdict.ruleName}`,
|
|
1121
|
+
reason: fsVerdict.reason,
|
|
1122
|
+
tier: 2,
|
|
1123
|
+
ruleName: fsVerdict.ruleName,
|
|
1124
|
+
ruleDescription: fsVerdict.reason
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
881
1128
|
if (config.policy.smartRules.length > 0) {
|
|
882
1129
|
const matchedRule = config.policy.smartRules.find(
|
|
883
|
-
(rule) => matchesPattern(toolName, rule.tool) && evaluateSmartConditions(args, rule)
|
|
1130
|
+
(rule) => matchesPattern(toolName, rule.tool) && !(bashCommand !== null && rule.name && AST_FS_REGEX_RULES.has(rule.name)) && evaluateSmartConditions(args, rule)
|
|
884
1131
|
);
|
|
885
1132
|
if (matchedRule) {
|
|
886
1133
|
if (matchedRule.verdict === "allow")
|
|
@@ -938,41 +1185,8 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
938
1185
|
tier: 3
|
|
939
1186
|
};
|
|
940
1187
|
}
|
|
941
|
-
const
|
|
942
|
-
if (
|
|
943
|
-
const sinks = pipeAnalysis.sinkTargets;
|
|
944
|
-
const allTrusted = sinks.length > 0 && sinks.every((host) => isTrustedHost2 ? isTrustedHost2(host) : false);
|
|
945
|
-
if (pipeAnalysis.risk === "critical") {
|
|
946
|
-
if (allTrusted) {
|
|
947
|
-
return {
|
|
948
|
-
decision: "review",
|
|
949
|
-
blockedByLabel: "Node9: Pipe-Chain to Trusted Host (obfuscated)",
|
|
950
|
-
reason: `Obfuscated pipe to trusted host(s): ${sinks.join(", ")} \u2014 requires approval`,
|
|
951
|
-
tier: 3
|
|
952
|
-
};
|
|
953
|
-
}
|
|
954
|
-
return {
|
|
955
|
-
decision: "block",
|
|
956
|
-
blockedByLabel: "Node9: Pipe-Chain Exfiltration (critical)",
|
|
957
|
-
reason: `Sensitive file piped through obfuscator to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
|
|
958
|
-
tier: 3
|
|
959
|
-
};
|
|
960
|
-
}
|
|
961
|
-
if (allTrusted) {
|
|
962
|
-
return {
|
|
963
|
-
decision: "allow",
|
|
964
|
-
blockedByLabel: "Node9: Pipe-Chain to Trusted Host",
|
|
965
|
-
reason: `Sensitive file piped to trusted host(s): ${sinks.join(", ")}`,
|
|
966
|
-
tier: 3
|
|
967
|
-
};
|
|
968
|
-
}
|
|
969
|
-
return {
|
|
970
|
-
decision: "review",
|
|
971
|
-
blockedByLabel: "Node9: Pipe-Chain Exfiltration (high)",
|
|
972
|
-
reason: `Sensitive file piped to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
|
|
973
|
-
tier: 3
|
|
974
|
-
};
|
|
975
|
-
}
|
|
1188
|
+
const ptVerdict = pipeChainVerdict(shellCommand, isTrustedHost2);
|
|
1189
|
+
if (ptVerdict) return ptVerdict;
|
|
976
1190
|
const firstToken = analyzed.actions[0] ?? "";
|
|
977
1191
|
if (["ssh", "scp", "rsync"].includes(firstToken)) {
|
|
978
1192
|
const rawTokens = shellCommand.trim().split(/\s+/);
|
|
@@ -1294,7 +1508,323 @@ function summarizeBlast(result, opts = {}) {
|
|
|
1294
1508
|
}))
|
|
1295
1509
|
};
|
|
1296
1510
|
}
|
|
1297
|
-
|
|
1511
|
+
function detectPii(text) {
|
|
1512
|
+
const found = /* @__PURE__ */ new Set();
|
|
1513
|
+
if (/@/.test(text) && PII_EMAIL_RE.test(text)) found.add("Email");
|
|
1514
|
+
if (/-/.test(text) && PII_SSN_RE.test(text)) found.add("SSN");
|
|
1515
|
+
if (PII_PHONE_RE.test(text)) found.add("Phone");
|
|
1516
|
+
if (PII_CC_RE.test(text)) found.add("Credit Card");
|
|
1517
|
+
return [...found];
|
|
1518
|
+
}
|
|
1519
|
+
function extractCanonicalFindings(call, ctx) {
|
|
1520
|
+
const out = [];
|
|
1521
|
+
const ts = call.timestamp;
|
|
1522
|
+
const toolNameLower = call.toolName.toLowerCase();
|
|
1523
|
+
const command = typeof call.args.command === "string" ? call.args.command : null;
|
|
1524
|
+
const isBash = isBashTool(call.toolName) && command !== null;
|
|
1525
|
+
if (call.outputBytes !== void 0 && call.outputBytes > LONG_OUTPUT_THRESHOLD_BYTES) {
|
|
1526
|
+
out.push(
|
|
1527
|
+
makeFinding({
|
|
1528
|
+
type: "long-output-redacted",
|
|
1529
|
+
ruleName: "long-output-redacted",
|
|
1530
|
+
verdict: "review",
|
|
1531
|
+
severity: "medium",
|
|
1532
|
+
reason: `Tool output exceeded ${LONG_OUTPUT_THRESHOLD_BYTES} bytes and was redacted`,
|
|
1533
|
+
toolName: call.toolName,
|
|
1534
|
+
ctx,
|
|
1535
|
+
ts,
|
|
1536
|
+
sourceType: "engine"
|
|
1537
|
+
})
|
|
1538
|
+
);
|
|
1539
|
+
}
|
|
1540
|
+
if (ctx.dlpEnabled) {
|
|
1541
|
+
const dlp = scanArgs(call.args);
|
|
1542
|
+
if (dlp) {
|
|
1543
|
+
out.push(
|
|
1544
|
+
makeFinding({
|
|
1545
|
+
type: "dlp",
|
|
1546
|
+
ruleName: `dlp:${dlp.patternName}`,
|
|
1547
|
+
patternName: dlp.patternName,
|
|
1548
|
+
verdict: dlp.severity === "block" ? "block" : "review",
|
|
1549
|
+
severity: dlp.severity === "block" ? "critical" : "medium",
|
|
1550
|
+
reason: `${dlp.patternName} detected in ${dlp.fieldPath}`,
|
|
1551
|
+
toolName: call.toolName,
|
|
1552
|
+
ctx,
|
|
1553
|
+
ts,
|
|
1554
|
+
sourceType: "engine",
|
|
1555
|
+
input: call.args,
|
|
1556
|
+
redactedSample: dlp.redactedSample
|
|
1557
|
+
})
|
|
1558
|
+
);
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
for (const value of stringValues(call.args)) {
|
|
1562
|
+
const piiHits = detectPii(value);
|
|
1563
|
+
for (const pattern of piiHits) {
|
|
1564
|
+
out.push(
|
|
1565
|
+
makeFinding({
|
|
1566
|
+
type: "pii",
|
|
1567
|
+
ruleName: `pii:${pattern.toLowerCase().replace(/\s+/g, "-")}`,
|
|
1568
|
+
patternName: pattern,
|
|
1569
|
+
verdict: "review",
|
|
1570
|
+
severity: "medium",
|
|
1571
|
+
reason: `${pattern} pattern detected in tool input`,
|
|
1572
|
+
toolName: call.toolName,
|
|
1573
|
+
ctx,
|
|
1574
|
+
ts,
|
|
1575
|
+
sourceType: "engine"
|
|
1576
|
+
})
|
|
1577
|
+
);
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
if (FILE_TOOLS.has(toolNameLower)) {
|
|
1581
|
+
const filePath = typeof call.args.file_path === "string" && call.args.file_path || typeof call.args.path === "string" && call.args.path || typeof call.args.pattern === "string" && call.args.pattern || "";
|
|
1582
|
+
if (filePath && SENSITIVE_PATH_RE.test(filePath)) {
|
|
1583
|
+
out.push(
|
|
1584
|
+
makeFinding({
|
|
1585
|
+
type: "sensitive-file-read",
|
|
1586
|
+
ruleName: "sensitive-file-read",
|
|
1587
|
+
verdict: "review",
|
|
1588
|
+
severity: "critical",
|
|
1589
|
+
reason: `Sensitive file path read via ${call.toolName}`,
|
|
1590
|
+
toolName: call.toolName,
|
|
1591
|
+
ctx,
|
|
1592
|
+
ts,
|
|
1593
|
+
sourceType: "engine",
|
|
1594
|
+
subjectPath: filePath
|
|
1595
|
+
})
|
|
1596
|
+
);
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
if (!isBash || command === null) {
|
|
1600
|
+
return out;
|
|
1601
|
+
}
|
|
1602
|
+
const fsVerdict = analyzeFsOperation(command);
|
|
1603
|
+
if (fsVerdict) {
|
|
1604
|
+
const isShield = fsVerdict.ruleName.startsWith("shield:");
|
|
1605
|
+
out.push(
|
|
1606
|
+
makeFinding({
|
|
1607
|
+
type: "ast-fs-op",
|
|
1608
|
+
ruleName: fsVerdict.ruleName,
|
|
1609
|
+
verdict: fsVerdict.verdict,
|
|
1610
|
+
severity: classifyRuleSeverity(fsVerdict.ruleName, fsVerdict.verdict),
|
|
1611
|
+
reason: fsVerdict.reason,
|
|
1612
|
+
toolName: call.toolName,
|
|
1613
|
+
ctx,
|
|
1614
|
+
ts,
|
|
1615
|
+
sourceType: isShield ? "shield" : "engine",
|
|
1616
|
+
shieldLabel: isShield ? "project-jail (AST)" : "Node9 (AST)",
|
|
1617
|
+
subjectPath: fsVerdict.path,
|
|
1618
|
+
input: call.args
|
|
1619
|
+
})
|
|
1620
|
+
);
|
|
1621
|
+
}
|
|
1622
|
+
for (const source of ctx.rules) {
|
|
1623
|
+
const r = source.rule;
|
|
1624
|
+
if (r.verdict === "allow") continue;
|
|
1625
|
+
if (r.tool && !matchesPattern(toolNameLower, r.tool)) continue;
|
|
1626
|
+
if (r.name && AST_FS_REGEX_RULES.has(r.name)) continue;
|
|
1627
|
+
if (!evaluateSmartConditions(call.args, r)) continue;
|
|
1628
|
+
out.push(
|
|
1629
|
+
makeFinding({
|
|
1630
|
+
type: "smart-rule",
|
|
1631
|
+
ruleName: r.name ?? r.tool,
|
|
1632
|
+
verdict: r.verdict === "block" ? "block" : "review",
|
|
1633
|
+
severity: classifyRuleSeverity(r.name ?? r.tool, r.verdict),
|
|
1634
|
+
reason: r.reason ?? `Smart rule ${r.name ?? r.tool} fired`,
|
|
1635
|
+
toolName: call.toolName,
|
|
1636
|
+
ctx,
|
|
1637
|
+
ts,
|
|
1638
|
+
sourceType: source.sourceType,
|
|
1639
|
+
shieldLabel: source.shieldLabel,
|
|
1640
|
+
input: call.args
|
|
1641
|
+
})
|
|
1642
|
+
);
|
|
1643
|
+
break;
|
|
1644
|
+
}
|
|
1645
|
+
const evalVerdict = detectDangerousShellExec(command);
|
|
1646
|
+
if (evalVerdict) {
|
|
1647
|
+
out.push(
|
|
1648
|
+
makeFinding({
|
|
1649
|
+
type: "eval-of-remote",
|
|
1650
|
+
ruleName: "eval-of-remote",
|
|
1651
|
+
verdict: evalVerdict,
|
|
1652
|
+
severity: classifyRuleSeverity("eval-remote", evalVerdict),
|
|
1653
|
+
reason: evalVerdict === "block" ? "Eval of remote download is a near-certain supply-chain attack" : "Eval of dynamic content (variable / subshell) requires approval",
|
|
1654
|
+
toolName: call.toolName,
|
|
1655
|
+
ctx,
|
|
1656
|
+
ts,
|
|
1657
|
+
sourceType: "engine",
|
|
1658
|
+
input: call.args
|
|
1659
|
+
})
|
|
1660
|
+
);
|
|
1661
|
+
}
|
|
1662
|
+
const pipe = analyzePipeChain(command);
|
|
1663
|
+
if (pipe.isPipeline && pipe.risk === "critical") {
|
|
1664
|
+
out.push(
|
|
1665
|
+
makeFinding({
|
|
1666
|
+
type: "pipe-to-shell",
|
|
1667
|
+
ruleName: "pipe-to-shell",
|
|
1668
|
+
verdict: "block",
|
|
1669
|
+
severity: "critical",
|
|
1670
|
+
reason: `Sensitive file piped through obfuscator to network sink: ${pipe.sourceFiles.join(", ")} \u2192 ${pipe.sinkTargets.join(", ")}`,
|
|
1671
|
+
toolName: call.toolName,
|
|
1672
|
+
ctx,
|
|
1673
|
+
ts,
|
|
1674
|
+
sourceType: "engine",
|
|
1675
|
+
input: call.args
|
|
1676
|
+
})
|
|
1677
|
+
);
|
|
1678
|
+
}
|
|
1679
|
+
if (DESTRUCTIVE_OP_RE.test(command)) {
|
|
1680
|
+
out.push(
|
|
1681
|
+
makeFinding({
|
|
1682
|
+
type: "destructive-op",
|
|
1683
|
+
ruleName: "destructive-op",
|
|
1684
|
+
verdict: "review",
|
|
1685
|
+
severity: "high",
|
|
1686
|
+
reason: "Destructive operation pattern detected",
|
|
1687
|
+
toolName: call.toolName,
|
|
1688
|
+
ctx,
|
|
1689
|
+
ts,
|
|
1690
|
+
sourceType: "engine",
|
|
1691
|
+
input: call.args
|
|
1692
|
+
})
|
|
1693
|
+
);
|
|
1694
|
+
}
|
|
1695
|
+
if (PRIVILEGE_ESCALATION_RE.test(command)) {
|
|
1696
|
+
out.push(
|
|
1697
|
+
makeFinding({
|
|
1698
|
+
type: "privilege-escalation",
|
|
1699
|
+
ruleName: "privilege-escalation",
|
|
1700
|
+
verdict: "review",
|
|
1701
|
+
severity: "high",
|
|
1702
|
+
reason: "Privilege-escalation pattern detected",
|
|
1703
|
+
toolName: call.toolName,
|
|
1704
|
+
ctx,
|
|
1705
|
+
ts,
|
|
1706
|
+
sourceType: "engine",
|
|
1707
|
+
input: call.args
|
|
1708
|
+
})
|
|
1709
|
+
);
|
|
1710
|
+
}
|
|
1711
|
+
return out;
|
|
1712
|
+
}
|
|
1713
|
+
function extractSessionLevelFindings(calls, ctx) {
|
|
1714
|
+
if (!ctx.loopDetection.enabled || calls.length === 0) return [];
|
|
1715
|
+
const out = [];
|
|
1716
|
+
const seenLoopKeys = /* @__PURE__ */ new Set();
|
|
1717
|
+
const ONE_YEAR_MS = 365 * 24 * 60 * 60 * 1e3;
|
|
1718
|
+
const windowMs = ctx.loopDetection.windowSeconds <= 0 ? ONE_YEAR_MS : ctx.loopDetection.windowSeconds * 1e3;
|
|
1719
|
+
let records = [];
|
|
1720
|
+
let syntheticTs = 0;
|
|
1721
|
+
for (let i = 0; i < calls.length; i++) {
|
|
1722
|
+
const call = calls[i];
|
|
1723
|
+
const parsed = new Date(call.timestamp).getTime();
|
|
1724
|
+
const now = Number.isFinite(parsed) ? parsed : ++syntheticTs;
|
|
1725
|
+
const verdict = evaluateLoopWindow(
|
|
1726
|
+
records,
|
|
1727
|
+
call.toolName,
|
|
1728
|
+
call.args,
|
|
1729
|
+
ctx.loopDetection.threshold,
|
|
1730
|
+
windowMs,
|
|
1731
|
+
now
|
|
1732
|
+
);
|
|
1733
|
+
records = verdict.nextRecords;
|
|
1734
|
+
if (!verdict.looping) continue;
|
|
1735
|
+
const last = records[records.length - 1];
|
|
1736
|
+
const key = `${last.t}|${last.h}`;
|
|
1737
|
+
if (seenLoopKeys.has(key)) continue;
|
|
1738
|
+
seenLoopKeys.add(key);
|
|
1739
|
+
out.push({
|
|
1740
|
+
type: "loop",
|
|
1741
|
+
ruleName: "loop",
|
|
1742
|
+
verdict: "review",
|
|
1743
|
+
severity: "medium",
|
|
1744
|
+
reason: `Tool called ${verdict.count} times with identical args within window`,
|
|
1745
|
+
toolName: call.toolName,
|
|
1746
|
+
agent: ctx.agent,
|
|
1747
|
+
sessionId: ctx.sessionId,
|
|
1748
|
+
project: ctx.project,
|
|
1749
|
+
lineIndex: call.lineIndex,
|
|
1750
|
+
sourceType: "engine",
|
|
1751
|
+
firstSeenAt: call.timestamp,
|
|
1752
|
+
lastSeenAt: call.timestamp,
|
|
1753
|
+
occurrenceCount: 1,
|
|
1754
|
+
loopCount: verdict.count,
|
|
1755
|
+
loopKind: "loop",
|
|
1756
|
+
commandPreview: previewArgs(call.args, DEDUPE_PREVIEW_LEN),
|
|
1757
|
+
costUsd: verdict.count * COST_PER_LOOP_ITER_USD
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
return out;
|
|
1761
|
+
}
|
|
1762
|
+
function toScanFinding(c) {
|
|
1763
|
+
const typeMap = {
|
|
1764
|
+
"smart-rule": null,
|
|
1765
|
+
"ast-fs-op": null,
|
|
1766
|
+
dlp: "dlp",
|
|
1767
|
+
pii: "pii",
|
|
1768
|
+
"sensitive-file-read": "sensitive-file-read",
|
|
1769
|
+
"privilege-escalation": "privilege-escalation",
|
|
1770
|
+
"destructive-op": "destructive-op",
|
|
1771
|
+
"pipe-to-shell": "pipe-to-shell",
|
|
1772
|
+
"eval-of-remote": "eval-of-remote",
|
|
1773
|
+
loop: "loop",
|
|
1774
|
+
"long-output-redacted": "long-output-redacted"
|
|
1775
|
+
};
|
|
1776
|
+
const sfType = typeMap[c.type];
|
|
1777
|
+
if (sfType === null) return null;
|
|
1778
|
+
return {
|
|
1779
|
+
sessionId: c.sessionId,
|
|
1780
|
+
type: sfType,
|
|
1781
|
+
...c.patternName && { patternName: c.patternName },
|
|
1782
|
+
lineIndex: c.lineIndex
|
|
1783
|
+
};
|
|
1784
|
+
}
|
|
1785
|
+
function previewArgs(input, max) {
|
|
1786
|
+
const cmd = input.command ?? input.query ?? input.file_path ?? JSON.stringify(input);
|
|
1787
|
+
const s = String(cmd).replace(TERMINAL_ESCAPE_RE, "").replace(/\s+/g, " ").trim();
|
|
1788
|
+
return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
|
|
1789
|
+
}
|
|
1790
|
+
function makeFinding(args) {
|
|
1791
|
+
const f = {
|
|
1792
|
+
type: args.type,
|
|
1793
|
+
ruleName: args.ruleName,
|
|
1794
|
+
verdict: args.verdict,
|
|
1795
|
+
severity: args.severity,
|
|
1796
|
+
reason: args.reason,
|
|
1797
|
+
toolName: args.toolName,
|
|
1798
|
+
agent: args.ctx.agent,
|
|
1799
|
+
sessionId: args.ctx.sessionId,
|
|
1800
|
+
project: args.ctx.project,
|
|
1801
|
+
lineIndex: args.ctx.lineIndex,
|
|
1802
|
+
sourceType: args.sourceType,
|
|
1803
|
+
firstSeenAt: args.ts,
|
|
1804
|
+
lastSeenAt: args.ts,
|
|
1805
|
+
occurrenceCount: 1
|
|
1806
|
+
};
|
|
1807
|
+
if (args.shieldLabel) f.shieldLabel = args.shieldLabel;
|
|
1808
|
+
if (args.subjectPath) f.subjectPath = args.subjectPath;
|
|
1809
|
+
if (args.input) f.input = args.input;
|
|
1810
|
+
if (args.patternName) f.patternName = args.patternName;
|
|
1811
|
+
if (args.redactedSample) f.redactedSample = args.redactedSample;
|
|
1812
|
+
return f;
|
|
1813
|
+
}
|
|
1814
|
+
function* stringValues(obj, depth = 0) {
|
|
1815
|
+
if (depth > 6) return;
|
|
1816
|
+
if (typeof obj === "string") {
|
|
1817
|
+
if (obj.length > 0) yield obj;
|
|
1818
|
+
return;
|
|
1819
|
+
}
|
|
1820
|
+
if (!obj || typeof obj !== "object") return;
|
|
1821
|
+
if (Array.isArray(obj)) {
|
|
1822
|
+
for (const v of obj) yield* stringValues(v, depth + 1);
|
|
1823
|
+
return;
|
|
1824
|
+
}
|
|
1825
|
+
for (const v of Object.values(obj)) yield* stringValues(v, depth + 1);
|
|
1826
|
+
}
|
|
1827
|
+
var ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, FS_READ_TOOLS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, BASH_TOOL_NAMES, AST_FS_REGEX_RULES, FS_OP_CACHE_MAX, fsOpCache, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, SQL_DML_KEYWORDS, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, LOOP_MAX_RECORDS, FINDING_TO_SIGNAL, SCAN_SIGNAL_WEIGHTS, LOOP_THRESHOLD_FOR_WASTE, COST_PER_LOOP_ITER_USD, DESTRUCTIVE_OP_RE, PRIVILEGE_ESCALATION_RE, SENSITIVE_PATH_RE, FILE_TOOLS, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC_RE, LONG_OUTPUT_THRESHOLD_BYTES, CANONICAL_EXTRACTOR_VERSION, DEDUPE_PREVIEW_LEN, TERMINAL_ESCAPE_RE;
|
|
1298
1828
|
var init_dist = __esm({
|
|
1299
1829
|
"packages/policy-engine/dist/index.mjs"() {
|
|
1300
1830
|
"use strict";
|
|
@@ -1762,6 +2292,83 @@ var init_dist = __esm({
|
|
|
1762
2292
|
]);
|
|
1763
2293
|
SHELL_INTERPRETERS = /* @__PURE__ */ new Set(["bash", "sh", "zsh", "fish", "dash", "ksh"]);
|
|
1764
2294
|
DOWNLOAD_CMDS = /* @__PURE__ */ new Set(["curl", "wget"]);
|
|
2295
|
+
NORMALIZE_CACHE_MAX = 5e3;
|
|
2296
|
+
normalizeCache = /* @__PURE__ */ new Map();
|
|
2297
|
+
AST_CACHE_MAX = 5e3;
|
|
2298
|
+
astCache = /* @__PURE__ */ new Map();
|
|
2299
|
+
PARSE_FAIL = /* @__PURE__ */ Symbol("parse-fail");
|
|
2300
|
+
FS_READ_TOOLS = /* @__PURE__ */ new Set([
|
|
2301
|
+
"cat",
|
|
2302
|
+
"less",
|
|
2303
|
+
"head",
|
|
2304
|
+
"tail",
|
|
2305
|
+
"bat",
|
|
2306
|
+
"more",
|
|
2307
|
+
"open",
|
|
2308
|
+
"print",
|
|
2309
|
+
"nano",
|
|
2310
|
+
"vim",
|
|
2311
|
+
"vi",
|
|
2312
|
+
"emacs",
|
|
2313
|
+
"code",
|
|
2314
|
+
"type"
|
|
2315
|
+
]);
|
|
2316
|
+
FS_OP_PRESCREEN_RE = /(?:^|[\s|;&(`\n])(?:rm|cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\b/;
|
|
2317
|
+
HOME_CACHE_ALLOWLIST = [
|
|
2318
|
+
".cache",
|
|
2319
|
+
".npm/_npx",
|
|
2320
|
+
".npm/_cacache",
|
|
2321
|
+
".cargo/registry",
|
|
2322
|
+
".gradle/caches",
|
|
2323
|
+
".gradle/.tmp",
|
|
2324
|
+
".m2/repository",
|
|
2325
|
+
".pnpm-store",
|
|
2326
|
+
".yarn/cache",
|
|
2327
|
+
".yarn/.cache",
|
|
2328
|
+
".cache/pip",
|
|
2329
|
+
".local/share/Trash",
|
|
2330
|
+
".rustup/downloads"
|
|
2331
|
+
];
|
|
2332
|
+
SENSITIVE_PATH_RULES = [
|
|
2333
|
+
{
|
|
2334
|
+
rule: "shield:project-jail:block-read-ssh",
|
|
2335
|
+
reason: "Reading SSH private keys is blocked by project-jail shield",
|
|
2336
|
+
match: (p) => /(^|[\\/])\.ssh[\\/]/i.test(p)
|
|
2337
|
+
},
|
|
2338
|
+
{
|
|
2339
|
+
rule: "shield:project-jail:block-read-aws",
|
|
2340
|
+
reason: "Reading AWS credentials is blocked by project-jail shield",
|
|
2341
|
+
match: (p) => /(^|[\\/])\.aws[\\/]/i.test(p)
|
|
2342
|
+
},
|
|
2343
|
+
{
|
|
2344
|
+
rule: "shield:project-jail:block-read-env",
|
|
2345
|
+
reason: "Reading .env files is blocked by project-jail shield",
|
|
2346
|
+
match: (p) => /(?:^|[\\/])\.env(?:\.local|\.production|\.staging)?$/i.test(p)
|
|
2347
|
+
},
|
|
2348
|
+
{
|
|
2349
|
+
rule: "shield:project-jail:block-read-credentials",
|
|
2350
|
+
reason: "Reading credential files is blocked by project-jail shield",
|
|
2351
|
+
match: (p) => /(?:credentials\.json|\.netrc|\.npmrc|\.docker[\\/]config\.json|gcloud[\\/]credentials)$/i.test(
|
|
2352
|
+
p
|
|
2353
|
+
)
|
|
2354
|
+
}
|
|
2355
|
+
];
|
|
2356
|
+
BASH_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
2357
|
+
"bash",
|
|
2358
|
+
"execute_bash",
|
|
2359
|
+
"run_shell_command",
|
|
2360
|
+
"shell",
|
|
2361
|
+
"exec_command"
|
|
2362
|
+
]);
|
|
2363
|
+
AST_FS_REGEX_RULES = /* @__PURE__ */ new Set([
|
|
2364
|
+
"block-rm-rf-home",
|
|
2365
|
+
"shield:project-jail:block-read-ssh",
|
|
2366
|
+
"shield:project-jail:block-read-aws",
|
|
2367
|
+
"shield:project-jail:block-read-env",
|
|
2368
|
+
"shield:project-jail:block-read-credentials"
|
|
2369
|
+
]);
|
|
2370
|
+
FS_OP_CACHE_MAX = 5e3;
|
|
2371
|
+
fsOpCache = /* @__PURE__ */ new Map();
|
|
1765
2372
|
SOURCE_COMMANDS = /* @__PURE__ */ new Set([
|
|
1766
2373
|
"cat",
|
|
1767
2374
|
"head",
|
|
@@ -2616,6 +3223,31 @@ var init_dist = __esm({
|
|
|
2616
3223
|
};
|
|
2617
3224
|
LOOP_THRESHOLD_FOR_WASTE = 3;
|
|
2618
3225
|
COST_PER_LOOP_ITER_USD = 6e-3;
|
|
3226
|
+
DESTRUCTIVE_OP_RE = /\brm\s+-[rRf]+\b|\bDROP\s+(TABLE|DATABASE|COLLECTION|SCHEMA)\b|\bTRUNCATE\s+TABLE\b|\bgit\s+push\s+(--force|-f)\b|\bFLUSHALL\b|\bFLUSHDB\b|\bkubectl\s+delete\b|\bhelm\s+uninstall\b/i;
|
|
3227
|
+
PRIVILEGE_ESCALATION_RE = /\b(sudo|su)\b\s+[a-z]|\bchmod\s+(0?777|\+x)\b|\bchown\s+root\b/i;
|
|
3228
|
+
SENSITIVE_PATH_RE = /\.aws\/(credentials|config)\b|\.ssh\/(id_rsa|id_ed25519|id_ecdsa|id_dsa)\b|\.env(\.|$|\b)|\.config\/gcloud\/credentials\.db\b|\.docker\/config\.json\b|\.netrc\b|\.npmrc\b|\.node9\/credentials\.json\b/i;
|
|
3229
|
+
FILE_TOOLS = /* @__PURE__ */ new Set([
|
|
3230
|
+
"read",
|
|
3231
|
+
"read_file",
|
|
3232
|
+
"edit",
|
|
3233
|
+
"edit_file",
|
|
3234
|
+
"write",
|
|
3235
|
+
"write_file",
|
|
3236
|
+
"multiedit",
|
|
3237
|
+
"grep",
|
|
3238
|
+
"grep_search",
|
|
3239
|
+
"glob",
|
|
3240
|
+
"list_files"
|
|
3241
|
+
]);
|
|
3242
|
+
PII_EMAIL_RE = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/;
|
|
3243
|
+
PII_SSN_RE = /\b\d{3}-\d{2}-\d{4}\b/;
|
|
3244
|
+
PII_PHONE_RE = /\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]\d{3}[-.\s]\d{4}\b/;
|
|
3245
|
+
PII_CC_RE = /\b(?:4\d{3}|5[1-5]\d{2}|3[47]\d{2}|6\d{3})[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b/;
|
|
3246
|
+
LONG_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
|
|
3247
|
+
CANONICAL_EXTRACTOR_VERSION = "canonical-v1";
|
|
3248
|
+
DEDUPE_PREVIEW_LEN = 120;
|
|
3249
|
+
TERMINAL_ESCAPE_RE = // eslint-disable-next-line no-control-regex
|
|
3250
|
+
/\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[@-_]|[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g;
|
|
2619
3251
|
}
|
|
2620
3252
|
});
|
|
2621
3253
|
|
|
@@ -5541,9 +6173,7 @@ function removeNode9McpServer(servers) {
|
|
|
5541
6173
|
return true;
|
|
5542
6174
|
}
|
|
5543
6175
|
function printDaemonTip() {
|
|
5544
|
-
console.log(
|
|
5545
|
-
chalk.cyan("\n \u{1F4A1} Node9 will protect you automatically using Native OS popups.") + chalk.white("\n To view your history or manage persistent rules, run:") + chalk.green("\n node9 daemon --openui")
|
|
5546
|
-
);
|
|
6176
|
+
console.log(chalk.cyan("\n \u{1F4A1} Node9 will protect you automatically using Native OS popups."));
|
|
5547
6177
|
}
|
|
5548
6178
|
function fullPathCommand(subcommand) {
|
|
5549
6179
|
if (process.env.NODE9_TESTING === "1") return `node9 ${subcommand}`;
|
|
@@ -6627,7 +7257,8 @@ function buildScanSummary(agents) {
|
|
|
6627
7257
|
timestamp: f.timestamp,
|
|
6628
7258
|
project: f.project,
|
|
6629
7259
|
sessionId: f.sessionId,
|
|
6630
|
-
agent: f.agent
|
|
7260
|
+
agent: f.agent,
|
|
7261
|
+
kind: f.kind
|
|
6631
7262
|
}))
|
|
6632
7263
|
);
|
|
6633
7264
|
const byVerdict = {
|
|
@@ -6645,10 +7276,7 @@ function buildScanSummary(agents) {
|
|
|
6645
7276
|
costUSD: a.scan.totalCostUSD
|
|
6646
7277
|
})).filter((s) => s.sessions > 0 || s.findings > 0);
|
|
6647
7278
|
const sections = buildSections(allFindings);
|
|
6648
|
-
const wastedIters = allLoops.reduce(
|
|
6649
|
-
(sum, l) => sum + Math.max(0, l.count - LOOP_THRESHOLD_FOR_WASTE),
|
|
6650
|
-
0
|
|
6651
|
-
);
|
|
7279
|
+
const wastedIters = allLoops.filter((l) => l.kind !== "long-iteration").reduce((sum, l) => sum + Math.max(0, l.count - LOOP_THRESHOLD_FOR_WASTE), 0);
|
|
6652
7280
|
const loopWastedUSD = wastedIters * COST_PER_LOOP_ITER_USD;
|
|
6653
7281
|
return {
|
|
6654
7282
|
stats,
|
|
@@ -7263,8 +7891,10 @@ var init_costSync = __esm({
|
|
|
7263
7891
|
// src/daemon/scan-watermark.ts
|
|
7264
7892
|
var scan_watermark_exports = {};
|
|
7265
7893
|
__export(scan_watermark_exports, {
|
|
7894
|
+
WATERMARK_SCHEMA_VERSION: () => WATERMARK_SCHEMA_VERSION,
|
|
7266
7895
|
extractFindingsFromLine: () => extractFindingsFromLine,
|
|
7267
7896
|
loadWatermark: () => loadWatermark,
|
|
7897
|
+
markUploadComplete: () => markUploadComplete,
|
|
7268
7898
|
saveWatermark: () => saveWatermark,
|
|
7269
7899
|
scanDelta: () => scanDelta,
|
|
7270
7900
|
tickScanWatcher: () => tickScanWatcher
|
|
@@ -7273,18 +7903,68 @@ import fs16 from "fs";
|
|
|
7273
7903
|
import os15 from "os";
|
|
7274
7904
|
import path18 from "path";
|
|
7275
7905
|
import readline from "readline";
|
|
7906
|
+
function freshWatermark() {
|
|
7907
|
+
return {
|
|
7908
|
+
schemaVersion: WATERMARK_SCHEMA_VERSION,
|
|
7909
|
+
extractorVersion: CANONICAL_EXTRACTOR_VERSION,
|
|
7910
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7911
|
+
files: {}
|
|
7912
|
+
};
|
|
7913
|
+
}
|
|
7276
7914
|
function loadWatermark() {
|
|
7915
|
+
let raw;
|
|
7277
7916
|
try {
|
|
7278
|
-
|
|
7279
|
-
const parsed = JSON.parse(raw);
|
|
7280
|
-
if (typeof parsed.createdAt === "string" && parsed.files && typeof parsed.files === "object") {
|
|
7281
|
-
return parsed;
|
|
7282
|
-
}
|
|
7917
|
+
raw = fs16.readFileSync(WATERMARK_FILE(), "utf-8");
|
|
7283
7918
|
} catch {
|
|
7919
|
+
return { status: "fresh", wm: freshWatermark() };
|
|
7284
7920
|
}
|
|
7285
|
-
|
|
7921
|
+
let parsed;
|
|
7922
|
+
try {
|
|
7923
|
+
parsed = JSON.parse(raw);
|
|
7924
|
+
} catch {
|
|
7925
|
+
return { status: "fresh", wm: freshWatermark() };
|
|
7926
|
+
}
|
|
7927
|
+
if (typeof parsed.createdAt !== "string" || !parsed.files || typeof parsed.files !== "object") {
|
|
7928
|
+
return { status: "fresh", wm: freshWatermark() };
|
|
7929
|
+
}
|
|
7930
|
+
const fileSchemaVersion = typeof parsed.schemaVersion === "number" ? parsed.schemaVersion : 1;
|
|
7931
|
+
if (fileSchemaVersion > WATERMARK_SCHEMA_VERSION) {
|
|
7932
|
+
const wm2 = {
|
|
7933
|
+
schemaVersion: fileSchemaVersion,
|
|
7934
|
+
extractorVersion: typeof parsed.extractorVersion === "string" ? parsed.extractorVersion : CANONICAL_EXTRACTOR_VERSION,
|
|
7935
|
+
createdAt: parsed.createdAt,
|
|
7936
|
+
files: parsed.files
|
|
7937
|
+
};
|
|
7938
|
+
return { status: "schema-future", wm: wm2 };
|
|
7939
|
+
}
|
|
7940
|
+
const fileExtractorVersion = typeof parsed.extractorVersion === "string" ? parsed.extractorVersion : "";
|
|
7941
|
+
if (fileExtractorVersion !== CANONICAL_EXTRACTOR_VERSION) {
|
|
7942
|
+
const filesIn = parsed.files;
|
|
7943
|
+
const filesOut = {};
|
|
7944
|
+
for (const [k, v] of Object.entries(filesIn)) {
|
|
7945
|
+
filesOut[k] = { scannedTo: 0 };
|
|
7946
|
+
void v;
|
|
7947
|
+
}
|
|
7948
|
+
const wm2 = {
|
|
7949
|
+
schemaVersion: WATERMARK_SCHEMA_VERSION,
|
|
7950
|
+
extractorVersion: CANONICAL_EXTRACTOR_VERSION,
|
|
7951
|
+
pendingResetUploadAs: "totals",
|
|
7952
|
+
createdAt: parsed.createdAt,
|
|
7953
|
+
files: filesOut
|
|
7954
|
+
};
|
|
7955
|
+
return { status: "extractor-stale", wm: wm2 };
|
|
7956
|
+
}
|
|
7957
|
+
const wm = {
|
|
7958
|
+
schemaVersion: WATERMARK_SCHEMA_VERSION,
|
|
7959
|
+
extractorVersion: CANONICAL_EXTRACTOR_VERSION,
|
|
7960
|
+
...parsed.pendingResetUploadAs === "totals" && { pendingResetUploadAs: "totals" },
|
|
7961
|
+
createdAt: parsed.createdAt,
|
|
7962
|
+
files: parsed.files
|
|
7963
|
+
};
|
|
7964
|
+
return { status: "current", wm };
|
|
7286
7965
|
}
|
|
7287
7966
|
function saveWatermark(wm) {
|
|
7967
|
+
if (wm.schemaVersion > WATERMARK_SCHEMA_VERSION) return;
|
|
7288
7968
|
const target = WATERMARK_FILE();
|
|
7289
7969
|
const dir = path18.dirname(target);
|
|
7290
7970
|
if (!fs16.existsSync(dir)) fs16.mkdirSync(dir, { recursive: true });
|
|
@@ -7377,6 +8057,16 @@ function extractFindingsFromLine(line, sessionId, lineIndex) {
|
|
|
7377
8057
|
}
|
|
7378
8058
|
}
|
|
7379
8059
|
}
|
|
8060
|
+
const ctx = {
|
|
8061
|
+
sessionId,
|
|
8062
|
+
lineIndex,
|
|
8063
|
+
project: "",
|
|
8064
|
+
agent: "claude",
|
|
8065
|
+
rules: [],
|
|
8066
|
+
toolInspection: { bash: "command", execute_bash: "command" },
|
|
8067
|
+
dlpEnabled: false
|
|
8068
|
+
// line-level DLP runs above already
|
|
8069
|
+
};
|
|
7380
8070
|
const message = line.message;
|
|
7381
8071
|
if (message && typeof message === "object") {
|
|
7382
8072
|
const content = message.content;
|
|
@@ -7387,73 +8077,105 @@ function extractFindingsFromLine(line, sessionId, lineIndex) {
|
|
|
7387
8077
|
if (b.type === "tool_result") {
|
|
7388
8078
|
const c = b.content;
|
|
7389
8079
|
const len = typeof c === "string" ? c.length : Array.isArray(c) ? JSON.stringify(c).length : 0;
|
|
7390
|
-
if (len >
|
|
8080
|
+
if (len > LONG_OUTPUT_THRESHOLD_BYTES2) {
|
|
7391
8081
|
findings.push({
|
|
7392
8082
|
type: "long-output-redacted",
|
|
7393
8083
|
sessionId,
|
|
7394
8084
|
lineIndex
|
|
7395
8085
|
});
|
|
7396
8086
|
}
|
|
8087
|
+
continue;
|
|
7397
8088
|
}
|
|
7398
8089
|
if (b.type !== "tool_use") continue;
|
|
7399
|
-
const toolName = typeof b.name === "string" ? b.name
|
|
7400
|
-
const input = b.input;
|
|
7401
|
-
|
|
7402
|
-
|
|
7403
|
-
|
|
7404
|
-
|
|
7405
|
-
|
|
7406
|
-
|
|
7407
|
-
|
|
7408
|
-
|
|
7409
|
-
|
|
7410
|
-
}
|
|
7411
|
-
if (toolName !== "bash" && toolName !== "execute_bash") continue;
|
|
7412
|
-
const command = input && typeof input.command === "string" ? input.command : "";
|
|
7413
|
-
if (!command) continue;
|
|
7414
|
-
const verdict = detectDangerousShellExec(command);
|
|
7415
|
-
if (verdict) {
|
|
7416
|
-
findings.push({ type: "eval-of-remote", sessionId, lineIndex });
|
|
7417
|
-
}
|
|
7418
|
-
const pipe = analyzePipeChain(command);
|
|
7419
|
-
if (pipe.isPipeline && pipe.risk === "critical") {
|
|
7420
|
-
findings.push({ type: "pipe-to-shell", sessionId, lineIndex });
|
|
7421
|
-
}
|
|
7422
|
-
if (DESTRUCTIVE_OP_RE.test(command)) {
|
|
7423
|
-
findings.push({ type: "destructive-op", sessionId, lineIndex });
|
|
7424
|
-
}
|
|
7425
|
-
if (PRIVILEGE_ESCALATION_RE.test(command)) {
|
|
7426
|
-
findings.push({
|
|
7427
|
-
type: "privilege-escalation",
|
|
7428
|
-
sessionId,
|
|
7429
|
-
lineIndex
|
|
7430
|
-
});
|
|
8090
|
+
const toolName = typeof b.name === "string" ? b.name : "";
|
|
8091
|
+
const input = b.input ?? {};
|
|
8092
|
+
const call = {
|
|
8093
|
+
toolName,
|
|
8094
|
+
args: input,
|
|
8095
|
+
timestamp: typeof obj.timestamp === "string" ? obj.timestamp : ""
|
|
8096
|
+
};
|
|
8097
|
+
const canonical = extractCanonicalFindings(call, ctx);
|
|
8098
|
+
for (const cf of canonical) {
|
|
8099
|
+
const sf = toScanFinding(cf);
|
|
8100
|
+
if (sf) findings.push(sf);
|
|
7431
8101
|
}
|
|
7432
8102
|
}
|
|
7433
8103
|
}
|
|
7434
8104
|
}
|
|
7435
8105
|
return findings;
|
|
7436
8106
|
}
|
|
7437
|
-
function
|
|
7438
|
-
const
|
|
7439
|
-
if (
|
|
7440
|
-
if (
|
|
7441
|
-
if (
|
|
7442
|
-
|
|
7443
|
-
|
|
8107
|
+
function markUploadComplete() {
|
|
8108
|
+
const state = loadWatermark();
|
|
8109
|
+
if (state.status === "schema-future") return;
|
|
8110
|
+
if (state.status === "extractor-stale") return;
|
|
8111
|
+
if (!state.wm.pendingResetUploadAs) return;
|
|
8112
|
+
delete state.wm.pendingResetUploadAs;
|
|
8113
|
+
saveWatermark(state.wm);
|
|
7444
8114
|
}
|
|
7445
8115
|
async function tickScanWatcher() {
|
|
7446
8116
|
if (process.env.NODE9_SCAN_DISABLE === "1") {
|
|
7447
|
-
return
|
|
7448
|
-
|
|
7449
|
-
|
|
7450
|
-
|
|
7451
|
-
|
|
7452
|
-
|
|
7453
|
-
|
|
7454
|
-
};
|
|
8117
|
+
return emptyTick("deltas");
|
|
8118
|
+
}
|
|
8119
|
+
const state = loadWatermark();
|
|
8120
|
+
if (state.status === "schema-future") {
|
|
8121
|
+
if (process.env.NODE9_DEBUG === "1") {
|
|
8122
|
+
process.stderr.write("[node9] watermark schema is from a newer daemon \u2014 skipping tick.\n");
|
|
8123
|
+
}
|
|
8124
|
+
return { ...emptyTick("deltas"), schemaFuture: true };
|
|
8125
|
+
}
|
|
8126
|
+
if (state.status === "extractor-stale") {
|
|
8127
|
+
if (process.env.NODE9_SKIP_WATERMARK_RESET === "1") {
|
|
8128
|
+
const acknowledged = readRawWatermarkPreservingOffsets();
|
|
8129
|
+
if (acknowledged) {
|
|
8130
|
+
saveWatermark(acknowledged);
|
|
8131
|
+
}
|
|
8132
|
+
process.stderr.write(
|
|
8133
|
+
"[node9] Extractor upgrade acknowledged via NODE9_SKIP_WATERMARK_RESET.\n Existing verdicts not refreshed \u2014 run `node9 scan --upload-history`\n to backfill them through the new pipeline.\n"
|
|
8134
|
+
);
|
|
8135
|
+
return runActualTick(loadWatermark().wm);
|
|
8136
|
+
}
|
|
8137
|
+
process.stderr.write(
|
|
8138
|
+
"[node9] Detector upgrade detected \u2014 re-scanning history through the new\n pipeline. Expect a one-time SaaS payload spike on this tick.\n Set NODE9_SKIP_WATERMARK_RESET=1 to skip.\n"
|
|
8139
|
+
);
|
|
8140
|
+
}
|
|
8141
|
+
return runActualTick(state.wm);
|
|
8142
|
+
}
|
|
8143
|
+
function emptyTick(uploadAs) {
|
|
8144
|
+
return {
|
|
8145
|
+
findings: [],
|
|
8146
|
+
totalToolCalls: 0,
|
|
8147
|
+
toolCallsBySession: {},
|
|
8148
|
+
filesScanned: 0,
|
|
8149
|
+
filesNew: 0,
|
|
8150
|
+
filesSkipped: 0,
|
|
8151
|
+
uploadAs,
|
|
8152
|
+
schemaFuture: false
|
|
8153
|
+
};
|
|
8154
|
+
}
|
|
8155
|
+
function readRawWatermarkPreservingOffsets() {
|
|
8156
|
+
let raw;
|
|
8157
|
+
try {
|
|
8158
|
+
raw = fs16.readFileSync(WATERMARK_FILE(), "utf-8");
|
|
8159
|
+
} catch {
|
|
8160
|
+
return null;
|
|
8161
|
+
}
|
|
8162
|
+
let parsed;
|
|
8163
|
+
try {
|
|
8164
|
+
parsed = JSON.parse(raw);
|
|
8165
|
+
} catch {
|
|
8166
|
+
return null;
|
|
7455
8167
|
}
|
|
7456
|
-
|
|
8168
|
+
if (typeof parsed.createdAt !== "string" || !parsed.files || typeof parsed.files !== "object") {
|
|
8169
|
+
return null;
|
|
8170
|
+
}
|
|
8171
|
+
return {
|
|
8172
|
+
schemaVersion: WATERMARK_SCHEMA_VERSION,
|
|
8173
|
+
extractorVersion: CANONICAL_EXTRACTOR_VERSION,
|
|
8174
|
+
createdAt: parsed.createdAt,
|
|
8175
|
+
files: parsed.files
|
|
8176
|
+
};
|
|
8177
|
+
}
|
|
8178
|
+
async function runActualTick(wm) {
|
|
7457
8179
|
const watermarkCreatedAt = new Date(wm.createdAt).getTime();
|
|
7458
8180
|
const findings = [];
|
|
7459
8181
|
let totalToolCalls = 0;
|
|
@@ -7500,10 +8222,20 @@ async function tickScanWatcher() {
|
|
|
7500
8222
|
wm.files[filePath] = { scannedTo: newScannedTo };
|
|
7501
8223
|
filesScanned++;
|
|
7502
8224
|
}
|
|
8225
|
+
const uploadAs = wm.pendingResetUploadAs === "totals" ? "totals" : "deltas";
|
|
7503
8226
|
saveWatermark(wm);
|
|
7504
|
-
return {
|
|
8227
|
+
return {
|
|
8228
|
+
findings,
|
|
8229
|
+
totalToolCalls,
|
|
8230
|
+
toolCallsBySession,
|
|
8231
|
+
filesScanned,
|
|
8232
|
+
filesNew,
|
|
8233
|
+
filesSkipped,
|
|
8234
|
+
uploadAs,
|
|
8235
|
+
schemaFuture: false
|
|
8236
|
+
};
|
|
7505
8237
|
}
|
|
7506
|
-
var PROJECTS_DIR, WATERMARK_FILE, MAX_LINE_BYTES,
|
|
8238
|
+
var PROJECTS_DIR, WATERMARK_FILE, MAX_LINE_BYTES, WATERMARK_SCHEMA_VERSION, LONG_OUTPUT_THRESHOLD_BYTES2;
|
|
7507
8239
|
var init_scan_watermark = __esm({
|
|
7508
8240
|
"src/daemon/scan-watermark.ts"() {
|
|
7509
8241
|
"use strict";
|
|
@@ -7512,27 +8244,8 @@ var init_scan_watermark = __esm({
|
|
|
7512
8244
|
PROJECTS_DIR = () => path18.join(os15.homedir(), ".claude", "projects");
|
|
7513
8245
|
WATERMARK_FILE = () => path18.join(os15.homedir(), ".node9", "scan-watermark.json");
|
|
7514
8246
|
MAX_LINE_BYTES = 2 * 1024 * 1024;
|
|
7515
|
-
|
|
7516
|
-
|
|
7517
|
-
FILE_TOOLS = /* @__PURE__ */ new Set([
|
|
7518
|
-
"read",
|
|
7519
|
-
"read_file",
|
|
7520
|
-
"edit",
|
|
7521
|
-
"edit_file",
|
|
7522
|
-
"write",
|
|
7523
|
-
"write_file",
|
|
7524
|
-
"multiedit",
|
|
7525
|
-
"grep",
|
|
7526
|
-
"grep_search",
|
|
7527
|
-
"glob",
|
|
7528
|
-
"list_files"
|
|
7529
|
-
]);
|
|
7530
|
-
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;
|
|
7531
|
-
PII_EMAIL_RE = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/;
|
|
7532
|
-
PII_SSN_RE = /\b\d{3}-\d{2}-\d{4}\b/;
|
|
7533
|
-
PII_PHONE_RE = /\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]\d{3}[-.\s]\d{4}\b/;
|
|
7534
|
-
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/;
|
|
7535
|
-
PRIVILEGE_ESCALATION_RE = /\b(sudo|su)\b\s+[a-z]|\bchmod\s+(0?777|\+x)\b|\bchown\s+root\b/i;
|
|
8247
|
+
WATERMARK_SCHEMA_VERSION = 2;
|
|
8248
|
+
LONG_OUTPUT_THRESHOLD_BYTES2 = LONG_OUTPUT_THRESHOLD_BYTES;
|
|
7536
8249
|
}
|
|
7537
8250
|
});
|
|
7538
8251
|
|
|
@@ -7657,6 +8370,13 @@ async function runUploadHistory(opts) {
|
|
|
7657
8370
|
let linesParsed = 0;
|
|
7658
8371
|
let linesSkipped = 0;
|
|
7659
8372
|
const dailyEntries = [];
|
|
8373
|
+
const liveLoopCfg = getConfig().policy.loopDetection;
|
|
8374
|
+
const loopCfg = {
|
|
8375
|
+
enabled: liveLoopCfg.enabled,
|
|
8376
|
+
threshold: 3,
|
|
8377
|
+
windowSeconds: 0
|
|
8378
|
+
// "no window" — engine treats this as session-wide
|
|
8379
|
+
};
|
|
7660
8380
|
for (const { filePath, sessionId, projectDir } of iterateJsonlFiles(cutoffMs)) {
|
|
7661
8381
|
filesScanned++;
|
|
7662
8382
|
let content;
|
|
@@ -7666,6 +8386,7 @@ async function runUploadHistory(opts) {
|
|
|
7666
8386
|
continue;
|
|
7667
8387
|
}
|
|
7668
8388
|
let lineIndex = 0;
|
|
8389
|
+
const sessionCalls = [];
|
|
7669
8390
|
for (const line of content.split("\n")) {
|
|
7670
8391
|
if (!line.trim()) continue;
|
|
7671
8392
|
let obj;
|
|
@@ -7682,10 +8403,38 @@ async function runUploadHistory(opts) {
|
|
|
7682
8403
|
if (obj["type"] === "assistant" && Array.isArray(msg?.content) && msg.content.some((b) => b.type === "tool_use")) {
|
|
7683
8404
|
totalToolCalls++;
|
|
7684
8405
|
toolCallsBySession[sessionId] = (toolCallsBySession[sessionId] ?? 0) + 1;
|
|
8406
|
+
const ts = typeof obj.timestamp === "string" ? obj.timestamp : "";
|
|
8407
|
+
for (const block of msg.content) {
|
|
8408
|
+
if (!block || typeof block !== "object") continue;
|
|
8409
|
+
const b = block;
|
|
8410
|
+
if (b.type !== "tool_use") continue;
|
|
8411
|
+
sessionCalls.push({
|
|
8412
|
+
toolName: typeof b.name === "string" ? b.name : "",
|
|
8413
|
+
args: b.input ?? {},
|
|
8414
|
+
timestamp: ts,
|
|
8415
|
+
lineIndex
|
|
8416
|
+
});
|
|
8417
|
+
}
|
|
7685
8418
|
}
|
|
7686
8419
|
linesParsed++;
|
|
7687
8420
|
lineIndex++;
|
|
7688
8421
|
}
|
|
8422
|
+
if (loopCfg.enabled && sessionCalls.length > 0) {
|
|
8423
|
+
const loops = extractSessionLevelFindings(sessionCalls, {
|
|
8424
|
+
sessionId,
|
|
8425
|
+
project: decodeProjectDirName(projectDir),
|
|
8426
|
+
agent: "claude",
|
|
8427
|
+
loopDetection: {
|
|
8428
|
+
enabled: loopCfg.enabled,
|
|
8429
|
+
threshold: loopCfg.threshold,
|
|
8430
|
+
windowSeconds: loopCfg.windowSeconds
|
|
8431
|
+
}
|
|
8432
|
+
});
|
|
8433
|
+
for (const cf of loops) {
|
|
8434
|
+
const sf = toScanFinding(cf);
|
|
8435
|
+
if (sf) findings.push(sf);
|
|
8436
|
+
}
|
|
8437
|
+
}
|
|
7689
8438
|
const fallbackWorkingDir = decodeProjectDirName(projectDir);
|
|
7690
8439
|
const dailyMap = parseJSONLFile(filePath, fallbackWorkingDir);
|
|
7691
8440
|
for (const entry of dailyMap.values()) {
|
|
@@ -7710,7 +8459,8 @@ async function runUploadHistory(opts) {
|
|
|
7710
8459
|
const scanUrl = creds.apiUrl.endsWith("/policies/sync") ? creds.apiUrl.replace(/\/policies\/sync$/, "/scan/report") : `${creds.apiUrl.replace(/\/$/, "")}/scan/report`;
|
|
7711
8460
|
await postJson(scanUrl, creds.apiKey, {
|
|
7712
8461
|
...summary,
|
|
7713
|
-
sessionTotals
|
|
8462
|
+
sessionTotals,
|
|
8463
|
+
extractorVersion: CANONICAL_EXTRACTOR_VERSION
|
|
7714
8464
|
});
|
|
7715
8465
|
console.log(chalk3.green(` \u2713 Uploaded scanner findings`));
|
|
7716
8466
|
if (dailyEntries.length > 0) {
|
|
@@ -7812,6 +8562,20 @@ function geminiModelPrice(model) {
|
|
|
7812
8562
|
if (base.includes("flash")) return GEMINI_PRICING["gemini-2.0-flash"];
|
|
7813
8563
|
return null;
|
|
7814
8564
|
}
|
|
8565
|
+
function isNode9SelfOutput(text) {
|
|
8566
|
+
let hits = 0;
|
|
8567
|
+
for (const re of SELF_OUTPUT_MARKERS) {
|
|
8568
|
+
if (re.test(text)) hits++;
|
|
8569
|
+
if (hits >= 2) return true;
|
|
8570
|
+
}
|
|
8571
|
+
return false;
|
|
8572
|
+
}
|
|
8573
|
+
function looksLikeFixtureToken(sample) {
|
|
8574
|
+
for (const re of FIXTURE_TOKEN_PATTERNS) {
|
|
8575
|
+
if (re.test(sample)) return true;
|
|
8576
|
+
}
|
|
8577
|
+
return false;
|
|
8578
|
+
}
|
|
7815
8579
|
function num(n) {
|
|
7816
8580
|
return n.toLocaleString();
|
|
7817
8581
|
}
|
|
@@ -7832,7 +8596,7 @@ function fmtTs(ts) {
|
|
|
7832
8596
|
}
|
|
7833
8597
|
}
|
|
7834
8598
|
function stripTerminalEscapes(s) {
|
|
7835
|
-
return s.replace(
|
|
8599
|
+
return s.replace(TERMINAL_ESCAPE_RE2, "");
|
|
7836
8600
|
}
|
|
7837
8601
|
function preview(input, max) {
|
|
7838
8602
|
const cmd = input.command ?? input.query ?? input.file_path ?? JSON.stringify(input);
|
|
@@ -7875,6 +8639,45 @@ function buildRecurringPatternSet(findings) {
|
|
|
7875
8639
|
}
|
|
7876
8640
|
return recurring;
|
|
7877
8641
|
}
|
|
8642
|
+
function pushFsOpAstFinding(command, toolName, input, timestamp, projLabel, sessionId, agent, result) {
|
|
8643
|
+
const fsVerdict = analyzeFsOperation(command);
|
|
8644
|
+
if (!fsVerdict) return false;
|
|
8645
|
+
const synthRule = {
|
|
8646
|
+
name: fsVerdict.ruleName,
|
|
8647
|
+
tool: "bash",
|
|
8648
|
+
conditions: [],
|
|
8649
|
+
verdict: fsVerdict.verdict,
|
|
8650
|
+
reason: fsVerdict.reason
|
|
8651
|
+
};
|
|
8652
|
+
const isShieldRule = fsVerdict.ruleName.startsWith("shield:");
|
|
8653
|
+
const synthSource = isShieldRule ? {
|
|
8654
|
+
shieldName: "project-jail",
|
|
8655
|
+
shieldLabel: "project-jail (AST)",
|
|
8656
|
+
sourceType: "shield",
|
|
8657
|
+
rule: synthRule
|
|
8658
|
+
} : {
|
|
8659
|
+
shieldName: "",
|
|
8660
|
+
shieldLabel: "default (AST)",
|
|
8661
|
+
sourceType: "default",
|
|
8662
|
+
rule: synthRule
|
|
8663
|
+
};
|
|
8664
|
+
const inputPreview = preview(input, 120);
|
|
8665
|
+
const isDupe = result.findings.some(
|
|
8666
|
+
(f) => f.source.rule.name === synthRule.name && preview(f.input, 120) === inputPreview && f.project === projLabel
|
|
8667
|
+
);
|
|
8668
|
+
if (!isDupe) {
|
|
8669
|
+
result.findings.push({
|
|
8670
|
+
source: synthSource,
|
|
8671
|
+
toolName,
|
|
8672
|
+
input,
|
|
8673
|
+
timestamp,
|
|
8674
|
+
project: projLabel,
|
|
8675
|
+
sessionId,
|
|
8676
|
+
agent
|
|
8677
|
+
});
|
|
8678
|
+
}
|
|
8679
|
+
return true;
|
|
8680
|
+
}
|
|
7878
8681
|
function isStaleFinding(timestamp, now = Date.now()) {
|
|
7879
8682
|
if (!timestamp) return false;
|
|
7880
8683
|
const t = Date.parse(timestamp);
|
|
@@ -7908,15 +8711,24 @@ function detectLoops(calls, project, sessionId, agent) {
|
|
|
7908
8711
|
const entry = counts.get(key) ?? {
|
|
7909
8712
|
count: 0,
|
|
7910
8713
|
timestamp: call.timestamp,
|
|
8714
|
+
firstTs: null,
|
|
8715
|
+
lastTs: null,
|
|
7911
8716
|
input: call.input,
|
|
7912
8717
|
toolName: call.toolName
|
|
7913
8718
|
};
|
|
7914
8719
|
entry.count++;
|
|
8720
|
+
const t = call.timestamp ? Date.parse(call.timestamp) : NaN;
|
|
8721
|
+
if (!Number.isNaN(t)) {
|
|
8722
|
+
if (entry.firstTs === null || t < entry.firstTs) entry.firstTs = t;
|
|
8723
|
+
if (entry.lastTs === null || t > entry.lastTs) entry.lastTs = t;
|
|
8724
|
+
}
|
|
7915
8725
|
counts.set(key, entry);
|
|
7916
8726
|
}
|
|
7917
8727
|
const findings = [];
|
|
7918
8728
|
for (const [, entry] of counts) {
|
|
7919
8729
|
if (entry.count >= LOOP_THRESHOLD) {
|
|
8730
|
+
const span = entry.firstTs !== null && entry.lastTs !== null ? entry.lastTs - entry.firstTs : 0;
|
|
8731
|
+
const kind = span >= LOOP_TIMESPAN_THRESHOLD_MS ? "long-iteration" : "loop";
|
|
7920
8732
|
findings.push({
|
|
7921
8733
|
toolName: entry.toolName,
|
|
7922
8734
|
commandPreview: preview(entry.input, 80),
|
|
@@ -7924,7 +8736,8 @@ function detectLoops(calls, project, sessionId, agent) {
|
|
|
7924
8736
|
timestamp: entry.timestamp,
|
|
7925
8737
|
project,
|
|
7926
8738
|
sessionId,
|
|
7927
|
-
agent
|
|
8739
|
+
agent,
|
|
8740
|
+
kind
|
|
7928
8741
|
});
|
|
7929
8742
|
}
|
|
7930
8743
|
}
|
|
@@ -8143,8 +8956,10 @@ function scanClaudeHistory(startDate, onProgress, onLine) {
|
|
|
8143
8956
|
}
|
|
8144
8957
|
const resultText = typeof block.content === "string" ? block.content : Array.isArray(block.content) ? block.content.map((c) => c.text ?? "").join("\n") : null;
|
|
8145
8958
|
if (!resultText) continue;
|
|
8959
|
+
if (isNode9SelfOutput(resultText)) continue;
|
|
8146
8960
|
const dlpMatch = scanArgs({ text: resultText });
|
|
8147
8961
|
if (dlpMatch) {
|
|
8962
|
+
if (looksLikeFixtureToken(dlpMatch.redactedSample)) continue;
|
|
8148
8963
|
if (firstDlpTs === null) firstDlpTs = entry.timestamp ?? null;
|
|
8149
8964
|
const isDupe = result.dlpFindings.some(
|
|
8150
8965
|
(f) => f.patternName === dlpMatch.patternName && f.redactedSample === dlpMatch.redactedSample && f.project === projLabel
|
|
@@ -8215,11 +9030,26 @@ function scanClaudeHistory(startDate, onProgress, onLine) {
|
|
|
8215
9030
|
});
|
|
8216
9031
|
}
|
|
8217
9032
|
}
|
|
8218
|
-
let
|
|
9033
|
+
let astFsMatched = false;
|
|
9034
|
+
const astRanForBash = toolNameLower === "bash" || toolNameLower === "execute_bash";
|
|
9035
|
+
if (astRanForBash) {
|
|
9036
|
+
astFsMatched = pushFsOpAstFinding(
|
|
9037
|
+
String(input.command ?? ""),
|
|
9038
|
+
toolName,
|
|
9039
|
+
input,
|
|
9040
|
+
entry.timestamp ?? "",
|
|
9041
|
+
projLabel,
|
|
9042
|
+
sessionId,
|
|
9043
|
+
"claude",
|
|
9044
|
+
result
|
|
9045
|
+
);
|
|
9046
|
+
}
|
|
9047
|
+
let ruleMatched = astFsMatched;
|
|
8219
9048
|
for (const source of ruleSources) {
|
|
8220
9049
|
const { rule } = source;
|
|
8221
9050
|
if (rule.verdict === "allow") continue;
|
|
8222
9051
|
if (rule.tool && !matchesPattern(toolNameLower, rule.tool)) continue;
|
|
9052
|
+
if (astRanForBash && rule.name && AST_FS_REGEX_RULES.has(rule.name)) continue;
|
|
8223
9053
|
if (!evaluateSmartConditions(input, rule)) continue;
|
|
8224
9054
|
const inputPreview = preview(input, 120);
|
|
8225
9055
|
const isDupe = result.findings.some(
|
|
@@ -8415,11 +9245,26 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
|
|
|
8415
9245
|
});
|
|
8416
9246
|
}
|
|
8417
9247
|
}
|
|
8418
|
-
let
|
|
9248
|
+
let astFsMatched = false;
|
|
9249
|
+
const astRanForBash = toolNameLower === "run_shell_command" || toolNameLower === "shell";
|
|
9250
|
+
if (astRanForBash) {
|
|
9251
|
+
astFsMatched = pushFsOpAstFinding(
|
|
9252
|
+
String(input.command ?? ""),
|
|
9253
|
+
toolName,
|
|
9254
|
+
input,
|
|
9255
|
+
msg.timestamp ?? "",
|
|
9256
|
+
projLabel,
|
|
9257
|
+
sessionId,
|
|
9258
|
+
"gemini",
|
|
9259
|
+
result
|
|
9260
|
+
);
|
|
9261
|
+
}
|
|
9262
|
+
let ruleMatched = astFsMatched;
|
|
8419
9263
|
for (const source of ruleSources) {
|
|
8420
9264
|
const { rule } = source;
|
|
8421
9265
|
if (rule.verdict === "allow") continue;
|
|
8422
9266
|
if (rule.tool && !matchesPattern(toolNameLower, rule.tool)) continue;
|
|
9267
|
+
if (astRanForBash && rule.name && AST_FS_REGEX_RULES.has(rule.name)) continue;
|
|
8423
9268
|
if (!evaluateSmartConditions(input, rule)) continue;
|
|
8424
9269
|
const inputPreview = preview(input, 120);
|
|
8425
9270
|
const isDupe = result.findings.some(
|
|
@@ -8637,12 +9482,27 @@ function scanCodexHistory(startDate, onProgress, onLine) {
|
|
|
8637
9482
|
});
|
|
8638
9483
|
}
|
|
8639
9484
|
}
|
|
8640
|
-
let
|
|
9485
|
+
let astFsMatched = false;
|
|
9486
|
+
const astRanForBash = toolNameLower === "exec_command" || toolNameLower === "bash";
|
|
9487
|
+
if (astRanForBash) {
|
|
9488
|
+
astFsMatched = pushFsOpAstFinding(
|
|
9489
|
+
String(input["command"] ?? ""),
|
|
9490
|
+
toolName,
|
|
9491
|
+
input,
|
|
9492
|
+
ts,
|
|
9493
|
+
projLabel,
|
|
9494
|
+
sessionId,
|
|
9495
|
+
"codex",
|
|
9496
|
+
result
|
|
9497
|
+
);
|
|
9498
|
+
}
|
|
9499
|
+
let ruleMatched = astFsMatched;
|
|
8641
9500
|
for (const source of ruleSources) {
|
|
8642
9501
|
const { rule } = source;
|
|
8643
9502
|
if (rule.verdict === "allow") continue;
|
|
8644
9503
|
if (rule.tool && !matchesPattern(toolNameLower === "exec_command" ? "bash" : toolNameLower, rule.tool))
|
|
8645
9504
|
continue;
|
|
9505
|
+
if (astRanForBash && rule.name && AST_FS_REGEX_RULES.has(rule.name)) continue;
|
|
8646
9506
|
if (!evaluateSmartConditions(input, rule)) continue;
|
|
8647
9507
|
const inputPreview = preview(input, 120);
|
|
8648
9508
|
const isDupe = result.findings.some(
|
|
@@ -8842,15 +9702,22 @@ function renderCompactScorecard(input) {
|
|
|
8842
9702
|
chalk4.red("\u{1F6D1} ") + chalk4.red.bold(String(blockedCount).padEnd(4)) + chalk4.dim("would have blocked".padEnd(20)) + chalk4.dim(`(${topBlocked})`)
|
|
8843
9703
|
);
|
|
8844
9704
|
}
|
|
8845
|
-
|
|
8846
|
-
|
|
9705
|
+
const realLoops = scan.loopFindings.filter((l) => l.kind !== "long-iteration");
|
|
9706
|
+
const longIterations = scan.loopFindings.filter((l) => l.kind === "long-iteration");
|
|
9707
|
+
if (realLoops.length > 0) {
|
|
9708
|
+
const wastedCalls = realLoops.reduce((s, l) => s + Math.max(0, l.count - 1), 0);
|
|
8847
9709
|
const wastePct = scan.totalToolCalls > 0 ? Math.round(wastedCalls / scan.totalToolCalls * 100) : 0;
|
|
8848
9710
|
const wasteParts = [];
|
|
8849
9711
|
if (wastePct > 0) wasteParts.push(`${wastePct}% wasted`);
|
|
8850
9712
|
if (summary.loopWastedUSD > 0) wasteParts.push("~" + fmtCost(summary.loopWastedUSD));
|
|
8851
9713
|
const wasteSummary = wasteParts.length ? `(${wasteParts.join(" \xB7 ")})` : "";
|
|
8852
9714
|
console.log(
|
|
8853
|
-
chalk4.yellow("\u{1F501} ") + chalk4.yellow.bold(String(
|
|
9715
|
+
chalk4.yellow("\u{1F501} ") + chalk4.yellow.bold(String(realLoops.length).padEnd(4)) + chalk4.dim("agent loops".padEnd(20)) + chalk4.dim(wasteSummary)
|
|
9716
|
+
);
|
|
9717
|
+
}
|
|
9718
|
+
if (longIterations.length > 0) {
|
|
9719
|
+
console.log(
|
|
9720
|
+
chalk4.dim("\u{1F4C2} ") + chalk4.dim.bold(String(longIterations.length).padEnd(4)) + chalk4.dim("long iterations".padEnd(20)) + chalk4.dim("(deep work \u2014 not waste)")
|
|
8854
9721
|
);
|
|
8855
9722
|
}
|
|
8856
9723
|
if (reviewCount > 0) {
|
|
@@ -9003,7 +9870,7 @@ function renderNarrativeScorecard(input) {
|
|
|
9003
9870
|
console.log("");
|
|
9004
9871
|
}
|
|
9005
9872
|
function registerScanCommand(program2) {
|
|
9006
|
-
program2.command("scan").description("Forecast: scan agent history and show what node9 would catch if installed").option("--all", "Scan all history (default: last
|
|
9873
|
+
program2.command("scan").description("Forecast: scan agent history and show what node9 would catch if installed").option("--all", "Scan all history (default: last 90 days)").option("--days <n>", "Scan last N days of history", "90").option("--top <n>", "Max findings to show per rule (default: 5)", "5").option("--drill-down", "Show all findings with full commands and session IDs").option("--compact", "Compact one-screen scorecard \u2014 for screenshots and sharing").option("--narrative", "Severity-grouped report \u2014 for video / dramatic sharing").option(
|
|
9007
9874
|
"--upload-history",
|
|
9008
9875
|
"Upload aggregate counts from existing JSONL sessions to the SaaS dashboard. Defaults to last 3 months; override with --since. Idempotent (safe to re-run)."
|
|
9009
9876
|
).option(
|
|
@@ -9022,7 +9889,7 @@ function registerScanCommand(program2) {
|
|
|
9022
9889
|
const previewWidth = 70;
|
|
9023
9890
|
const startDate = options.all ? null : (() => {
|
|
9024
9891
|
const d = /* @__PURE__ */ new Date();
|
|
9025
|
-
d.setDate(d.getDate() - (parseInt(options.days, 10) ||
|
|
9892
|
+
d.setDate(d.getDate() - (parseInt(options.days, 10) || 90));
|
|
9026
9893
|
d.setHours(0, 0, 0, 0);
|
|
9027
9894
|
return d;
|
|
9028
9895
|
})();
|
|
@@ -9096,7 +9963,7 @@ function registerScanCommand(program2) {
|
|
|
9096
9963
|
);
|
|
9097
9964
|
return;
|
|
9098
9965
|
}
|
|
9099
|
-
const rangeLabel = options.all ? chalk4.dim("all time") : chalk4.dim(`last ${options.days ??
|
|
9966
|
+
const rangeLabel = options.all ? chalk4.dim("all time") : chalk4.dim(`last ${options.days ?? 90} days`);
|
|
9100
9967
|
const dateRange = scan.firstDate && scan.lastDate ? chalk4.dim(` ${fmtTs(scan.firstDate)} \u2013 ${fmtTs(scan.lastDate)}`) : "";
|
|
9101
9968
|
const breakdownParts = [];
|
|
9102
9969
|
if (claudeScan.sessions > 0)
|
|
@@ -9382,13 +10249,14 @@ function registerScanCommand(program2) {
|
|
|
9382
10249
|
}
|
|
9383
10250
|
);
|
|
9384
10251
|
}
|
|
9385
|
-
var CLAUDE_PRICING, GEMINI_PRICING, CODE_EXTENSIONS,
|
|
10252
|
+
var CLAUDE_PRICING, GEMINI_PRICING, CODE_EXTENSIONS, SELF_OUTPUT_MARKERS, FIXTURE_TOKEN_PATTERNS, TERMINAL_ESCAPE_RE2, LOOP_TOOLS, LOOP_THRESHOLD, LOOP_TIMESPAN_THRESHOLD_MS, STUCK_TOOLS_MIN_WASTE, STUCK_TOOLS_LIMIT, RECURRING_SESSION_THRESHOLD, STALE_AGE_DAYS, DEFAULT_RULE_NAMES, classifyRuleSeverity2, narrativeRuleLabel2;
|
|
9386
10253
|
var init_scan = __esm({
|
|
9387
10254
|
"src/cli/commands/scan.ts"() {
|
|
9388
10255
|
"use strict";
|
|
9389
10256
|
init_shields();
|
|
9390
10257
|
init_config();
|
|
9391
10258
|
init_policy();
|
|
10259
|
+
init_dist();
|
|
9392
10260
|
init_dlp();
|
|
9393
10261
|
init_dist();
|
|
9394
10262
|
init_scan_summary();
|
|
@@ -9441,7 +10309,23 @@ var init_scan = __esm({
|
|
|
9441
10309
|
".vue",
|
|
9442
10310
|
".svelte"
|
|
9443
10311
|
]);
|
|
9444
|
-
|
|
10312
|
+
SELF_OUTPUT_MARKERS = [
|
|
10313
|
+
/redactedSample:\s*['"]/,
|
|
10314
|
+
/patternName:\s*['"]/,
|
|
10315
|
+
/\bseverity:\s*['"](?:block|review|allow)['"]/,
|
|
10316
|
+
/NODE9 SECURITY ALERT/
|
|
10317
|
+
];
|
|
10318
|
+
FIXTURE_TOKEN_PATTERNS = [
|
|
10319
|
+
/(.)\1{5,}/,
|
|
10320
|
+
// 6+ repeated characters (aaaaaa, 000000)
|
|
10321
|
+
/(?:EXAMPLE|FAKE|DUMMY|PLACEHOLDER|XXXXX)/i,
|
|
10322
|
+
/abcdefghijklmn/i,
|
|
10323
|
+
// long alpha sequence — fixture, not entropy
|
|
10324
|
+
/1234567890/,
|
|
10325
|
+
// long digit sequence — fixture, not entropy
|
|
10326
|
+
/qwerty/i
|
|
10327
|
+
];
|
|
10328
|
+
TERMINAL_ESCAPE_RE2 = /\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[@-_]|[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g;
|
|
9445
10329
|
LOOP_TOOLS = /* @__PURE__ */ new Set([
|
|
9446
10330
|
"bash",
|
|
9447
10331
|
"execute_bash",
|
|
@@ -9453,6 +10337,7 @@ var init_scan = __esm({
|
|
|
9453
10337
|
"multiedit"
|
|
9454
10338
|
]);
|
|
9455
10339
|
LOOP_THRESHOLD = 3;
|
|
10340
|
+
LOOP_TIMESPAN_THRESHOLD_MS = 10 * 60 * 1e3;
|
|
9456
10341
|
STUCK_TOOLS_MIN_WASTE = 5;
|
|
9457
10342
|
STUCK_TOOLS_LIMIT = 3;
|
|
9458
10343
|
RECURRING_SESSION_THRESHOLD = 3;
|
|
@@ -9984,7 +10869,86 @@ function abandonPending() {
|
|
|
9984
10869
|
}, 200);
|
|
9985
10870
|
}
|
|
9986
10871
|
}
|
|
10872
|
+
function logActivitySocket(msg) {
|
|
10873
|
+
try {
|
|
10874
|
+
fs20.appendFileSync(
|
|
10875
|
+
path22.join(homeDir, ".node9", "hook-debug.log"),
|
|
10876
|
+
`[${(/* @__PURE__ */ new Date()).toISOString()}] [activity-socket] ${msg}
|
|
10877
|
+
`
|
|
10878
|
+
);
|
|
10879
|
+
} catch {
|
|
10880
|
+
}
|
|
10881
|
+
}
|
|
10882
|
+
function shouldRebind(now = Date.now()) {
|
|
10883
|
+
if (activityCircuitTripped) return false;
|
|
10884
|
+
activityRebindAttempts = activityRebindAttempts.filter(
|
|
10885
|
+
(t) => now - t < ACTIVITY_REBIND_WINDOW_MS
|
|
10886
|
+
);
|
|
10887
|
+
activityRebindAttempts.push(now);
|
|
10888
|
+
if (activityRebindAttempts.length > ACTIVITY_REBIND_MAX_ATTEMPTS) {
|
|
10889
|
+
activityCircuitTripped = true;
|
|
10890
|
+
return false;
|
|
10891
|
+
}
|
|
10892
|
+
return true;
|
|
10893
|
+
}
|
|
9987
10894
|
function startActivitySocket() {
|
|
10895
|
+
bindActivitySocket();
|
|
10896
|
+
if (process.platform !== "win32") {
|
|
10897
|
+
try {
|
|
10898
|
+
activitySocketWatcher = fs20.watch(os18.tmpdir(), (eventType, filename) => {
|
|
10899
|
+
if (filename !== path22.basename(ACTIVITY_SOCKET_PATH2)) return;
|
|
10900
|
+
if (eventType !== "rename") return;
|
|
10901
|
+
if (fs20.existsSync(ACTIVITY_SOCKET_PATH2)) return;
|
|
10902
|
+
attemptRebind("watch-unlink");
|
|
10903
|
+
});
|
|
10904
|
+
activitySocketWatcher.on("error", (err2) => {
|
|
10905
|
+
logActivitySocket(`watcher error: ${err2.message}`);
|
|
10906
|
+
});
|
|
10907
|
+
activitySocketWatcher.unref();
|
|
10908
|
+
} catch (err2) {
|
|
10909
|
+
logActivitySocket(`failed to start watcher: ${err2.message}`);
|
|
10910
|
+
}
|
|
10911
|
+
}
|
|
10912
|
+
activityHealthInterval = setInterval(() => {
|
|
10913
|
+
if (!fs20.existsSync(ACTIVITY_SOCKET_PATH2)) attemptRebind("health-probe");
|
|
10914
|
+
}, ACTIVITY_HEALTH_PROBE_MS);
|
|
10915
|
+
activityHealthInterval.unref();
|
|
10916
|
+
process.on("exit", () => {
|
|
10917
|
+
if (activitySocketWatcher) {
|
|
10918
|
+
try {
|
|
10919
|
+
activitySocketWatcher.close();
|
|
10920
|
+
} catch {
|
|
10921
|
+
}
|
|
10922
|
+
}
|
|
10923
|
+
if (activityHealthInterval) clearInterval(activityHealthInterval);
|
|
10924
|
+
try {
|
|
10925
|
+
fs20.unlinkSync(ACTIVITY_SOCKET_PATH2);
|
|
10926
|
+
} catch {
|
|
10927
|
+
}
|
|
10928
|
+
});
|
|
10929
|
+
}
|
|
10930
|
+
function attemptRebind(reason) {
|
|
10931
|
+
if (!shouldRebind()) {
|
|
10932
|
+
logActivitySocket(
|
|
10933
|
+
`circuit breaker tripped after ${ACTIVITY_REBIND_MAX_ATTEMPTS} attempts in ${ACTIVITY_REBIND_WINDOW_MS}ms \u2014 flight recorder down`
|
|
10934
|
+
);
|
|
10935
|
+
broadcast("flight-recorder-down", {
|
|
10936
|
+
reason: "rebind-loop",
|
|
10937
|
+
message: "Activity socket repeatedly disappearing \u2014 run: node9 daemon restart"
|
|
10938
|
+
});
|
|
10939
|
+
return;
|
|
10940
|
+
}
|
|
10941
|
+
logActivitySocket(`rebinding (reason: ${reason}, attempt ${activityRebindAttempts.length})`);
|
|
10942
|
+
if (activitySocketServer) {
|
|
10943
|
+
try {
|
|
10944
|
+
activitySocketServer.close();
|
|
10945
|
+
} catch {
|
|
10946
|
+
}
|
|
10947
|
+
activitySocketServer = null;
|
|
10948
|
+
}
|
|
10949
|
+
bindActivitySocket();
|
|
10950
|
+
}
|
|
10951
|
+
function bindActivitySocket() {
|
|
9988
10952
|
try {
|
|
9989
10953
|
fs20.unlinkSync(ACTIVITY_SOCKET_PATH2);
|
|
9990
10954
|
} catch {
|
|
@@ -10082,15 +11046,15 @@ function startActivitySocket() {
|
|
|
10082
11046
|
socket.on("error", () => {
|
|
10083
11047
|
});
|
|
10084
11048
|
});
|
|
10085
|
-
unixServer.
|
|
10086
|
-
|
|
10087
|
-
|
|
10088
|
-
|
|
10089
|
-
|
|
10090
|
-
}
|
|
11049
|
+
unixServer.on("error", (err2) => {
|
|
11050
|
+
logActivitySocket(`server error: ${err2.message}`);
|
|
11051
|
+
});
|
|
11052
|
+
unixServer.listen(ACTIVITY_SOCKET_PATH2, () => {
|
|
11053
|
+
logActivitySocket(`bound to ${ACTIVITY_SOCKET_PATH2}`);
|
|
10091
11054
|
});
|
|
11055
|
+
activitySocketServer = unixServer;
|
|
10092
11056
|
}
|
|
10093
|
-
var homeDir, DAEMON_PID_FILE, DECISIONS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, INSIGHT_COUNTS_FILE, pending, sseClients, suggestionTracker, taintStore, insightCounts, _abandonTimer, _hadBrowserClient, _daemonServer, daemonRejectionHandlerRegistered, AUTO_DENY_MS, TRUST_DURATIONS, autoStarted, ACTIVITY_SOCKET_PATH2, ACTIVITY_RING_SIZE, activityRing, LARGE_RESPONSE_RING_SIZE, largeResponseRing, cachedScanResult, cachedScanTs, SCAN_CACHE_TTL_MS, SECRET_KEY_RE, INPUT_PRICE_PER_1M, OUTPUT_PRICE_PER_1M, BYTES_PER_TOKEN, WRITE_TOOL_NAMES;
|
|
11057
|
+
var homeDir, DAEMON_PID_FILE, DECISIONS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, INSIGHT_COUNTS_FILE, pending, sseClients, suggestionTracker, taintStore, insightCounts, _abandonTimer, _hadBrowserClient, _daemonServer, daemonRejectionHandlerRegistered, AUTO_DENY_MS, TRUST_DURATIONS, autoStarted, ACTIVITY_SOCKET_PATH2, ACTIVITY_RING_SIZE, activityRing, LARGE_RESPONSE_RING_SIZE, largeResponseRing, cachedScanResult, cachedScanTs, SCAN_CACHE_TTL_MS, SECRET_KEY_RE, INPUT_PRICE_PER_1M, OUTPUT_PRICE_PER_1M, BYTES_PER_TOKEN, WRITE_TOOL_NAMES, ACTIVITY_REBIND_MAX_ATTEMPTS, ACTIVITY_REBIND_WINDOW_MS, ACTIVITY_HEALTH_PROBE_MS, activitySocketServer, activitySocketWatcher, activityHealthInterval, activityRebindAttempts, activityCircuitTripped;
|
|
10094
11058
|
var init_state2 = __esm({
|
|
10095
11059
|
"src/daemon/state.ts"() {
|
|
10096
11060
|
"use strict";
|
|
@@ -10146,6 +11110,14 @@ var init_state2 = __esm({
|
|
|
10146
11110
|
"notebook_edit",
|
|
10147
11111
|
"notebookedit"
|
|
10148
11112
|
]);
|
|
11113
|
+
ACTIVITY_REBIND_MAX_ATTEMPTS = 5;
|
|
11114
|
+
ACTIVITY_REBIND_WINDOW_MS = 6e4;
|
|
11115
|
+
ACTIVITY_HEALTH_PROBE_MS = 3e4;
|
|
11116
|
+
activitySocketServer = null;
|
|
11117
|
+
activitySocketWatcher = null;
|
|
11118
|
+
activityHealthInterval = null;
|
|
11119
|
+
activityRebindAttempts = [];
|
|
11120
|
+
activityCircuitTripped = false;
|
|
10149
11121
|
}
|
|
10150
11122
|
});
|
|
10151
11123
|
|
|
@@ -10352,16 +11324,19 @@ async function pushBlastSnapshot(creds) {
|
|
|
10352
11324
|
async function pushScanSnapshot(creds) {
|
|
10353
11325
|
try {
|
|
10354
11326
|
const tick = await tickScanWatcher();
|
|
11327
|
+
if (tick.schemaFuture) return;
|
|
10355
11328
|
if (tick.findings.length === 0 && tick.totalToolCalls === 0) {
|
|
10356
11329
|
return;
|
|
10357
11330
|
}
|
|
10358
11331
|
const summary = summarizeScan(tick.findings, {
|
|
10359
11332
|
totalToolCalls: tick.totalToolCalls
|
|
10360
11333
|
});
|
|
10361
|
-
const
|
|
11334
|
+
const perSession = buildSessionDeltas(tick.findings, tick.toolCallsBySession);
|
|
10362
11335
|
const scanUrl = creds.apiUrl.endsWith("/policies/sync") ? creds.apiUrl.replace(/\/policies\/sync$/, "/scan/report") : null;
|
|
10363
11336
|
if (!scanUrl) return;
|
|
11337
|
+
const body = tick.uploadAs === "totals" ? { ...summary, sessionTotals: perSession, extractorVersion: CANONICAL_EXTRACTOR_VERSION } : { ...summary, sessionDeltas: perSession, extractorVersion: CANONICAL_EXTRACTOR_VERSION };
|
|
10364
11338
|
const parsed = new URL(scanUrl);
|
|
11339
|
+
let posted = false;
|
|
10365
11340
|
await new Promise((resolve) => {
|
|
10366
11341
|
const req = https2.request(
|
|
10367
11342
|
{
|
|
@@ -10376,6 +11351,9 @@ async function pushScanSnapshot(creds) {
|
|
|
10376
11351
|
timeout: 1e4
|
|
10377
11352
|
},
|
|
10378
11353
|
(res) => {
|
|
11354
|
+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
|
11355
|
+
posted = true;
|
|
11356
|
+
}
|
|
10379
11357
|
res.resume();
|
|
10380
11358
|
res.on("end", resolve);
|
|
10381
11359
|
res.on("error", () => resolve());
|
|
@@ -10386,9 +11364,12 @@ async function pushScanSnapshot(creds) {
|
|
|
10386
11364
|
req.destroy();
|
|
10387
11365
|
resolve();
|
|
10388
11366
|
});
|
|
10389
|
-
req.write(JSON.stringify(
|
|
11367
|
+
req.write(JSON.stringify(body));
|
|
10390
11368
|
req.end();
|
|
10391
11369
|
});
|
|
11370
|
+
if (posted && tick.uploadAs === "totals") {
|
|
11371
|
+
markUploadComplete();
|
|
11372
|
+
}
|
|
10392
11373
|
} catch {
|
|
10393
11374
|
}
|
|
10394
11375
|
}
|
|
@@ -12292,6 +13273,8 @@ async function startTail(options = {}) {
|
|
|
12292
13273
|
let initialReplayDone = false;
|
|
12293
13274
|
const activityPending = /* @__PURE__ */ new Map();
|
|
12294
13275
|
const orphanedResults = /* @__PURE__ */ new Map();
|
|
13276
|
+
let lastActivityFromDaemon = Date.now();
|
|
13277
|
+
let stallWarned = false;
|
|
12295
13278
|
const authToken = getInternalToken() ?? "";
|
|
12296
13279
|
const approvalQueue = [];
|
|
12297
13280
|
let cardActive = false;
|
|
@@ -12518,6 +13501,24 @@ async function startTail(options = {}) {
|
|
|
12518
13501
|
console.log(chalk29.dim("\n\u{1F6F0}\uFE0F Disconnected."));
|
|
12519
13502
|
process.exit(0);
|
|
12520
13503
|
});
|
|
13504
|
+
const STALL_THRESHOLD_MS = 6e4;
|
|
13505
|
+
const stallWatchdog = setInterval(() => {
|
|
13506
|
+
if (stallWarned) return;
|
|
13507
|
+
if (Date.now() - lastActivityFromDaemon < STALL_THRESHOLD_MS) return;
|
|
13508
|
+
try {
|
|
13509
|
+
const auditMtime = fs44.statSync(auditLog).mtimeMs;
|
|
13510
|
+
if (Date.now() - auditMtime >= STALL_THRESHOLD_MS) return;
|
|
13511
|
+
console.log("");
|
|
13512
|
+
console.log(
|
|
13513
|
+
chalk29.yellow(
|
|
13514
|
+
"\u26A0\uFE0F Tail appears stalled \u2014 hooks are firing but no events are arriving. Try: node9 daemon restart"
|
|
13515
|
+
)
|
|
13516
|
+
);
|
|
13517
|
+
stallWarned = true;
|
|
13518
|
+
} catch {
|
|
13519
|
+
}
|
|
13520
|
+
}, STALL_THRESHOLD_MS / 2);
|
|
13521
|
+
stallWatchdog.unref();
|
|
12521
13522
|
const sseUrl = `http://127.0.0.1:${port}/events?capabilities=input`;
|
|
12522
13523
|
const req = http2.get(
|
|
12523
13524
|
sseUrl,
|
|
@@ -12563,7 +13564,18 @@ async function startTail(options = {}) {
|
|
|
12563
13564
|
}
|
|
12564
13565
|
);
|
|
12565
13566
|
function handleMessage(event, rawData) {
|
|
13567
|
+
lastActivityFromDaemon = Date.now();
|
|
12566
13568
|
if (event === "csrf") return;
|
|
13569
|
+
if (event === "flight-recorder-down") {
|
|
13570
|
+
try {
|
|
13571
|
+
const parsed = JSON.parse(rawData);
|
|
13572
|
+
const msg = parsed.message ?? "Flight recorder is down \u2014 run: node9 daemon restart";
|
|
13573
|
+
console.log("");
|
|
13574
|
+
console.log(chalk29.bgRed.white.bold(` \u26A0\uFE0F ${msg} `));
|
|
13575
|
+
} catch {
|
|
13576
|
+
}
|
|
13577
|
+
return;
|
|
13578
|
+
}
|
|
12567
13579
|
if (event === "init") {
|
|
12568
13580
|
try {
|
|
12569
13581
|
const parsed = JSON.parse(rawData);
|
|
@@ -16139,7 +17151,6 @@ function registerInitCommand(program2) {
|
|
|
16139
17151
|
console.log(chalk15.green.bold(`\u{1F6E1}\uFE0F Node9 is protecting ${agentList}!`));
|
|
16140
17152
|
console.log("");
|
|
16141
17153
|
console.log(chalk15.white(" Watch live: ") + chalk15.cyan("node9 tail"));
|
|
16142
|
-
console.log(chalk15.white(" Local UI: ") + chalk15.cyan("node9 daemon --openui"));
|
|
16143
17154
|
console.log("");
|
|
16144
17155
|
console.log(chalk15.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"));
|
|
16145
17156
|
console.log(
|