@node9/proxy 1.19.2 → 1.19.4
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 +200 -112
- package/dist/cli.mjs +168 -80
- package/dist/dashboard.mjs +4651 -0
- package/dist/index.js +21 -23
- package/dist/index.mjs +17 -19
- package/package.json +4 -1
|
@@ -0,0 +1,4651 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res) => function __init() {
|
|
4
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
+
};
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// src/tui/dashboard/types.ts
|
|
12
|
+
function windowStartMs(window, openedAt) {
|
|
13
|
+
if (window === "now") return openedAt;
|
|
14
|
+
const day = 864e5;
|
|
15
|
+
switch (window) {
|
|
16
|
+
case "1d":
|
|
17
|
+
return Date.now() - day;
|
|
18
|
+
case "7d":
|
|
19
|
+
return Date.now() - 7 * day;
|
|
20
|
+
case "30d":
|
|
21
|
+
return Date.now() - 30 * day;
|
|
22
|
+
case "60d":
|
|
23
|
+
return Date.now() - 60 * day;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
var EMPTY_SESSION_FORENSIC;
|
|
27
|
+
var init_types = __esm({
|
|
28
|
+
"src/tui/dashboard/types.ts"() {
|
|
29
|
+
"use strict";
|
|
30
|
+
EMPTY_SESSION_FORENSIC = {
|
|
31
|
+
pii: 0,
|
|
32
|
+
sensitiveFileRead: 0,
|
|
33
|
+
privilegeEscalation: 0,
|
|
34
|
+
destructiveOp: 0,
|
|
35
|
+
pipeToShell: 0,
|
|
36
|
+
evalOfRemote: 0,
|
|
37
|
+
longOutputRedacted: 0
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// packages/policy-engine/dist/index.mjs
|
|
43
|
+
import safeRegex from "safe-regex2";
|
|
44
|
+
import mvdanSh from "mvdan-sh";
|
|
45
|
+
import pm from "picomatch";
|
|
46
|
+
import safeRegex2 from "safe-regex2";
|
|
47
|
+
import safeRegex3 from "safe-regex2";
|
|
48
|
+
function isAssignmentContext(text) {
|
|
49
|
+
return ASSIGNMENT_CONTEXT_RE.test(text);
|
|
50
|
+
}
|
|
51
|
+
function shannonEntropy(s) {
|
|
52
|
+
if (s.length === 0) return 0;
|
|
53
|
+
const freq = /* @__PURE__ */ new Map();
|
|
54
|
+
for (const ch of s) freq.set(ch, (freq.get(ch) ?? 0) + 1);
|
|
55
|
+
let h = 0;
|
|
56
|
+
for (const count of freq.values()) {
|
|
57
|
+
const p = count / s.length;
|
|
58
|
+
h -= p * Math.log2(p);
|
|
59
|
+
}
|
|
60
|
+
return h;
|
|
61
|
+
}
|
|
62
|
+
function assertBuiltinPatternsAreSafe() {
|
|
63
|
+
for (const p of DLP_PATTERNS) {
|
|
64
|
+
if (!safeRegex(p.regex.source)) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`[node9 engine] Builtin DLP pattern '${p.name}' is vulnerable to ReDoS: ${p.regex.source}`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
for (const re of SENSITIVE_PATH_PATTERNS) {
|
|
71
|
+
if (!safeRegex(re.source)) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`[node9 engine] Builtin sensitive-path pattern is vulnerable to ReDoS: ${re.source}`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function maskSecret(raw, pattern) {
|
|
79
|
+
const match = raw.match(pattern);
|
|
80
|
+
if (!match) return "****";
|
|
81
|
+
const secret = match[0];
|
|
82
|
+
if (secret.length < 8) return "****";
|
|
83
|
+
const prefix = secret.slice(0, 4);
|
|
84
|
+
const suffix = secret.slice(-4);
|
|
85
|
+
const stars = "*".repeat(Math.min(secret.length - 8, 12));
|
|
86
|
+
return `${prefix}${stars}${suffix}`;
|
|
87
|
+
}
|
|
88
|
+
function scanArgs(args, depth = 0, fieldPath = "args") {
|
|
89
|
+
if (depth > MAX_DEPTH || args === null || args === void 0) return null;
|
|
90
|
+
if (Array.isArray(args)) {
|
|
91
|
+
for (let i = 0; i < args.length; i++) {
|
|
92
|
+
const match = scanArgs(args[i], depth + 1, `${fieldPath}[${i}]`);
|
|
93
|
+
if (match) return match;
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
if (typeof args === "object") {
|
|
98
|
+
for (const [key, value] of Object.entries(args)) {
|
|
99
|
+
const match = scanArgs(value, depth + 1, `${fieldPath}.${key}`);
|
|
100
|
+
if (match) return match;
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
if (typeof args === "string") {
|
|
105
|
+
const text = args.length > MAX_STRING_BYTES ? args.slice(0, MAX_STRING_BYTES) : args;
|
|
106
|
+
const textLower = text.toLowerCase();
|
|
107
|
+
const assignmentCtx = isAssignmentContext(text);
|
|
108
|
+
for (const pattern of DLP_PATTERNS) {
|
|
109
|
+
if (pattern.keywords && !pattern.keywords.some((kw) => textLower.includes(kw.toLowerCase()))) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (pattern.regex.test(text)) {
|
|
113
|
+
const raw = text.match(pattern.regex)?.[0] ?? "";
|
|
114
|
+
if (DLP_STOPWORDS.some((sw) => raw.toLowerCase().includes(sw))) continue;
|
|
115
|
+
if (pattern.minEntropy !== void 0 && shannonEntropy(raw) < pattern.minEntropy) continue;
|
|
116
|
+
const severity = pattern.contextBoost && assignmentCtx ? "block" : pattern.severity;
|
|
117
|
+
return {
|
|
118
|
+
patternName: pattern.name,
|
|
119
|
+
fieldPath,
|
|
120
|
+
redactedSample: maskSecret(text, pattern.regex),
|
|
121
|
+
severity
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (text.length < MAX_JSON_PARSE_BYTES) {
|
|
126
|
+
const trimmed = text.trim();
|
|
127
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
128
|
+
try {
|
|
129
|
+
const parsed = JSON.parse(text);
|
|
130
|
+
const inner = scanArgs(parsed, depth + 1, fieldPath);
|
|
131
|
+
if (inner) return inner;
|
|
132
|
+
} catch {
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
function isCatHeredocOrLit(part) {
|
|
140
|
+
if (!part) return false;
|
|
141
|
+
const t = syntax.NodeType(part);
|
|
142
|
+
if (t === "Lit") return true;
|
|
143
|
+
if (t !== "CmdSubst") return false;
|
|
144
|
+
const stmts = part.Stmts || [];
|
|
145
|
+
if (stmts.length !== 1) return false;
|
|
146
|
+
const stmt = stmts[0];
|
|
147
|
+
const redirs = stmt.Redirs || stmt.Cmd?.Redirs || [];
|
|
148
|
+
const hasHeredoc = redirs.some((r) => r && r.Hdoc);
|
|
149
|
+
if (!hasHeredoc) return false;
|
|
150
|
+
const cmd = stmt.Cmd;
|
|
151
|
+
if (!cmd || syntax.NodeType(cmd) !== "CallExpr") return false;
|
|
152
|
+
const firstArg = cmd.Args?.[0]?.Parts || [];
|
|
153
|
+
if (firstArg.length !== 1 || syntax.NodeType(firstArg[0]) !== "Lit") return false;
|
|
154
|
+
return (firstArg[0].Value || "").toLowerCase() === "cat";
|
|
155
|
+
}
|
|
156
|
+
function parseShared(command) {
|
|
157
|
+
const cached = astCache.get(command);
|
|
158
|
+
if (cached !== void 0) {
|
|
159
|
+
astCache.delete(command);
|
|
160
|
+
astCache.set(command, cached);
|
|
161
|
+
return cached;
|
|
162
|
+
}
|
|
163
|
+
let parsed;
|
|
164
|
+
try {
|
|
165
|
+
parsed = sharedParser.Parse(command, "cmd");
|
|
166
|
+
} catch {
|
|
167
|
+
parsed = PARSE_FAIL;
|
|
168
|
+
}
|
|
169
|
+
if (astCache.size >= AST_CACHE_MAX) {
|
|
170
|
+
const oldest = astCache.keys().next().value;
|
|
171
|
+
if (oldest !== void 0) astCache.delete(oldest);
|
|
172
|
+
}
|
|
173
|
+
astCache.set(command, parsed);
|
|
174
|
+
return parsed;
|
|
175
|
+
}
|
|
176
|
+
function cachedNormalize(command, compute) {
|
|
177
|
+
const hit = normalizeCache.get(command);
|
|
178
|
+
if (hit !== void 0) {
|
|
179
|
+
normalizeCache.delete(command);
|
|
180
|
+
normalizeCache.set(command, hit);
|
|
181
|
+
return hit;
|
|
182
|
+
}
|
|
183
|
+
const result = compute();
|
|
184
|
+
if (normalizeCache.size >= NORMALIZE_CACHE_MAX) {
|
|
185
|
+
const oldest = normalizeCache.keys().next().value;
|
|
186
|
+
if (oldest !== void 0) normalizeCache.delete(oldest);
|
|
187
|
+
}
|
|
188
|
+
normalizeCache.set(command, result);
|
|
189
|
+
return result;
|
|
190
|
+
}
|
|
191
|
+
function normalizeCommandForPolicy(command) {
|
|
192
|
+
return cachedNormalize(command, () => normalizeCommandForPolicyImpl(command));
|
|
193
|
+
}
|
|
194
|
+
function normalizeCommandForPolicyImpl(command) {
|
|
195
|
+
const f = parseShared(command);
|
|
196
|
+
if (f === PARSE_FAIL) return command;
|
|
197
|
+
try {
|
|
198
|
+
const strips = [];
|
|
199
|
+
syntax.Walk(f, (node) => {
|
|
200
|
+
if (!node) return false;
|
|
201
|
+
const n = node;
|
|
202
|
+
if (syntax.NodeType(n) !== "CallExpr") return true;
|
|
203
|
+
const args = n.Args || [];
|
|
204
|
+
for (let i = 0; i < args.length - 1; i++) {
|
|
205
|
+
const argParts = args[i].Parts || [];
|
|
206
|
+
if (argParts.length !== 1 || syntax.NodeType(argParts[0]) !== "Lit") continue;
|
|
207
|
+
const flagVal = argParts[0].Value || "";
|
|
208
|
+
if (!MESSAGE_FLAGS.has(flagVal.toLowerCase())) continue;
|
|
209
|
+
const next = args[i + 1];
|
|
210
|
+
const nextParts = next.Parts || [];
|
|
211
|
+
if (nextParts.length !== 1) continue;
|
|
212
|
+
const quotedNode = nextParts[0];
|
|
213
|
+
const nt = syntax.NodeType(quotedNode);
|
|
214
|
+
if (nt === "SglQuoted") {
|
|
215
|
+
strips.push([next.Pos().Offset(), next.End().Offset()]);
|
|
216
|
+
} else if (nt === "DblQuoted") {
|
|
217
|
+
const innerParts = quotedNode.Parts || [];
|
|
218
|
+
const allLit = innerParts.length === 0 || innerParts.every((p) => syntax.NodeType(p) === "Lit");
|
|
219
|
+
if (allLit) {
|
|
220
|
+
strips.push([next.Pos().Offset(), next.End().Offset()]);
|
|
221
|
+
} else if (innerParts.every((p) => isCatHeredocOrLit(p))) {
|
|
222
|
+
strips.push([next.Pos().Offset(), next.End().Offset()]);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return true;
|
|
227
|
+
});
|
|
228
|
+
if (strips.length === 0) return command;
|
|
229
|
+
strips.sort((a, b) => b[0] - a[0]);
|
|
230
|
+
let result = command;
|
|
231
|
+
for (const [start, end] of strips) {
|
|
232
|
+
result = result.slice(0, start) + '""' + result.slice(end);
|
|
233
|
+
}
|
|
234
|
+
return result;
|
|
235
|
+
} catch {
|
|
236
|
+
return command;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
function scanArgsForDynamicExec(args, startIdx) {
|
|
240
|
+
let hasCmdSubst = false;
|
|
241
|
+
let hasParamExp = false;
|
|
242
|
+
let hasCurl = false;
|
|
243
|
+
for (let i = startIdx; i < args.length; i++) {
|
|
244
|
+
syntax.Walk(args[i], (inner) => {
|
|
245
|
+
if (!inner) return false;
|
|
246
|
+
const inn = inner;
|
|
247
|
+
const it = syntax.NodeType(inn);
|
|
248
|
+
if (it === "CmdSubst") hasCmdSubst = true;
|
|
249
|
+
if (it === "ParamExp") hasParamExp = true;
|
|
250
|
+
if (it === "Lit" && DOWNLOAD_CMDS.has(inn.Value?.toLowerCase())) hasCurl = true;
|
|
251
|
+
return true;
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
if (hasCmdSubst && hasCurl) return "block";
|
|
255
|
+
if (hasCmdSubst || hasParamExp) return "review";
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
function detectDangerousShellExec(command) {
|
|
259
|
+
try {
|
|
260
|
+
const f = sharedParser.Parse(command, "cmd");
|
|
261
|
+
let result = null;
|
|
262
|
+
syntax.Walk(f, (node) => {
|
|
263
|
+
if (!node || result === "block") return false;
|
|
264
|
+
const n = node;
|
|
265
|
+
if (syntax.NodeType(n) !== "CallExpr") return true;
|
|
266
|
+
const args = n.Args || [];
|
|
267
|
+
if (args.length === 0) return true;
|
|
268
|
+
const firstParts = args[0].Parts || [];
|
|
269
|
+
if (firstParts.length !== 1 || syntax.NodeType(firstParts[0]) !== "Lit") return true;
|
|
270
|
+
const cmdName = firstParts[0].Value?.toLowerCase() ?? "";
|
|
271
|
+
if (cmdName === "eval") {
|
|
272
|
+
const v = scanArgsForDynamicExec(args, 1);
|
|
273
|
+
if (v === "block" || v === "review" && result === null) result = v;
|
|
274
|
+
} else if (SHELL_INTERPRETERS.has(cmdName)) {
|
|
275
|
+
for (let i = 1; i < args.length - 1; i++) {
|
|
276
|
+
const flagParts = args[i].Parts || [];
|
|
277
|
+
if (flagParts.length !== 1 || syntax.NodeType(flagParts[0]) !== "Lit" || flagParts[0].Value !== "-c")
|
|
278
|
+
continue;
|
|
279
|
+
const v = scanArgsForDynamicExec(args, i + 1);
|
|
280
|
+
if (v === "block" || v === "review" && result === null) result = v;
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return true;
|
|
285
|
+
});
|
|
286
|
+
return result;
|
|
287
|
+
} catch {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
function isBashTool(toolName) {
|
|
292
|
+
return BASH_TOOL_NAMES.has(toolName.toLowerCase());
|
|
293
|
+
}
|
|
294
|
+
function isProtectedHomePath(rawPath) {
|
|
295
|
+
let p = rawPath.replace(/^\$HOME[\\/]?|^\$\{HOME\}[\\/]?/, "~/");
|
|
296
|
+
let underHome = false;
|
|
297
|
+
if (p === "~" || p.startsWith("~/") || p.startsWith("~\\")) {
|
|
298
|
+
p = p.replace(/^~[\\/]?/, "");
|
|
299
|
+
underHome = true;
|
|
300
|
+
} else if (/^\/home\/[^/]+/.test(p) || /^\/root(\/|$)/.test(p)) {
|
|
301
|
+
p = p.replace(/^\/home\/[^/]+[\\/]?|^\/root[\\/]?/, "");
|
|
302
|
+
underHome = true;
|
|
303
|
+
}
|
|
304
|
+
if (!underHome) return false;
|
|
305
|
+
if (p === "" || p === "." || p === "./") return true;
|
|
306
|
+
for (const safe of HOME_CACHE_ALLOWLIST) {
|
|
307
|
+
if (p === safe || p.startsWith(safe + "/") || p.startsWith(safe + "\\")) {
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
function extractLiteralArgs(callExpr) {
|
|
314
|
+
const args = callExpr.Args || [];
|
|
315
|
+
if (args.length === 0) return { name: "", flags: [], paths: [] };
|
|
316
|
+
const litFromWord = (w) => {
|
|
317
|
+
const parts = w?.Parts || [];
|
|
318
|
+
let s = "";
|
|
319
|
+
for (const p of parts) {
|
|
320
|
+
const t = syntax.NodeType(p);
|
|
321
|
+
if (t === "Lit") s += (p.Value ?? "").replace(/\\(.)/g, "$1");
|
|
322
|
+
else if (t === "SglQuoted") s += p.Value ?? "";
|
|
323
|
+
else if (t === "DblQuoted") {
|
|
324
|
+
const inner = p.Parts || [];
|
|
325
|
+
if (!inner.every((ip) => syntax.NodeType(ip) === "Lit")) return null;
|
|
326
|
+
s += inner.map((ip) => ip.Value ?? "").join("");
|
|
327
|
+
} else {
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return s;
|
|
332
|
+
};
|
|
333
|
+
const name = (litFromWord(args[0]) || "").toLowerCase();
|
|
334
|
+
const flags = [];
|
|
335
|
+
const paths = [];
|
|
336
|
+
for (let i = 1; i < args.length; i++) {
|
|
337
|
+
const v = litFromWord(args[i]);
|
|
338
|
+
if (v === null) continue;
|
|
339
|
+
if (v.startsWith("-")) flags.push(v);
|
|
340
|
+
else paths.push(v);
|
|
341
|
+
}
|
|
342
|
+
return { name, flags, paths };
|
|
343
|
+
}
|
|
344
|
+
function analyzeFsOperation(command) {
|
|
345
|
+
if (!FS_OP_PRESCREEN_RE.test(command)) return null;
|
|
346
|
+
if (fsOpCache.has(command)) {
|
|
347
|
+
const hit = fsOpCache.get(command) ?? null;
|
|
348
|
+
fsOpCache.delete(command);
|
|
349
|
+
fsOpCache.set(command, hit);
|
|
350
|
+
return hit;
|
|
351
|
+
}
|
|
352
|
+
const computed = analyzeFsOperationImpl(command);
|
|
353
|
+
if (fsOpCache.size >= FS_OP_CACHE_MAX) {
|
|
354
|
+
const oldest = fsOpCache.keys().next().value;
|
|
355
|
+
if (oldest !== void 0) fsOpCache.delete(oldest);
|
|
356
|
+
}
|
|
357
|
+
fsOpCache.set(command, computed);
|
|
358
|
+
return computed;
|
|
359
|
+
}
|
|
360
|
+
function analyzeFsOperationImpl(command) {
|
|
361
|
+
const f = parseShared(command);
|
|
362
|
+
if (f === PARSE_FAIL) return null;
|
|
363
|
+
let result = null;
|
|
364
|
+
try {
|
|
365
|
+
syntax.Walk(f, (node) => {
|
|
366
|
+
if (!node || result) return false;
|
|
367
|
+
const n = node;
|
|
368
|
+
if (syntax.NodeType(n) !== "CallExpr") return true;
|
|
369
|
+
const { name, flags, paths } = extractLiteralArgs(n);
|
|
370
|
+
if (!name) return true;
|
|
371
|
+
if (name === "rm") {
|
|
372
|
+
const flagStr = flags.join("").toLowerCase();
|
|
373
|
+
const hasR = /[r]/.test(flagStr) || flags.includes("--recursive");
|
|
374
|
+
const hasF = /[f]/.test(flagStr) || flags.includes("--force");
|
|
375
|
+
if (hasR && hasF) {
|
|
376
|
+
for (const p of paths) {
|
|
377
|
+
if (isProtectedHomePath(p)) {
|
|
378
|
+
result = {
|
|
379
|
+
ruleName: "block-rm-rf-home",
|
|
380
|
+
verdict: "block",
|
|
381
|
+
reason: "Recursive delete of home directory is irreversible",
|
|
382
|
+
path: p
|
|
383
|
+
};
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
if (p === "/" || /^\/+$/.test(p)) {
|
|
387
|
+
result = {
|
|
388
|
+
ruleName: "block-rm-rf-home",
|
|
389
|
+
verdict: "block",
|
|
390
|
+
reason: "Recursive delete of root is catastrophic",
|
|
391
|
+
path: p
|
|
392
|
+
};
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
if (FS_READ_TOOLS.has(name)) {
|
|
399
|
+
for (const p of paths) {
|
|
400
|
+
for (const sp of SENSITIVE_PATH_RULES) {
|
|
401
|
+
if (sp.match(p)) {
|
|
402
|
+
result = { ruleName: sp.rule, verdict: "block", reason: sp.reason, path: p };
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return true;
|
|
409
|
+
});
|
|
410
|
+
return result;
|
|
411
|
+
} catch {
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
function analyzeShellCommand(command) {
|
|
416
|
+
const actions = [];
|
|
417
|
+
const paths = [];
|
|
418
|
+
const allTokens = [];
|
|
419
|
+
const addToken = (token) => {
|
|
420
|
+
const lower = token.toLowerCase();
|
|
421
|
+
allTokens.push(lower);
|
|
422
|
+
if (lower.includes("/")) allTokens.push(...lower.split("/").filter(Boolean));
|
|
423
|
+
if (lower.startsWith("-")) allTokens.push(lower.replace(/^-+/, ""));
|
|
424
|
+
};
|
|
425
|
+
try {
|
|
426
|
+
const f = sharedParser.Parse(command, "cmd");
|
|
427
|
+
syntax.Walk(f, (node) => {
|
|
428
|
+
if (!node) return false;
|
|
429
|
+
const n = node;
|
|
430
|
+
if (syntax.NodeType(n) !== "CallExpr") return true;
|
|
431
|
+
const wordValues = (n.Args || []).map((arg) => {
|
|
432
|
+
return (arg.Parts || []).map((p) => (p.Value ?? "").replace(/\\(.)/g, "$1")).join("");
|
|
433
|
+
}).filter((s) => s.length > 0);
|
|
434
|
+
if (wordValues.length > 0) {
|
|
435
|
+
const cmd = wordValues[0].toLowerCase();
|
|
436
|
+
if (!actions.includes(cmd)) actions.push(cmd);
|
|
437
|
+
wordValues.forEach((w) => addToken(w));
|
|
438
|
+
wordValues.slice(1).forEach((w) => {
|
|
439
|
+
if (!w.startsWith("-")) paths.push(w);
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
return true;
|
|
443
|
+
});
|
|
444
|
+
} catch {
|
|
445
|
+
}
|
|
446
|
+
if (allTokens.length === 0) {
|
|
447
|
+
const normalized = command.replace(/\\(.)/g, "$1");
|
|
448
|
+
const sanitized = normalized.replace(/["'<>]/g, " ");
|
|
449
|
+
const segments = sanitized.split(/[|;&]|\$\(|\)|`/);
|
|
450
|
+
segments.forEach((segment) => {
|
|
451
|
+
const tokens = segment.trim().split(/\s+/).filter(Boolean);
|
|
452
|
+
if (tokens.length > 0) {
|
|
453
|
+
const action = tokens[0].toLowerCase();
|
|
454
|
+
if (!actions.includes(action)) actions.push(action);
|
|
455
|
+
tokens.forEach((t) => {
|
|
456
|
+
addToken(t);
|
|
457
|
+
if (t !== tokens[0] && !t.startsWith("-")) {
|
|
458
|
+
if (!paths.includes(t)) paths.push(t);
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
return { actions, paths, allTokens };
|
|
465
|
+
}
|
|
466
|
+
function isSensitivePath(p) {
|
|
467
|
+
return SENSITIVE_PATTERNS.some((re) => re.test(p));
|
|
468
|
+
}
|
|
469
|
+
function splitOnPipe(cmd) {
|
|
470
|
+
const segments = [];
|
|
471
|
+
let current = "";
|
|
472
|
+
let inSingle = false;
|
|
473
|
+
let inDouble = false;
|
|
474
|
+
for (let i = 0; i < cmd.length; i++) {
|
|
475
|
+
const ch = cmd[i];
|
|
476
|
+
if (ch === "'" && !inDouble) {
|
|
477
|
+
inSingle = !inSingle;
|
|
478
|
+
current += ch;
|
|
479
|
+
} else if (ch === '"' && !inSingle) {
|
|
480
|
+
inDouble = !inDouble;
|
|
481
|
+
current += ch;
|
|
482
|
+
} else if (ch === "|" && !inSingle && !inDouble && cmd[i + 1] !== "|" && (i === 0 || cmd[i - 1] !== "|")) {
|
|
483
|
+
segments.push(current.trim());
|
|
484
|
+
current = "";
|
|
485
|
+
} else {
|
|
486
|
+
current += ch;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
if (current.trim()) segments.push(current.trim());
|
|
490
|
+
return segments.filter(Boolean);
|
|
491
|
+
}
|
|
492
|
+
function positionalTokens(segment) {
|
|
493
|
+
return segment.split(/\s+/).slice(1).filter((t) => !t.startsWith("-") && !t.startsWith("@") && t.length > 0);
|
|
494
|
+
}
|
|
495
|
+
function analyzePipeChain(command) {
|
|
496
|
+
const segments = splitOnPipe(command);
|
|
497
|
+
if (segments.length < 2) {
|
|
498
|
+
return {
|
|
499
|
+
isPipeline: false,
|
|
500
|
+
hasSensitiveSource: false,
|
|
501
|
+
hasExternalSink: false,
|
|
502
|
+
hasObfuscation: false,
|
|
503
|
+
sourceFiles: [],
|
|
504
|
+
sinkTargets: [],
|
|
505
|
+
risk: "none"
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
const sourceFiles = [];
|
|
509
|
+
const sinkTargets = [];
|
|
510
|
+
let hasSensitiveSource = false;
|
|
511
|
+
let hasExternalSink = false;
|
|
512
|
+
let hasObfuscation = false;
|
|
513
|
+
for (const segment of segments) {
|
|
514
|
+
const tokens = segment.split(/\s+/).filter(Boolean);
|
|
515
|
+
if (tokens.length === 0) continue;
|
|
516
|
+
const binary = tokens[0].toLowerCase();
|
|
517
|
+
const args = positionalTokens(segment);
|
|
518
|
+
if (SOURCE_COMMANDS.has(binary)) {
|
|
519
|
+
sourceFiles.push(...args);
|
|
520
|
+
if (args.some(isSensitivePath)) hasSensitiveSource = true;
|
|
521
|
+
}
|
|
522
|
+
if (OBFUSCATORS.has(binary)) hasObfuscation = true;
|
|
523
|
+
if (SINK_COMMANDS.has(binary)) {
|
|
524
|
+
const targets = args.filter(
|
|
525
|
+
(a) => a.includes(".") || a.includes("://") || /^\d+\.\d+/.test(a)
|
|
526
|
+
);
|
|
527
|
+
sinkTargets.push(...targets);
|
|
528
|
+
if (targets.length > 0) hasExternalSink = true;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
const fullCmd = command.toLowerCase();
|
|
532
|
+
if (!hasSensitiveSource) {
|
|
533
|
+
const redirMatch = fullCmd.match(/<\s*(\S+)/);
|
|
534
|
+
if (redirMatch && isSensitivePath(redirMatch[1])) {
|
|
535
|
+
hasSensitiveSource = true;
|
|
536
|
+
sourceFiles.push(redirMatch[1]);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
const risk = hasSensitiveSource && hasExternalSink && hasObfuscation ? "critical" : hasSensitiveSource && hasExternalSink ? "high" : hasExternalSink ? "medium" : "none";
|
|
540
|
+
return {
|
|
541
|
+
isPipeline: true,
|
|
542
|
+
hasSensitiveSource,
|
|
543
|
+
hasExternalSink,
|
|
544
|
+
hasObfuscation,
|
|
545
|
+
sourceFiles,
|
|
546
|
+
sinkTargets,
|
|
547
|
+
risk
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
function validateRegex(pattern) {
|
|
551
|
+
if (!pattern) return "Pattern is required";
|
|
552
|
+
if (pattern.length > MAX_REGEX_LENGTH) return `Pattern exceeds max length of ${MAX_REGEX_LENGTH}`;
|
|
553
|
+
try {
|
|
554
|
+
new RegExp(pattern);
|
|
555
|
+
} catch (e) {
|
|
556
|
+
return `Invalid regex syntax: ${e.message}`;
|
|
557
|
+
}
|
|
558
|
+
if (/\\\d+[*+{]/.test(pattern)) return "Quantified backreferences are forbidden (ReDoS risk)";
|
|
559
|
+
if (!safeRegex2(pattern)) return "Pattern rejected: potential ReDoS vulnerability detected";
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
function getCompiledRegex(pattern, flags = "") {
|
|
563
|
+
if (flags && !/^[gimsuy]+$/.test(flags)) return null;
|
|
564
|
+
const key = `${pattern}\0${flags}`;
|
|
565
|
+
if (regexCache.has(key)) {
|
|
566
|
+
const cached = regexCache.get(key);
|
|
567
|
+
regexCache.delete(key);
|
|
568
|
+
regexCache.set(key, cached);
|
|
569
|
+
return cached;
|
|
570
|
+
}
|
|
571
|
+
if (validateRegex(pattern) !== null) return null;
|
|
572
|
+
try {
|
|
573
|
+
const re = new RegExp(pattern, flags);
|
|
574
|
+
if (regexCache.size >= REGEX_CACHE_MAX) {
|
|
575
|
+
const oldest = regexCache.keys().next().value;
|
|
576
|
+
if (oldest) regexCache.delete(oldest);
|
|
577
|
+
}
|
|
578
|
+
regexCache.set(key, re);
|
|
579
|
+
return re;
|
|
580
|
+
} catch {
|
|
581
|
+
return null;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
function matchesPattern(text, patterns) {
|
|
585
|
+
const p = Array.isArray(patterns) ? patterns : [patterns];
|
|
586
|
+
if (p.length === 0) return false;
|
|
587
|
+
const isMatch = pm(p, { nocase: true, dot: true });
|
|
588
|
+
const target = text.toLowerCase();
|
|
589
|
+
const directMatch = isMatch(target);
|
|
590
|
+
if (directMatch) return true;
|
|
591
|
+
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
592
|
+
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
593
|
+
}
|
|
594
|
+
function getNestedValue(obj, path8) {
|
|
595
|
+
if (!obj || typeof obj !== "object") return null;
|
|
596
|
+
const segments = path8.split(".");
|
|
597
|
+
for (const seg of segments) {
|
|
598
|
+
if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
|
|
599
|
+
}
|
|
600
|
+
return segments.reduce((prev, curr) => prev?.[curr], obj);
|
|
601
|
+
}
|
|
602
|
+
function evaluateSmartConditions(args, rule) {
|
|
603
|
+
if (!rule.conditions || rule.conditions.length === 0) return true;
|
|
604
|
+
const mode = rule.conditionMode ?? "all";
|
|
605
|
+
const fieldCache = /* @__PURE__ */ new Map();
|
|
606
|
+
const resolveField = (field) => {
|
|
607
|
+
if (fieldCache.has(field)) return fieldCache.get(field) ?? null;
|
|
608
|
+
const rawVal = getNestedValue(args, field);
|
|
609
|
+
const rawStr = rawVal !== null && rawVal !== void 0 ? String(rawVal) : null;
|
|
610
|
+
const stripped = field === "command" && rawStr !== null ? normalizeCommandForPolicy(rawStr) : rawStr;
|
|
611
|
+
const val = stripped !== null ? stripped.replace(/\s+/g, " ").trim() : null;
|
|
612
|
+
fieldCache.set(field, val);
|
|
613
|
+
return val;
|
|
614
|
+
};
|
|
615
|
+
const results = rule.conditions.map((cond) => {
|
|
616
|
+
const val = resolveField(cond.field);
|
|
617
|
+
switch (cond.op) {
|
|
618
|
+
case "exists":
|
|
619
|
+
return val !== null && val !== "";
|
|
620
|
+
case "notExists":
|
|
621
|
+
return val === null || val === "";
|
|
622
|
+
case "contains":
|
|
623
|
+
return val !== null && cond.value ? val.includes(cond.value) : false;
|
|
624
|
+
case "notContains":
|
|
625
|
+
return val !== null && cond.value ? !val.includes(cond.value) : true;
|
|
626
|
+
case "matches": {
|
|
627
|
+
if (val === null || !cond.value) return false;
|
|
628
|
+
const reM = getCompiledRegex(cond.value, cond.flags ?? "");
|
|
629
|
+
if (!reM) return false;
|
|
630
|
+
return reM.test(val);
|
|
631
|
+
}
|
|
632
|
+
case "notMatches": {
|
|
633
|
+
if (!cond.value) return false;
|
|
634
|
+
if (val === null) return true;
|
|
635
|
+
const reN = getCompiledRegex(cond.value, cond.flags ?? "");
|
|
636
|
+
if (!reN) return false;
|
|
637
|
+
return !reN.test(val);
|
|
638
|
+
}
|
|
639
|
+
case "matchesGlob":
|
|
640
|
+
return val !== null && cond.value ? pm.isMatch(val, cond.value) : false;
|
|
641
|
+
case "notMatchesGlob":
|
|
642
|
+
return val !== null && cond.value ? !pm.isMatch(val, cond.value) : false;
|
|
643
|
+
default:
|
|
644
|
+
return false;
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
return mode === "any" ? results.some((r) => r) : results.every((r) => r);
|
|
648
|
+
}
|
|
649
|
+
function isShieldVerdict(v) {
|
|
650
|
+
return v === "allow" || v === "review" || v === "block";
|
|
651
|
+
}
|
|
652
|
+
function validateShieldDefinition(raw) {
|
|
653
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
654
|
+
return { error: "Shield file is not an object" };
|
|
655
|
+
}
|
|
656
|
+
const r = raw;
|
|
657
|
+
if (typeof r.name !== "string" || !r.name) return { error: "Shield file missing 'name'" };
|
|
658
|
+
if (typeof r.description !== "string") return { error: "Shield file missing 'description'" };
|
|
659
|
+
if (!Array.isArray(r.aliases)) return { error: "Shield file missing 'aliases' array" };
|
|
660
|
+
if (!Array.isArray(r.smartRules)) return { error: "Shield file missing 'smartRules' array" };
|
|
661
|
+
if (!Array.isArray(r.dangerousWords))
|
|
662
|
+
return { error: "Shield file missing 'dangerousWords' array" };
|
|
663
|
+
return { ok: r };
|
|
664
|
+
}
|
|
665
|
+
function validateOverrides(raw) {
|
|
666
|
+
const warnings = [];
|
|
667
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return { overrides: {}, warnings };
|
|
668
|
+
const result = {};
|
|
669
|
+
for (const [shieldName, rules] of Object.entries(raw)) {
|
|
670
|
+
if (!rules || typeof rules !== "object" || Array.isArray(rules)) continue;
|
|
671
|
+
const validRules = {};
|
|
672
|
+
for (const [ruleName, verdict] of Object.entries(rules)) {
|
|
673
|
+
if (isShieldVerdict(verdict)) {
|
|
674
|
+
validRules[ruleName] = verdict;
|
|
675
|
+
} else {
|
|
676
|
+
warnings.push(
|
|
677
|
+
`shields.json contains invalid verdict "${String(verdict)}" for ${shieldName}/${ruleName} \u2014 entry ignored. File may be corrupted or tampered with.`
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
if (Object.keys(validRules).length > 0) result[shieldName] = validRules;
|
|
682
|
+
}
|
|
683
|
+
return { overrides: result, warnings };
|
|
684
|
+
}
|
|
685
|
+
function assertBuiltinShieldRegexesAreSafe() {
|
|
686
|
+
for (const shield of Object.values(BUILTIN_SHIELDS)) {
|
|
687
|
+
for (const rule of shield.smartRules) {
|
|
688
|
+
const conditions = rule.conditions ?? [];
|
|
689
|
+
for (const cond of conditions) {
|
|
690
|
+
if (cond.op !== "matches" && cond.op !== "notMatches") continue;
|
|
691
|
+
const pattern = cond.value;
|
|
692
|
+
if (!pattern) continue;
|
|
693
|
+
if (!safeRegex3(pattern)) {
|
|
694
|
+
throw new Error(
|
|
695
|
+
`[node9 engine] Shield '${shield.name}' rule '${rule.name ?? rule.tool}' has unsafe regex: ${pattern}`
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
function classifyRuleSeverity(name, verdict) {
|
|
703
|
+
const n = name.toLowerCase();
|
|
704
|
+
const criticalPatterns = [
|
|
705
|
+
"rm-rf",
|
|
706
|
+
"eval-remote",
|
|
707
|
+
"eval-curl",
|
|
708
|
+
"read-aws",
|
|
709
|
+
"read-ssh",
|
|
710
|
+
"read-gcp",
|
|
711
|
+
"read-cred",
|
|
712
|
+
"delete-repo",
|
|
713
|
+
"helm-uninstall",
|
|
714
|
+
"drop-table",
|
|
715
|
+
"drop-database",
|
|
716
|
+
"drop-collection",
|
|
717
|
+
"truncate",
|
|
718
|
+
"flushall",
|
|
719
|
+
"flushdb",
|
|
720
|
+
"pipe-shell"
|
|
721
|
+
];
|
|
722
|
+
if (criticalPatterns.some((p) => n.includes(p))) return "critical";
|
|
723
|
+
const highPatterns = [
|
|
724
|
+
"force-push",
|
|
725
|
+
"force_push",
|
|
726
|
+
"git-destructive",
|
|
727
|
+
"reset-hard",
|
|
728
|
+
"rebase",
|
|
729
|
+
"delete-branch",
|
|
730
|
+
"delete-remote"
|
|
731
|
+
];
|
|
732
|
+
if (highPatterns.some((p) => n.includes(p))) return "high";
|
|
733
|
+
if (verdict === "block") return "high";
|
|
734
|
+
return "medium";
|
|
735
|
+
}
|
|
736
|
+
function detectPii(text) {
|
|
737
|
+
const found = /* @__PURE__ */ new Set();
|
|
738
|
+
if (/@/.test(text) && PII_EMAIL_RE.test(text)) found.add("Email");
|
|
739
|
+
if (/-/.test(text) && PII_SSN_RE.test(text)) found.add("SSN");
|
|
740
|
+
if (PII_PHONE_RE.test(text)) found.add("Phone");
|
|
741
|
+
if (PII_CC_RE.test(text)) found.add("Credit Card");
|
|
742
|
+
return [...found];
|
|
743
|
+
}
|
|
744
|
+
function extractCanonicalFindings(call, ctx) {
|
|
745
|
+
const out = [];
|
|
746
|
+
const ts = call.timestamp;
|
|
747
|
+
const toolNameLower = call.toolName.toLowerCase();
|
|
748
|
+
const command = typeof call.args.command === "string" ? call.args.command : null;
|
|
749
|
+
const isBash = isBashTool(call.toolName) && command !== null;
|
|
750
|
+
if (call.outputBytes !== void 0 && call.outputBytes > LONG_OUTPUT_THRESHOLD_BYTES) {
|
|
751
|
+
out.push(
|
|
752
|
+
makeFinding({
|
|
753
|
+
type: "long-output-redacted",
|
|
754
|
+
ruleName: "long-output-redacted",
|
|
755
|
+
verdict: "review",
|
|
756
|
+
severity: "medium",
|
|
757
|
+
reason: `Tool output exceeded ${LONG_OUTPUT_THRESHOLD_BYTES} bytes and was redacted`,
|
|
758
|
+
toolName: call.toolName,
|
|
759
|
+
ctx,
|
|
760
|
+
ts,
|
|
761
|
+
sourceType: "engine"
|
|
762
|
+
})
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
if (ctx.dlpEnabled) {
|
|
766
|
+
const dlp = scanArgs(call.args);
|
|
767
|
+
if (dlp) {
|
|
768
|
+
out.push(
|
|
769
|
+
makeFinding({
|
|
770
|
+
type: "dlp",
|
|
771
|
+
ruleName: `dlp:${dlp.patternName}`,
|
|
772
|
+
patternName: dlp.patternName,
|
|
773
|
+
verdict: dlp.severity === "block" ? "block" : "review",
|
|
774
|
+
severity: dlp.severity === "block" ? "critical" : "medium",
|
|
775
|
+
reason: `${dlp.patternName} detected in ${dlp.fieldPath}`,
|
|
776
|
+
toolName: call.toolName,
|
|
777
|
+
ctx,
|
|
778
|
+
ts,
|
|
779
|
+
sourceType: "engine",
|
|
780
|
+
input: call.args,
|
|
781
|
+
redactedSample: dlp.redactedSample
|
|
782
|
+
})
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
for (const value of stringValues(call.args)) {
|
|
787
|
+
const piiHits = detectPii(value);
|
|
788
|
+
for (const pattern of piiHits) {
|
|
789
|
+
out.push(
|
|
790
|
+
makeFinding({
|
|
791
|
+
type: "pii",
|
|
792
|
+
ruleName: `pii:${pattern.toLowerCase().replace(/\s+/g, "-")}`,
|
|
793
|
+
patternName: pattern,
|
|
794
|
+
verdict: "review",
|
|
795
|
+
severity: "medium",
|
|
796
|
+
reason: `${pattern} pattern detected in tool input`,
|
|
797
|
+
toolName: call.toolName,
|
|
798
|
+
ctx,
|
|
799
|
+
ts,
|
|
800
|
+
sourceType: "engine"
|
|
801
|
+
})
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
if (FILE_TOOLS.has(toolNameLower)) {
|
|
806
|
+
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 || "";
|
|
807
|
+
if (filePath && SENSITIVE_PATH_RE.test(filePath)) {
|
|
808
|
+
out.push(
|
|
809
|
+
makeFinding({
|
|
810
|
+
type: "sensitive-file-read",
|
|
811
|
+
ruleName: "sensitive-file-read",
|
|
812
|
+
verdict: "review",
|
|
813
|
+
severity: "critical",
|
|
814
|
+
reason: `Sensitive file path read via ${call.toolName}`,
|
|
815
|
+
toolName: call.toolName,
|
|
816
|
+
ctx,
|
|
817
|
+
ts,
|
|
818
|
+
sourceType: "engine",
|
|
819
|
+
subjectPath: filePath
|
|
820
|
+
})
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
if (!isBash || command === null) {
|
|
825
|
+
return out;
|
|
826
|
+
}
|
|
827
|
+
const fsVerdict = analyzeFsOperation(command);
|
|
828
|
+
if (fsVerdict) {
|
|
829
|
+
const isShield = fsVerdict.ruleName.startsWith("shield:");
|
|
830
|
+
out.push(
|
|
831
|
+
makeFinding({
|
|
832
|
+
type: "ast-fs-op",
|
|
833
|
+
ruleName: fsVerdict.ruleName,
|
|
834
|
+
verdict: fsVerdict.verdict,
|
|
835
|
+
severity: classifyRuleSeverity(fsVerdict.ruleName, fsVerdict.verdict),
|
|
836
|
+
reason: fsVerdict.reason,
|
|
837
|
+
toolName: call.toolName,
|
|
838
|
+
ctx,
|
|
839
|
+
ts,
|
|
840
|
+
sourceType: isShield ? "shield" : "engine",
|
|
841
|
+
shieldLabel: isShield ? "project-jail (AST)" : "Node9 (AST)",
|
|
842
|
+
subjectPath: fsVerdict.path,
|
|
843
|
+
input: call.args
|
|
844
|
+
})
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
for (const source of ctx.rules) {
|
|
848
|
+
const r = source.rule;
|
|
849
|
+
if (r.verdict === "allow") continue;
|
|
850
|
+
if (r.tool && !matchesPattern(toolNameLower, r.tool)) continue;
|
|
851
|
+
if (r.name && AST_FS_REGEX_RULES.has(r.name)) continue;
|
|
852
|
+
if (!evaluateSmartConditions(call.args, r)) continue;
|
|
853
|
+
out.push(
|
|
854
|
+
makeFinding({
|
|
855
|
+
type: "smart-rule",
|
|
856
|
+
ruleName: r.name ?? r.tool,
|
|
857
|
+
verdict: r.verdict === "block" ? "block" : "review",
|
|
858
|
+
severity: classifyRuleSeverity(r.name ?? r.tool, r.verdict),
|
|
859
|
+
reason: r.reason ?? `Smart rule ${r.name ?? r.tool} fired`,
|
|
860
|
+
toolName: call.toolName,
|
|
861
|
+
ctx,
|
|
862
|
+
ts,
|
|
863
|
+
sourceType: source.sourceType,
|
|
864
|
+
shieldLabel: source.shieldLabel,
|
|
865
|
+
input: call.args
|
|
866
|
+
})
|
|
867
|
+
);
|
|
868
|
+
break;
|
|
869
|
+
}
|
|
870
|
+
const evalVerdict = detectDangerousShellExec(command);
|
|
871
|
+
if (evalVerdict) {
|
|
872
|
+
out.push(
|
|
873
|
+
makeFinding({
|
|
874
|
+
type: "eval-of-remote",
|
|
875
|
+
ruleName: "eval-of-remote",
|
|
876
|
+
verdict: evalVerdict,
|
|
877
|
+
severity: classifyRuleSeverity("eval-remote", evalVerdict),
|
|
878
|
+
reason: evalVerdict === "block" ? "Eval of remote download is a near-certain supply-chain attack" : "Eval of dynamic content (variable / subshell) requires approval",
|
|
879
|
+
toolName: call.toolName,
|
|
880
|
+
ctx,
|
|
881
|
+
ts,
|
|
882
|
+
sourceType: "engine",
|
|
883
|
+
input: call.args
|
|
884
|
+
})
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
const pipe = analyzePipeChain(command);
|
|
888
|
+
if (pipe.isPipeline && pipe.risk === "critical") {
|
|
889
|
+
out.push(
|
|
890
|
+
makeFinding({
|
|
891
|
+
type: "pipe-to-shell",
|
|
892
|
+
ruleName: "pipe-to-shell",
|
|
893
|
+
verdict: "block",
|
|
894
|
+
severity: "critical",
|
|
895
|
+
reason: `Sensitive file piped through obfuscator to network sink: ${pipe.sourceFiles.join(", ")} \u2192 ${pipe.sinkTargets.join(", ")}`,
|
|
896
|
+
toolName: call.toolName,
|
|
897
|
+
ctx,
|
|
898
|
+
ts,
|
|
899
|
+
sourceType: "engine",
|
|
900
|
+
input: call.args
|
|
901
|
+
})
|
|
902
|
+
);
|
|
903
|
+
}
|
|
904
|
+
if (DESTRUCTIVE_OP_RE.test(command)) {
|
|
905
|
+
out.push(
|
|
906
|
+
makeFinding({
|
|
907
|
+
type: "destructive-op",
|
|
908
|
+
ruleName: "destructive-op",
|
|
909
|
+
verdict: "review",
|
|
910
|
+
severity: "high",
|
|
911
|
+
reason: "Destructive operation pattern detected",
|
|
912
|
+
toolName: call.toolName,
|
|
913
|
+
ctx,
|
|
914
|
+
ts,
|
|
915
|
+
sourceType: "engine",
|
|
916
|
+
input: call.args
|
|
917
|
+
})
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
const ast = analyzeShellCommand(command);
|
|
921
|
+
const sudoVariant = ast.actions.includes("sudo") || ast.actions.includes("su");
|
|
922
|
+
const chmodVariant = ast.actions.includes("chmod") && (ast.allTokens.includes("777") || ast.allTokens.includes("0777") || ast.allTokens.includes("+x"));
|
|
923
|
+
const chownVariant = ast.actions.includes("chown") && ast.allTokens.includes("root");
|
|
924
|
+
if (sudoVariant || chmodVariant || chownVariant) {
|
|
925
|
+
out.push(
|
|
926
|
+
makeFinding({
|
|
927
|
+
type: "privilege-escalation",
|
|
928
|
+
ruleName: "privilege-escalation",
|
|
929
|
+
verdict: "review",
|
|
930
|
+
severity: "high",
|
|
931
|
+
reason: "Privilege-escalation pattern detected",
|
|
932
|
+
toolName: call.toolName,
|
|
933
|
+
ctx,
|
|
934
|
+
ts,
|
|
935
|
+
sourceType: "engine",
|
|
936
|
+
input: call.args
|
|
937
|
+
})
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
return out;
|
|
941
|
+
}
|
|
942
|
+
function toScanFinding(c) {
|
|
943
|
+
const typeMap = {
|
|
944
|
+
"smart-rule": null,
|
|
945
|
+
"ast-fs-op": null,
|
|
946
|
+
dlp: "dlp",
|
|
947
|
+
pii: "pii",
|
|
948
|
+
"sensitive-file-read": "sensitive-file-read",
|
|
949
|
+
"privilege-escalation": "privilege-escalation",
|
|
950
|
+
"destructive-op": "destructive-op",
|
|
951
|
+
"pipe-to-shell": "pipe-to-shell",
|
|
952
|
+
"eval-of-remote": "eval-of-remote",
|
|
953
|
+
loop: "loop",
|
|
954
|
+
"long-output-redacted": "long-output-redacted"
|
|
955
|
+
};
|
|
956
|
+
const sfType = typeMap[c.type];
|
|
957
|
+
if (sfType === null) return null;
|
|
958
|
+
return {
|
|
959
|
+
sessionId: c.sessionId,
|
|
960
|
+
type: sfType,
|
|
961
|
+
...c.patternName && { patternName: c.patternName },
|
|
962
|
+
lineIndex: c.lineIndex
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
function makeFinding(args) {
|
|
966
|
+
const f = {
|
|
967
|
+
type: args.type,
|
|
968
|
+
ruleName: args.ruleName,
|
|
969
|
+
verdict: args.verdict,
|
|
970
|
+
severity: args.severity,
|
|
971
|
+
reason: args.reason,
|
|
972
|
+
toolName: args.toolName,
|
|
973
|
+
agent: args.ctx.agent,
|
|
974
|
+
sessionId: args.ctx.sessionId,
|
|
975
|
+
project: args.ctx.project,
|
|
976
|
+
lineIndex: args.ctx.lineIndex,
|
|
977
|
+
sourceType: args.sourceType,
|
|
978
|
+
firstSeenAt: args.ts,
|
|
979
|
+
lastSeenAt: args.ts,
|
|
980
|
+
occurrenceCount: 1
|
|
981
|
+
};
|
|
982
|
+
if (args.shieldLabel) f.shieldLabel = args.shieldLabel;
|
|
983
|
+
if (args.subjectPath) f.subjectPath = args.subjectPath;
|
|
984
|
+
if (args.input) f.input = args.input;
|
|
985
|
+
if (args.patternName) f.patternName = args.patternName;
|
|
986
|
+
if (args.redactedSample) f.redactedSample = args.redactedSample;
|
|
987
|
+
return f;
|
|
988
|
+
}
|
|
989
|
+
function* stringValues(obj, depth = 0) {
|
|
990
|
+
if (depth > 6) return;
|
|
991
|
+
if (typeof obj === "string") {
|
|
992
|
+
if (obj.length > 0) yield obj;
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
if (!obj || typeof obj !== "object") return;
|
|
996
|
+
if (Array.isArray(obj)) {
|
|
997
|
+
for (const v of obj) yield* stringValues(v, depth + 1);
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
for (const v of Object.values(obj)) yield* stringValues(v, depth + 1);
|
|
1001
|
+
}
|
|
1002
|
+
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, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, DESTRUCTIVE_OP_RE, SENSITIVE_PATH_RE, FILE_TOOLS, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC_RE, LONG_OUTPUT_THRESHOLD_BYTES;
|
|
1003
|
+
var init_dist = __esm({
|
|
1004
|
+
"packages/policy-engine/dist/index.mjs"() {
|
|
1005
|
+
"use strict";
|
|
1006
|
+
ASSIGNMENT_CONTEXT_RE = /\b(?:password|passwd|secret|token|api[_-]?key|auth(?:_key|_token)?|credential|private[_-]?key|access[_-]?key|client[_-]?secret)\s*[=:]\s*/i;
|
|
1007
|
+
DLP_STOPWORDS = [
|
|
1008
|
+
"example",
|
|
1009
|
+
"placeholder",
|
|
1010
|
+
"changeme",
|
|
1011
|
+
"your_key",
|
|
1012
|
+
"your_token",
|
|
1013
|
+
"your_secret",
|
|
1014
|
+
"replace_me",
|
|
1015
|
+
"insert_key",
|
|
1016
|
+
"put_your",
|
|
1017
|
+
"fake",
|
|
1018
|
+
"dummy",
|
|
1019
|
+
"sample",
|
|
1020
|
+
"xxxxxxxx",
|
|
1021
|
+
"aaaaaa",
|
|
1022
|
+
"bbbbbb",
|
|
1023
|
+
"00000000",
|
|
1024
|
+
"${",
|
|
1025
|
+
"{{",
|
|
1026
|
+
"%{",
|
|
1027
|
+
"<your",
|
|
1028
|
+
"test_key",
|
|
1029
|
+
"test_token",
|
|
1030
|
+
"your",
|
|
1031
|
+
"here"
|
|
1032
|
+
];
|
|
1033
|
+
DLP_PATTERNS = [
|
|
1034
|
+
// ── AWS ───────────────────────────────────────────────────────────────────
|
|
1035
|
+
{
|
|
1036
|
+
name: "AWS Access Key ID",
|
|
1037
|
+
regex: /\b(?:A3T[A-Z0-9]|AKIA|ASIA|ABIA|ACCA)[A-Z2-7]{16}\b/,
|
|
1038
|
+
severity: "block",
|
|
1039
|
+
keywords: ["akia", "asia", "abia", "acca", "a3t"]
|
|
1040
|
+
},
|
|
1041
|
+
// ── GitHub ────────────────────────────────────────────────────────────────
|
|
1042
|
+
{
|
|
1043
|
+
name: "GitHub Token",
|
|
1044
|
+
regex: /\bgh[pous]_[A-Za-z0-9]{36}\b/,
|
|
1045
|
+
severity: "block",
|
|
1046
|
+
keywords: ["ghp_", "gho_", "ghu_", "ghs_"],
|
|
1047
|
+
minEntropy: 3
|
|
1048
|
+
},
|
|
1049
|
+
{
|
|
1050
|
+
name: "GitHub Fine-Grained PAT",
|
|
1051
|
+
regex: /\bgithub_pat_\w{82}\b/,
|
|
1052
|
+
severity: "block",
|
|
1053
|
+
keywords: ["github_pat_"]
|
|
1054
|
+
},
|
|
1055
|
+
// ── Slack ─────────────────────────────────────────────────────────────────
|
|
1056
|
+
{
|
|
1057
|
+
name: "Slack Bot Token",
|
|
1058
|
+
// Real tokens are ~50–80 chars; lower bound 20 avoids false negatives on partial tokens
|
|
1059
|
+
regex: /\bxoxb-[0-9A-Za-z-]{20,100}\b/,
|
|
1060
|
+
severity: "block",
|
|
1061
|
+
keywords: ["xoxb-"]
|
|
1062
|
+
},
|
|
1063
|
+
// ── Anthropic ─────────────────────────────────────────────────────────────
|
|
1064
|
+
// Listed before OpenAI — Anthropic keys start with sk-ant- which would also
|
|
1065
|
+
// match the broader OpenAI sk- pattern; more specific rules must come first.
|
|
1066
|
+
{
|
|
1067
|
+
name: "Anthropic API Key",
|
|
1068
|
+
regex: /\bsk-ant-api03-[a-zA-Z0-9_-]{93}AA\b/,
|
|
1069
|
+
severity: "block",
|
|
1070
|
+
keywords: ["sk-ant-api03"]
|
|
1071
|
+
},
|
|
1072
|
+
{
|
|
1073
|
+
name: "Anthropic Admin Key",
|
|
1074
|
+
regex: /\bsk-ant-admin01-[a-zA-Z0-9_-]{93}AA\b/,
|
|
1075
|
+
severity: "block",
|
|
1076
|
+
keywords: ["sk-ant-admin01"]
|
|
1077
|
+
},
|
|
1078
|
+
// ── OpenAI ────────────────────────────────────────────────────────────────
|
|
1079
|
+
{
|
|
1080
|
+
name: "OpenAI API Key",
|
|
1081
|
+
regex: /\bsk-[a-zA-Z0-9_-]{20,}\b/,
|
|
1082
|
+
severity: "block",
|
|
1083
|
+
keywords: ["sk-"],
|
|
1084
|
+
minEntropy: 3.5
|
|
1085
|
+
},
|
|
1086
|
+
// ── Stripe ────────────────────────────────────────────────────────────────
|
|
1087
|
+
{
|
|
1088
|
+
name: "Stripe Secret Key",
|
|
1089
|
+
regex: /\bsk_(?:live|test)_[0-9a-zA-Z]{24}\b/,
|
|
1090
|
+
severity: "block",
|
|
1091
|
+
keywords: ["sk_live_", "sk_test_"]
|
|
1092
|
+
},
|
|
1093
|
+
// ── GCP ───────────────────────────────────────────────────────────────────
|
|
1094
|
+
{
|
|
1095
|
+
name: "GCP API Key",
|
|
1096
|
+
regex: /\bAIza[0-9A-Za-z_-]{35}\b/,
|
|
1097
|
+
severity: "block",
|
|
1098
|
+
keywords: ["aiza"],
|
|
1099
|
+
minEntropy: 3
|
|
1100
|
+
},
|
|
1101
|
+
{
|
|
1102
|
+
name: "GCP Service Account",
|
|
1103
|
+
regex: /"type"\s*:\s*"service_account"/,
|
|
1104
|
+
severity: "block",
|
|
1105
|
+
keywords: ["service_account"]
|
|
1106
|
+
},
|
|
1107
|
+
// ── Azure ─────────────────────────────────────────────────────────────────
|
|
1108
|
+
// Pattern: 3 alphanum chars + digit + Q~ + 31-34 alphanum chars
|
|
1109
|
+
{
|
|
1110
|
+
name: "Azure AD Client Secret",
|
|
1111
|
+
regex: /(?:^|[\s>=:(,])([a-zA-Z0-9_~.]{3}\dQ~[a-zA-Z0-9_~.-]{31,34})(?:$|[\s<),])/,
|
|
1112
|
+
severity: "block",
|
|
1113
|
+
keywords: ["q~"]
|
|
1114
|
+
},
|
|
1115
|
+
// ── Databricks ────────────────────────────────────────────────────────────
|
|
1116
|
+
{
|
|
1117
|
+
name: "Databricks API Token",
|
|
1118
|
+
regex: /\bdapi[a-f0-9]{32}(?:-\d)?\b/,
|
|
1119
|
+
severity: "block",
|
|
1120
|
+
keywords: ["dapi"]
|
|
1121
|
+
},
|
|
1122
|
+
// ── DigitalOcean ──────────────────────────────────────────────────────────
|
|
1123
|
+
{
|
|
1124
|
+
name: "DigitalOcean PAT",
|
|
1125
|
+
regex: /\bdop_v1_[a-f0-9]{64}\b/,
|
|
1126
|
+
severity: "block",
|
|
1127
|
+
keywords: ["dop_v1_"]
|
|
1128
|
+
},
|
|
1129
|
+
{
|
|
1130
|
+
name: "DigitalOcean Access Token",
|
|
1131
|
+
regex: /\bdoo_v1_[a-f0-9]{64}\b/,
|
|
1132
|
+
severity: "block",
|
|
1133
|
+
keywords: ["doo_v1_"]
|
|
1134
|
+
},
|
|
1135
|
+
// ── Doppler ───────────────────────────────────────────────────────────────
|
|
1136
|
+
{
|
|
1137
|
+
name: "Doppler Token",
|
|
1138
|
+
regex: /\bdp\.pt\.[a-z0-9]{43}\b/i,
|
|
1139
|
+
severity: "block",
|
|
1140
|
+
keywords: ["dp.pt."]
|
|
1141
|
+
},
|
|
1142
|
+
// ── HashiCorp Vault ───────────────────────────────────────────────────────
|
|
1143
|
+
{
|
|
1144
|
+
name: "HashiCorp Vault Service Token",
|
|
1145
|
+
regex: /\bhvs\.[\w-]{90,120}\b/,
|
|
1146
|
+
severity: "block",
|
|
1147
|
+
keywords: ["hvs."]
|
|
1148
|
+
},
|
|
1149
|
+
{
|
|
1150
|
+
name: "HashiCorp Vault Batch Token",
|
|
1151
|
+
regex: /\bhvb\.[\w-]{138,300}\b/,
|
|
1152
|
+
severity: "block",
|
|
1153
|
+
keywords: ["hvb."]
|
|
1154
|
+
},
|
|
1155
|
+
// ── Hugging Face ──────────────────────────────────────────────────────────
|
|
1156
|
+
{
|
|
1157
|
+
name: "HuggingFace Token",
|
|
1158
|
+
regex: /\bhf_[A-Za-z]{34}\b/,
|
|
1159
|
+
severity: "block",
|
|
1160
|
+
keywords: ["hf_"],
|
|
1161
|
+
minEntropy: 3
|
|
1162
|
+
},
|
|
1163
|
+
// ── Postman ───────────────────────────────────────────────────────────────
|
|
1164
|
+
{
|
|
1165
|
+
name: "Postman API Token",
|
|
1166
|
+
regex: /\bPMAK-[a-f0-9]{24}-[a-f0-9]{34}\b/i,
|
|
1167
|
+
severity: "block",
|
|
1168
|
+
keywords: ["pmak-"]
|
|
1169
|
+
},
|
|
1170
|
+
// ── Pulumi ────────────────────────────────────────────────────────────────
|
|
1171
|
+
{
|
|
1172
|
+
name: "Pulumi Access Token",
|
|
1173
|
+
regex: /\bpul-[a-f0-9]{40}\b/,
|
|
1174
|
+
severity: "block",
|
|
1175
|
+
keywords: ["pul-"]
|
|
1176
|
+
},
|
|
1177
|
+
// ── SendGrid ──────────────────────────────────────────────────────────────
|
|
1178
|
+
{
|
|
1179
|
+
name: "SendGrid API Key",
|
|
1180
|
+
regex: /\bSG\.[a-zA-Z0-9=_.-]{66}\b/,
|
|
1181
|
+
severity: "block",
|
|
1182
|
+
keywords: ["sg."]
|
|
1183
|
+
},
|
|
1184
|
+
// ── Private keys (PEM) ────────────────────────────────────────────────────
|
|
1185
|
+
{
|
|
1186
|
+
name: "Private Key (PEM)",
|
|
1187
|
+
regex: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
|
|
1188
|
+
severity: "block",
|
|
1189
|
+
keywords: ["-----begin"]
|
|
1190
|
+
},
|
|
1191
|
+
// ── NPM ───────────────────────────────────────────────────────────────────
|
|
1192
|
+
{
|
|
1193
|
+
name: "NPM Auth Token",
|
|
1194
|
+
regex: /_authToken\s*=\s*[A-Za-z0-9_-]{20,}/,
|
|
1195
|
+
severity: "block",
|
|
1196
|
+
keywords: ["_authtoken"]
|
|
1197
|
+
},
|
|
1198
|
+
// ── JWT ───────────────────────────────────────────────────────────────────
|
|
1199
|
+
// review (not block): JWTs appear legitimately in API calls; flag for human approval
|
|
1200
|
+
// contextBoost: promoted to block when assigned (e.g. TOKEN=eyJ...)
|
|
1201
|
+
{
|
|
1202
|
+
name: "JWT",
|
|
1203
|
+
regex: /\bey[a-zA-Z0-9]{17,}\.ey[a-zA-Z0-9\/_-]{17,}\.[a-zA-Z0-9\/_-]{10,}={0,2}\b/,
|
|
1204
|
+
severity: "review",
|
|
1205
|
+
keywords: ["eyj"],
|
|
1206
|
+
contextBoost: true
|
|
1207
|
+
},
|
|
1208
|
+
// ── Stripe (extended — adds restricted key rk_ prefix) ──────────────────
|
|
1209
|
+
{
|
|
1210
|
+
name: "Stripe Restricted Key",
|
|
1211
|
+
regex: /\brk_(?:live|test|prod)_[0-9a-zA-Z]{10,99}\b/,
|
|
1212
|
+
severity: "block",
|
|
1213
|
+
keywords: ["rk_live_", "rk_test_", "rk_prod_"]
|
|
1214
|
+
},
|
|
1215
|
+
// ── Slack (app token) ─────────────────────────────────────────────────────
|
|
1216
|
+
{
|
|
1217
|
+
name: "Slack App Token",
|
|
1218
|
+
regex: /\bxapp-\d-[A-Z0-9]+-\d+-[a-f0-9]+\b/,
|
|
1219
|
+
severity: "block",
|
|
1220
|
+
keywords: ["xapp-"]
|
|
1221
|
+
},
|
|
1222
|
+
// ── GitLab ────────────────────────────────────────────────────────────────
|
|
1223
|
+
{ name: "GitLab PAT", regex: /\bglpat-[\w-]{20}\b/, severity: "block", keywords: ["glpat-"] },
|
|
1224
|
+
{
|
|
1225
|
+
name: "GitLab Deploy Token",
|
|
1226
|
+
regex: /\bgldt-[0-9a-zA-Z_-]{20}\b/,
|
|
1227
|
+
severity: "block",
|
|
1228
|
+
keywords: ["gldt-"]
|
|
1229
|
+
},
|
|
1230
|
+
{
|
|
1231
|
+
name: "GitLab CI Job Token",
|
|
1232
|
+
regex: /\bglcbt-[0-9a-zA-Z]{1,5}_[0-9a-zA-Z_-]{20}\b/,
|
|
1233
|
+
severity: "block",
|
|
1234
|
+
keywords: ["glcbt-"]
|
|
1235
|
+
},
|
|
1236
|
+
// ── npm (publish token) ───────────────────────────────────────────────────
|
|
1237
|
+
{
|
|
1238
|
+
name: "npm Access Token",
|
|
1239
|
+
regex: /\bnpm_[a-zA-Z0-9]{36}\b/,
|
|
1240
|
+
severity: "block",
|
|
1241
|
+
keywords: ["npm_"]
|
|
1242
|
+
},
|
|
1243
|
+
// ── Shopify ───────────────────────────────────────────────────────────────
|
|
1244
|
+
{
|
|
1245
|
+
name: "Shopify Access Token",
|
|
1246
|
+
regex: /\bshpat_[a-fA-F0-9]{32}\b/,
|
|
1247
|
+
severity: "block",
|
|
1248
|
+
keywords: ["shpat_"]
|
|
1249
|
+
},
|
|
1250
|
+
{
|
|
1251
|
+
name: "Shopify Custom Access Token",
|
|
1252
|
+
regex: /\bshpca_[a-fA-F0-9]{32}\b/,
|
|
1253
|
+
severity: "block",
|
|
1254
|
+
keywords: ["shpca_"]
|
|
1255
|
+
},
|
|
1256
|
+
{
|
|
1257
|
+
name: "Shopify Private App Token",
|
|
1258
|
+
regex: /\bshppa_[a-fA-F0-9]{32}\b/,
|
|
1259
|
+
severity: "block",
|
|
1260
|
+
keywords: ["shppa_"]
|
|
1261
|
+
},
|
|
1262
|
+
{
|
|
1263
|
+
name: "Shopify Shared Secret",
|
|
1264
|
+
regex: /\bshpss_[a-fA-F0-9]{32}\b/,
|
|
1265
|
+
severity: "block",
|
|
1266
|
+
keywords: ["shpss_"]
|
|
1267
|
+
},
|
|
1268
|
+
// ── Linear ────────────────────────────────────────────────────────────────
|
|
1269
|
+
{
|
|
1270
|
+
name: "Linear API Key",
|
|
1271
|
+
regex: /\blin_api_[a-zA-Z0-9]{40}\b/,
|
|
1272
|
+
severity: "block",
|
|
1273
|
+
keywords: ["lin_api_"]
|
|
1274
|
+
},
|
|
1275
|
+
// ── PlanetScale ───────────────────────────────────────────────────────────
|
|
1276
|
+
{
|
|
1277
|
+
name: "PlanetScale API Token",
|
|
1278
|
+
regex: /\bpscale_tkn_[\w.-]{32,64}\b/,
|
|
1279
|
+
severity: "block",
|
|
1280
|
+
keywords: ["pscale_tkn_"]
|
|
1281
|
+
},
|
|
1282
|
+
{
|
|
1283
|
+
name: "PlanetScale Password",
|
|
1284
|
+
regex: /\bpscale_pw_[\w.-]{32,64}\b/,
|
|
1285
|
+
severity: "block",
|
|
1286
|
+
keywords: ["pscale_pw_"]
|
|
1287
|
+
},
|
|
1288
|
+
// ── Sentry ────────────────────────────────────────────────────────────────
|
|
1289
|
+
{
|
|
1290
|
+
name: "Sentry User Token",
|
|
1291
|
+
regex: /\bsntryu_[a-f0-9]{64}\b/,
|
|
1292
|
+
severity: "block",
|
|
1293
|
+
keywords: ["sntryu_"]
|
|
1294
|
+
},
|
|
1295
|
+
// ── Grafana ───────────────────────────────────────────────────────────────
|
|
1296
|
+
{
|
|
1297
|
+
name: "Grafana Service Account Token",
|
|
1298
|
+
regex: /\bglsa_[a-zA-Z0-9]{32}_[a-f0-9]{8}\b/,
|
|
1299
|
+
severity: "block",
|
|
1300
|
+
keywords: ["glsa_"]
|
|
1301
|
+
},
|
|
1302
|
+
// ── Heroku ────────────────────────────────────────────────────────────────
|
|
1303
|
+
{
|
|
1304
|
+
name: "Heroku API Key",
|
|
1305
|
+
regex: /\bHRKU-AA[0-9a-zA-Z_-]{58}\b/,
|
|
1306
|
+
severity: "block",
|
|
1307
|
+
keywords: ["hrku-aa"]
|
|
1308
|
+
},
|
|
1309
|
+
// ── PyPI ──────────────────────────────────────────────────────────────────
|
|
1310
|
+
{
|
|
1311
|
+
name: "PyPI Upload Token",
|
|
1312
|
+
regex: /\bpypi-[A-Za-z0-9_-]{50,}\b/,
|
|
1313
|
+
severity: "block",
|
|
1314
|
+
keywords: ["pypi-"],
|
|
1315
|
+
minEntropy: 3
|
|
1316
|
+
},
|
|
1317
|
+
// ── Bearer Token ─────────────────────────────────────────────────────────
|
|
1318
|
+
// contextBoost: promoted to block when assigned (e.g. AUTH_TOKEN=Bearer eyJ...)
|
|
1319
|
+
{
|
|
1320
|
+
name: "Bearer Token",
|
|
1321
|
+
regex: /Bearer\s+[a-zA-Z0-9\-._~+/]{20,}=*/i,
|
|
1322
|
+
severity: "review",
|
|
1323
|
+
keywords: ["bearer"],
|
|
1324
|
+
contextBoost: true,
|
|
1325
|
+
minEntropy: 3
|
|
1326
|
+
},
|
|
1327
|
+
// ── Resend ────────────────────────────────────────────────────────────────
|
|
1328
|
+
{
|
|
1329
|
+
name: "Resend API Key",
|
|
1330
|
+
regex: /\bre_[a-zA-Z0-9]{24}\b/,
|
|
1331
|
+
severity: "block",
|
|
1332
|
+
keywords: ["re_"]
|
|
1333
|
+
},
|
|
1334
|
+
// ── Telegram ──────────────────────────────────────────────────────────────
|
|
1335
|
+
{
|
|
1336
|
+
name: "Telegram Bot Token",
|
|
1337
|
+
regex: /\b[0-9]{7,10}:AA[a-zA-Z0-9_-]{33}\b/,
|
|
1338
|
+
severity: "block",
|
|
1339
|
+
keywords: [":aa"]
|
|
1340
|
+
},
|
|
1341
|
+
// ── Mapbox ────────────────────────────────────────────────────────────────
|
|
1342
|
+
{
|
|
1343
|
+
name: "Mapbox Access Token",
|
|
1344
|
+
regex: /\bpk\.eyJ1[a-zA-Z0-9._-]{20,}\b/,
|
|
1345
|
+
severity: "block",
|
|
1346
|
+
keywords: ["pk.eyj1"],
|
|
1347
|
+
minEntropy: 3
|
|
1348
|
+
},
|
|
1349
|
+
// ── Notion ────────────────────────────────────────────────────────────────
|
|
1350
|
+
{
|
|
1351
|
+
name: "Notion Integration Token",
|
|
1352
|
+
regex: /\bsecret_[a-zA-Z0-9]{43}\b/,
|
|
1353
|
+
severity: "block",
|
|
1354
|
+
keywords: ["secret_"]
|
|
1355
|
+
},
|
|
1356
|
+
// ── Square ────────────────────────────────────────────────────────────────
|
|
1357
|
+
{
|
|
1358
|
+
name: "Square Access Token",
|
|
1359
|
+
regex: /\bsq0atp-[0-9A-Za-z_-]{22}\b/,
|
|
1360
|
+
severity: "block",
|
|
1361
|
+
keywords: ["sq0atp-"]
|
|
1362
|
+
},
|
|
1363
|
+
{
|
|
1364
|
+
name: "Square OAuth Secret",
|
|
1365
|
+
regex: /\bsq0csp-[0-9A-Za-z_-]{43}\b/,
|
|
1366
|
+
severity: "block",
|
|
1367
|
+
keywords: ["sq0csp-"]
|
|
1368
|
+
},
|
|
1369
|
+
// ── Typeform ──────────────────────────────────────────────────────────────
|
|
1370
|
+
{
|
|
1371
|
+
name: "Typeform Token",
|
|
1372
|
+
regex: /\btfp_[a-zA-Z0-9_]{59}\b/,
|
|
1373
|
+
severity: "block",
|
|
1374
|
+
keywords: ["tfp_"]
|
|
1375
|
+
},
|
|
1376
|
+
// ── Cloudinary ────────────────────────────────────────────────────────────
|
|
1377
|
+
{
|
|
1378
|
+
name: "Cloudinary URL",
|
|
1379
|
+
regex: /\bcloudinary:\/\/[0-9]+:[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+/,
|
|
1380
|
+
severity: "block",
|
|
1381
|
+
keywords: ["cloudinary://"]
|
|
1382
|
+
},
|
|
1383
|
+
// ── Airtable ──────────────────────────────────────────────────────────────
|
|
1384
|
+
// New PAT format: pat + 14 alphanum + . + 64 alphanum
|
|
1385
|
+
{
|
|
1386
|
+
name: "Airtable PAT",
|
|
1387
|
+
regex: /\bpat[a-zA-Z0-9]{14}\.[a-zA-Z0-9]{64}\b/,
|
|
1388
|
+
severity: "block",
|
|
1389
|
+
keywords: ["pat"]
|
|
1390
|
+
},
|
|
1391
|
+
// ── RubyGems ──────────────────────────────────────────────────────────────
|
|
1392
|
+
{
|
|
1393
|
+
name: "RubyGems API Key",
|
|
1394
|
+
regex: /\brubygems_[a-f0-9]{48}\b/,
|
|
1395
|
+
severity: "block",
|
|
1396
|
+
keywords: ["rubygems_"]
|
|
1397
|
+
},
|
|
1398
|
+
// ── Shippo ────────────────────────────────────────────────────────────────
|
|
1399
|
+
{
|
|
1400
|
+
name: "Shippo Token",
|
|
1401
|
+
regex: /\bshippo_(?:live|test)_[a-f0-9]{40}\b/,
|
|
1402
|
+
severity: "block",
|
|
1403
|
+
keywords: ["shippo_"]
|
|
1404
|
+
},
|
|
1405
|
+
// ── Plaid ─────────────────────────────────────────────────────────────────
|
|
1406
|
+
{
|
|
1407
|
+
name: "Plaid Access Token",
|
|
1408
|
+
regex: /\baccess-(?:sandbox|development|production)-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/,
|
|
1409
|
+
severity: "block",
|
|
1410
|
+
keywords: ["access-sandbox", "access-development", "access-production"]
|
|
1411
|
+
},
|
|
1412
|
+
// ── Age ───────────────────────────────────────────────────────────────────
|
|
1413
|
+
{
|
|
1414
|
+
name: "Age Identity Key",
|
|
1415
|
+
regex: /\bAGE-SECRET-KEY-1[QPZRY9X8GF2TVDW0S3JNLH]{58}\b/,
|
|
1416
|
+
severity: "block",
|
|
1417
|
+
keywords: ["age-secret-key-"]
|
|
1418
|
+
}
|
|
1419
|
+
];
|
|
1420
|
+
DLP_PATTERNS_GLOBAL = DLP_PATTERNS.map(
|
|
1421
|
+
(p) => ({
|
|
1422
|
+
pattern: p,
|
|
1423
|
+
globalRegex: new RegExp(
|
|
1424
|
+
p.regex.source,
|
|
1425
|
+
p.regex.flags.includes("g") ? p.regex.flags : p.regex.flags + "g"
|
|
1426
|
+
)
|
|
1427
|
+
})
|
|
1428
|
+
);
|
|
1429
|
+
SENSITIVE_PATH_PATTERNS = [
|
|
1430
|
+
/[/\\]\.ssh[/\\]/i,
|
|
1431
|
+
/[/\\]\.aws[/\\]/i,
|
|
1432
|
+
/[/\\]\.config[/\\]gcloud[/\\]/i,
|
|
1433
|
+
/[/\\]\.azure[/\\]/i,
|
|
1434
|
+
/[/\\]\.kube[/\\]config$/i,
|
|
1435
|
+
/[/\\]\.env($|\.)/i,
|
|
1436
|
+
// .env, .env.local, .env.production — not .envoy
|
|
1437
|
+
/[/\\]\.git-credentials$/i,
|
|
1438
|
+
/[/\\]\.npmrc$/i,
|
|
1439
|
+
/[/\\]\.docker[/\\]config\.json$/i,
|
|
1440
|
+
/[/\\][^/\\]+\.pem$/i,
|
|
1441
|
+
/[/\\][^/\\]+\.key$/i,
|
|
1442
|
+
/[/\\][^/\\]+\.p12$/i,
|
|
1443
|
+
/[/\\][^/\\]+\.pfx$/i,
|
|
1444
|
+
/^(?:[a-zA-Z]:)?\/etc\/passwd$/,
|
|
1445
|
+
/^(?:[a-zA-Z]:)?\/etc\/shadow$/,
|
|
1446
|
+
/^(?:[a-zA-Z]:)?\/etc\/sudoers$/,
|
|
1447
|
+
/[/\\]credentials\.json$/i,
|
|
1448
|
+
/[/\\]id_rsa$/i,
|
|
1449
|
+
/[/\\]id_ed25519$/i,
|
|
1450
|
+
/[/\\]id_ecdsa$/i
|
|
1451
|
+
];
|
|
1452
|
+
assertBuiltinPatternsAreSafe();
|
|
1453
|
+
MAX_DEPTH = 5;
|
|
1454
|
+
MAX_STRING_BYTES = 1e5;
|
|
1455
|
+
MAX_JSON_PARSE_BYTES = 1e4;
|
|
1456
|
+
({ syntax } = mvdanSh);
|
|
1457
|
+
sharedParser = syntax.NewParser();
|
|
1458
|
+
MESSAGE_FLAGS = /* @__PURE__ */ new Set([
|
|
1459
|
+
"-m",
|
|
1460
|
+
"--message",
|
|
1461
|
+
"--body",
|
|
1462
|
+
"--title",
|
|
1463
|
+
"--description",
|
|
1464
|
+
"--comment",
|
|
1465
|
+
"--subject",
|
|
1466
|
+
"--summary"
|
|
1467
|
+
]);
|
|
1468
|
+
SHELL_INTERPRETERS = /* @__PURE__ */ new Set(["bash", "sh", "zsh", "fish", "dash", "ksh"]);
|
|
1469
|
+
DOWNLOAD_CMDS = /* @__PURE__ */ new Set(["curl", "wget"]);
|
|
1470
|
+
NORMALIZE_CACHE_MAX = 5e3;
|
|
1471
|
+
normalizeCache = /* @__PURE__ */ new Map();
|
|
1472
|
+
AST_CACHE_MAX = 5e3;
|
|
1473
|
+
astCache = /* @__PURE__ */ new Map();
|
|
1474
|
+
PARSE_FAIL = /* @__PURE__ */ Symbol("parse-fail");
|
|
1475
|
+
FS_READ_TOOLS = /* @__PURE__ */ new Set([
|
|
1476
|
+
"cat",
|
|
1477
|
+
"less",
|
|
1478
|
+
"head",
|
|
1479
|
+
"tail",
|
|
1480
|
+
"bat",
|
|
1481
|
+
"more",
|
|
1482
|
+
"open",
|
|
1483
|
+
"print",
|
|
1484
|
+
"nano",
|
|
1485
|
+
"vim",
|
|
1486
|
+
"vi",
|
|
1487
|
+
"emacs",
|
|
1488
|
+
"code",
|
|
1489
|
+
"type"
|
|
1490
|
+
]);
|
|
1491
|
+
FS_OP_PRESCREEN_RE = /(?:^|[\s|;&(`\n])(?:rm|cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\b/;
|
|
1492
|
+
HOME_CACHE_ALLOWLIST = [
|
|
1493
|
+
".cache",
|
|
1494
|
+
".npm/_npx",
|
|
1495
|
+
".npm/_cacache",
|
|
1496
|
+
".cargo/registry",
|
|
1497
|
+
".gradle/caches",
|
|
1498
|
+
".gradle/.tmp",
|
|
1499
|
+
".m2/repository",
|
|
1500
|
+
".pnpm-store",
|
|
1501
|
+
".yarn/cache",
|
|
1502
|
+
".yarn/.cache",
|
|
1503
|
+
".cache/pip",
|
|
1504
|
+
".local/share/Trash",
|
|
1505
|
+
".rustup/downloads"
|
|
1506
|
+
];
|
|
1507
|
+
SENSITIVE_PATH_RULES = [
|
|
1508
|
+
{
|
|
1509
|
+
rule: "shield:project-jail:block-read-ssh",
|
|
1510
|
+
reason: "Reading SSH private keys is blocked by project-jail shield",
|
|
1511
|
+
match: (p) => /(^|[\\/])\.ssh[\\/]/i.test(p)
|
|
1512
|
+
},
|
|
1513
|
+
{
|
|
1514
|
+
rule: "shield:project-jail:block-read-aws",
|
|
1515
|
+
reason: "Reading AWS credentials is blocked by project-jail shield",
|
|
1516
|
+
match: (p) => /(^|[\\/])\.aws[\\/]/i.test(p)
|
|
1517
|
+
},
|
|
1518
|
+
{
|
|
1519
|
+
rule: "shield:project-jail:block-read-env",
|
|
1520
|
+
reason: "Reading .env files is blocked by project-jail shield",
|
|
1521
|
+
match: (p) => /(?:^|[\\/])\.env(?:\.local|\.production|\.staging)?$/i.test(p)
|
|
1522
|
+
},
|
|
1523
|
+
{
|
|
1524
|
+
rule: "shield:project-jail:block-read-credentials",
|
|
1525
|
+
reason: "Reading credential files is blocked by project-jail shield",
|
|
1526
|
+
match: (p) => /(?:credentials\.json|\.netrc|\.npmrc|\.docker[\\/]config\.json|gcloud[\\/]credentials)$/i.test(
|
|
1527
|
+
p
|
|
1528
|
+
)
|
|
1529
|
+
}
|
|
1530
|
+
];
|
|
1531
|
+
BASH_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
1532
|
+
"bash",
|
|
1533
|
+
"execute_bash",
|
|
1534
|
+
"run_shell_command",
|
|
1535
|
+
"shell",
|
|
1536
|
+
"exec_command"
|
|
1537
|
+
]);
|
|
1538
|
+
AST_FS_REGEX_RULES = /* @__PURE__ */ new Set([
|
|
1539
|
+
"block-rm-rf-home",
|
|
1540
|
+
"shield:project-jail:block-read-ssh",
|
|
1541
|
+
"shield:project-jail:block-read-aws",
|
|
1542
|
+
"shield:project-jail:block-read-env",
|
|
1543
|
+
"shield:project-jail:block-read-credentials"
|
|
1544
|
+
]);
|
|
1545
|
+
FS_OP_CACHE_MAX = 5e3;
|
|
1546
|
+
fsOpCache = /* @__PURE__ */ new Map();
|
|
1547
|
+
SOURCE_COMMANDS = /* @__PURE__ */ new Set([
|
|
1548
|
+
"cat",
|
|
1549
|
+
"head",
|
|
1550
|
+
"tail",
|
|
1551
|
+
"grep",
|
|
1552
|
+
"awk",
|
|
1553
|
+
"sed",
|
|
1554
|
+
"cut",
|
|
1555
|
+
"sort",
|
|
1556
|
+
"tee",
|
|
1557
|
+
"less",
|
|
1558
|
+
"more",
|
|
1559
|
+
"strings",
|
|
1560
|
+
"xxd"
|
|
1561
|
+
]);
|
|
1562
|
+
SINK_COMMANDS = /* @__PURE__ */ new Set([
|
|
1563
|
+
"curl",
|
|
1564
|
+
"wget",
|
|
1565
|
+
"nc",
|
|
1566
|
+
"ncat",
|
|
1567
|
+
"netcat",
|
|
1568
|
+
"ssh",
|
|
1569
|
+
"scp",
|
|
1570
|
+
"rsync",
|
|
1571
|
+
"socat",
|
|
1572
|
+
"ftp",
|
|
1573
|
+
"sftp",
|
|
1574
|
+
"telnet"
|
|
1575
|
+
]);
|
|
1576
|
+
OBFUSCATORS = /* @__PURE__ */ new Set([
|
|
1577
|
+
"base64",
|
|
1578
|
+
"gzip",
|
|
1579
|
+
"gunzip",
|
|
1580
|
+
"bzip2",
|
|
1581
|
+
"xz",
|
|
1582
|
+
"zstd",
|
|
1583
|
+
"openssl",
|
|
1584
|
+
"gpg",
|
|
1585
|
+
"python",
|
|
1586
|
+
"python3",
|
|
1587
|
+
"perl",
|
|
1588
|
+
"ruby",
|
|
1589
|
+
"node"
|
|
1590
|
+
]);
|
|
1591
|
+
SENSITIVE_PATTERNS = [
|
|
1592
|
+
/(?:^|\/)\.env(?:\.|$)/i,
|
|
1593
|
+
// .env, .env.local, .env.production
|
|
1594
|
+
/id_rsa|id_ed25519|id_ecdsa|id_dsa/i,
|
|
1595
|
+
// SSH private keys
|
|
1596
|
+
/\.pem$|\.key$|\.p12$|\.pfx$/i,
|
|
1597
|
+
// certificate files
|
|
1598
|
+
/(?:^|\/)\.ssh\//i,
|
|
1599
|
+
// ~/.ssh/ directory
|
|
1600
|
+
/(?:^|\/)\.aws\/credentials/i,
|
|
1601
|
+
// AWS credentials
|
|
1602
|
+
/(?:^|\/)\.netrc$/i,
|
|
1603
|
+
// netrc (stores HTTP credentials)
|
|
1604
|
+
/(?:^|\/)(passwd|shadow|sudoers)$/i,
|
|
1605
|
+
// /etc/passwd, /etc/shadow
|
|
1606
|
+
/(?:^|\/)credentials(?:\.json)?$/i
|
|
1607
|
+
// generic credentials files
|
|
1608
|
+
];
|
|
1609
|
+
MAX_REGEX_LENGTH = 100;
|
|
1610
|
+
REGEX_CACHE_MAX = 500;
|
|
1611
|
+
regexCache = /* @__PURE__ */ new Map();
|
|
1612
|
+
FORBIDDEN_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
1613
|
+
aws_default = {
|
|
1614
|
+
name: "aws",
|
|
1615
|
+
description: "Protects AWS infrastructure from destructive AI operations",
|
|
1616
|
+
aliases: ["amazon"],
|
|
1617
|
+
smartRules: [
|
|
1618
|
+
{
|
|
1619
|
+
name: "shield:aws:block-delete-s3-bucket",
|
|
1620
|
+
tool: "*",
|
|
1621
|
+
conditions: [
|
|
1622
|
+
{
|
|
1623
|
+
field: "command",
|
|
1624
|
+
op: "matches",
|
|
1625
|
+
value: "aws\\s+s3.*rb\\s|aws\\s+s3api\\s+delete-bucket",
|
|
1626
|
+
flags: "i"
|
|
1627
|
+
}
|
|
1628
|
+
],
|
|
1629
|
+
verdict: "block",
|
|
1630
|
+
reason: "S3 bucket deletion is irreversible \u2014 blocked by AWS shield"
|
|
1631
|
+
},
|
|
1632
|
+
{
|
|
1633
|
+
name: "shield:aws:review-iam-changes",
|
|
1634
|
+
tool: "*",
|
|
1635
|
+
conditions: [
|
|
1636
|
+
{
|
|
1637
|
+
field: "command",
|
|
1638
|
+
op: "matches",
|
|
1639
|
+
value: "aws\\s+iam\\s+(create|delete|attach|detach|put|remove)",
|
|
1640
|
+
flags: "i"
|
|
1641
|
+
}
|
|
1642
|
+
],
|
|
1643
|
+
verdict: "review",
|
|
1644
|
+
reason: "IAM changes require human approval (AWS shield)"
|
|
1645
|
+
},
|
|
1646
|
+
{
|
|
1647
|
+
name: "shield:aws:block-ec2-terminate",
|
|
1648
|
+
tool: "*",
|
|
1649
|
+
conditions: [
|
|
1650
|
+
{
|
|
1651
|
+
field: "command",
|
|
1652
|
+
op: "matches",
|
|
1653
|
+
value: "aws\\s+ec2\\s+terminate-instances",
|
|
1654
|
+
flags: "i"
|
|
1655
|
+
}
|
|
1656
|
+
],
|
|
1657
|
+
verdict: "block",
|
|
1658
|
+
reason: "EC2 instance termination is irreversible \u2014 blocked by AWS shield"
|
|
1659
|
+
},
|
|
1660
|
+
{
|
|
1661
|
+
name: "shield:aws:review-rds-delete",
|
|
1662
|
+
tool: "*",
|
|
1663
|
+
conditions: [
|
|
1664
|
+
{ field: "command", op: "matches", value: "aws\\s+rds\\s+delete-", flags: "i" }
|
|
1665
|
+
],
|
|
1666
|
+
verdict: "review",
|
|
1667
|
+
reason: "RDS deletion requires human approval (AWS shield)"
|
|
1668
|
+
}
|
|
1669
|
+
],
|
|
1670
|
+
dangerousWords: []
|
|
1671
|
+
};
|
|
1672
|
+
bash_safe_default = {
|
|
1673
|
+
name: "bash-safe",
|
|
1674
|
+
description: "Blocks high-risk bash patterns: pipe-to-shell, rm -rf /, disk overwrites, eval",
|
|
1675
|
+
aliases: ["bash", "shell"],
|
|
1676
|
+
smartRules: [
|
|
1677
|
+
{
|
|
1678
|
+
name: "shield:bash-safe:block-pipe-to-shell",
|
|
1679
|
+
tool: "bash",
|
|
1680
|
+
conditions: [
|
|
1681
|
+
{
|
|
1682
|
+
field: "command",
|
|
1683
|
+
op: "matches",
|
|
1684
|
+
value: "(^|&&|\\|\\||;)\\s*(curl|wget)\\s+[^|]*\\|\\s*(?:(bash|sh|zsh|fish)|(python3?|ruby|perl|node)\\b(?!\\s+-[cem]\\b))",
|
|
1685
|
+
flags: "i"
|
|
1686
|
+
}
|
|
1687
|
+
],
|
|
1688
|
+
verdict: "block",
|
|
1689
|
+
reason: "Pipe-to-shell is a common supply-chain attack vector \u2014 blocked by bash-safe shield"
|
|
1690
|
+
},
|
|
1691
|
+
{
|
|
1692
|
+
name: "shield:bash-safe:block-obfuscated-exec",
|
|
1693
|
+
tool: "bash",
|
|
1694
|
+
conditions: [
|
|
1695
|
+
{
|
|
1696
|
+
field: "command",
|
|
1697
|
+
op: "matches",
|
|
1698
|
+
value: "\\bbase64\\s+(-d|--decode)[^|;&]*\\|\\s*(bash|sh|zsh)",
|
|
1699
|
+
flags: "i"
|
|
1700
|
+
}
|
|
1701
|
+
],
|
|
1702
|
+
verdict: "block",
|
|
1703
|
+
reason: "Obfuscated execution via base64 decode \u2014 blocked by bash-safe shield"
|
|
1704
|
+
},
|
|
1705
|
+
{
|
|
1706
|
+
name: "shield:bash-safe:block-rm-root",
|
|
1707
|
+
tool: "bash",
|
|
1708
|
+
conditions: [
|
|
1709
|
+
{
|
|
1710
|
+
field: "command",
|
|
1711
|
+
op: "matches",
|
|
1712
|
+
value: "rm\\s+(-[a-zA-Z]*r[a-zA-Z]*f|-[a-zA-Z]*f[a-zA-Z]*r)[a-zA-Z]*\\s+(\\/|~|\\$HOME|\\$\\{HOME\\})\\s*$",
|
|
1713
|
+
flags: "i"
|
|
1714
|
+
}
|
|
1715
|
+
],
|
|
1716
|
+
verdict: "block",
|
|
1717
|
+
reason: "rm -rf of root or home directory is catastrophic \u2014 blocked by bash-safe shield"
|
|
1718
|
+
},
|
|
1719
|
+
{
|
|
1720
|
+
name: "shield:bash-safe:block-disk-overwrite",
|
|
1721
|
+
tool: "bash",
|
|
1722
|
+
conditions: [
|
|
1723
|
+
{
|
|
1724
|
+
field: "command",
|
|
1725
|
+
op: "matches",
|
|
1726
|
+
value: "(^|&&|\\|\\||;)\\s*dd\\s+.*of=\\/dev\\/(sd|nvme|hd|vd|xvd)",
|
|
1727
|
+
flags: "i"
|
|
1728
|
+
}
|
|
1729
|
+
],
|
|
1730
|
+
verdict: "block",
|
|
1731
|
+
reason: "Writing directly to a block device is irreversible \u2014 blocked by bash-safe shield"
|
|
1732
|
+
},
|
|
1733
|
+
{
|
|
1734
|
+
name: "shield:bash-safe:block-eval-remote",
|
|
1735
|
+
tool: "bash",
|
|
1736
|
+
conditions: [
|
|
1737
|
+
{
|
|
1738
|
+
field: "command",
|
|
1739
|
+
op: "matches",
|
|
1740
|
+
value: "(^|&&|\\|\\||;)\\s*eval\\s+.*\\$\\((curl|wget)\\b",
|
|
1741
|
+
flags: "i"
|
|
1742
|
+
}
|
|
1743
|
+
],
|
|
1744
|
+
verdict: "block",
|
|
1745
|
+
reason: "eval of remote download is a near-certain supply-chain attack \u2014 blocked by bash-safe shield"
|
|
1746
|
+
},
|
|
1747
|
+
{
|
|
1748
|
+
name: "shield:bash-safe:review-eval-dynamic",
|
|
1749
|
+
tool: "bash",
|
|
1750
|
+
conditions: [
|
|
1751
|
+
{
|
|
1752
|
+
field: "command",
|
|
1753
|
+
op: "matches",
|
|
1754
|
+
value: '(^|&&|\\|\\||[;|\\n{(`])\\s*eval\\s+([\\$`(]|"[^"]*\\$)',
|
|
1755
|
+
flags: "i"
|
|
1756
|
+
}
|
|
1757
|
+
],
|
|
1758
|
+
verdict: "review",
|
|
1759
|
+
reason: "eval of dynamic content \u2014 backup regex rule for scan path (real-time uses AST detection)"
|
|
1760
|
+
}
|
|
1761
|
+
],
|
|
1762
|
+
dangerousWords: []
|
|
1763
|
+
};
|
|
1764
|
+
docker_default = {
|
|
1765
|
+
name: "docker",
|
|
1766
|
+
description: "Protects Docker environments from destructive AI operations",
|
|
1767
|
+
aliases: [],
|
|
1768
|
+
smartRules: [
|
|
1769
|
+
{
|
|
1770
|
+
name: "shield:docker:block-system-prune",
|
|
1771
|
+
tool: "*",
|
|
1772
|
+
conditions: [
|
|
1773
|
+
{
|
|
1774
|
+
field: "command",
|
|
1775
|
+
op: "matches",
|
|
1776
|
+
value: "docker\\s+system\\s+prune",
|
|
1777
|
+
flags: "i"
|
|
1778
|
+
}
|
|
1779
|
+
],
|
|
1780
|
+
verdict: "block",
|
|
1781
|
+
reason: "docker system prune removes all unused containers, images, and volumes \u2014 blocked by Docker shield"
|
|
1782
|
+
},
|
|
1783
|
+
{
|
|
1784
|
+
name: "shield:docker:block-volume-prune",
|
|
1785
|
+
tool: "*",
|
|
1786
|
+
conditions: [
|
|
1787
|
+
{
|
|
1788
|
+
field: "command",
|
|
1789
|
+
op: "matches",
|
|
1790
|
+
value: "docker\\s+volume\\s+prune",
|
|
1791
|
+
flags: "i"
|
|
1792
|
+
}
|
|
1793
|
+
],
|
|
1794
|
+
verdict: "block",
|
|
1795
|
+
reason: "docker volume prune destroys all unused volumes and their data \u2014 blocked by Docker shield"
|
|
1796
|
+
},
|
|
1797
|
+
{
|
|
1798
|
+
name: "shield:docker:block-rm-force",
|
|
1799
|
+
tool: "*",
|
|
1800
|
+
conditionMode: "all",
|
|
1801
|
+
conditions: [
|
|
1802
|
+
{
|
|
1803
|
+
field: "command",
|
|
1804
|
+
op: "matches",
|
|
1805
|
+
value: "docker\\s+rm\\b",
|
|
1806
|
+
flags: "i"
|
|
1807
|
+
},
|
|
1808
|
+
{
|
|
1809
|
+
field: "command",
|
|
1810
|
+
op: "matches",
|
|
1811
|
+
value: "(^|\\s)(-f|--force)(\\s|$)",
|
|
1812
|
+
flags: "i"
|
|
1813
|
+
}
|
|
1814
|
+
],
|
|
1815
|
+
verdict: "block",
|
|
1816
|
+
reason: "Force-removing running containers is destructive \u2014 blocked by Docker shield"
|
|
1817
|
+
},
|
|
1818
|
+
{
|
|
1819
|
+
name: "shield:docker:review-volume-rm",
|
|
1820
|
+
tool: "*",
|
|
1821
|
+
conditions: [
|
|
1822
|
+
{
|
|
1823
|
+
field: "command",
|
|
1824
|
+
op: "matches",
|
|
1825
|
+
value: "docker\\s+volume\\s+rm\\s+",
|
|
1826
|
+
flags: "i"
|
|
1827
|
+
}
|
|
1828
|
+
],
|
|
1829
|
+
verdict: "review",
|
|
1830
|
+
reason: "Volume removal deletes persistent data and requires human approval (Docker shield)"
|
|
1831
|
+
},
|
|
1832
|
+
{
|
|
1833
|
+
name: "shield:docker:review-stop-kill",
|
|
1834
|
+
tool: "*",
|
|
1835
|
+
conditions: [
|
|
1836
|
+
{
|
|
1837
|
+
field: "command",
|
|
1838
|
+
op: "matches",
|
|
1839
|
+
value: "docker\\s+(stop|kill)\\s+",
|
|
1840
|
+
flags: "i"
|
|
1841
|
+
}
|
|
1842
|
+
],
|
|
1843
|
+
verdict: "review",
|
|
1844
|
+
reason: "Stopping or killing containers requires human approval (Docker shield)"
|
|
1845
|
+
},
|
|
1846
|
+
{
|
|
1847
|
+
name: "shield:docker:review-image-rm",
|
|
1848
|
+
tool: "*",
|
|
1849
|
+
conditions: [
|
|
1850
|
+
{
|
|
1851
|
+
field: "command",
|
|
1852
|
+
op: "matches",
|
|
1853
|
+
value: "docker\\s+image\\s+rm\\b",
|
|
1854
|
+
flags: "i"
|
|
1855
|
+
}
|
|
1856
|
+
],
|
|
1857
|
+
verdict: "review",
|
|
1858
|
+
reason: "Image removal requires human approval (Docker shield)"
|
|
1859
|
+
},
|
|
1860
|
+
{
|
|
1861
|
+
name: "shield:docker:review-rmi-force",
|
|
1862
|
+
tool: "*",
|
|
1863
|
+
conditionMode: "all",
|
|
1864
|
+
conditions: [
|
|
1865
|
+
{
|
|
1866
|
+
field: "command",
|
|
1867
|
+
op: "matches",
|
|
1868
|
+
value: "docker\\s+rmi\\b",
|
|
1869
|
+
flags: "i"
|
|
1870
|
+
},
|
|
1871
|
+
{
|
|
1872
|
+
field: "command",
|
|
1873
|
+
op: "matches",
|
|
1874
|
+
value: "(^|\\s)(-f|--force)(\\s|$)",
|
|
1875
|
+
flags: "i"
|
|
1876
|
+
}
|
|
1877
|
+
],
|
|
1878
|
+
verdict: "review",
|
|
1879
|
+
reason: "Force image removal requires human approval (Docker shield)"
|
|
1880
|
+
}
|
|
1881
|
+
],
|
|
1882
|
+
dangerousWords: []
|
|
1883
|
+
};
|
|
1884
|
+
filesystem_default = {
|
|
1885
|
+
name: "filesystem",
|
|
1886
|
+
description: "Protects the local filesystem from dangerous AI operations",
|
|
1887
|
+
aliases: ["fs"],
|
|
1888
|
+
smartRules: [
|
|
1889
|
+
{
|
|
1890
|
+
name: "shield:filesystem:review-chmod-777",
|
|
1891
|
+
tool: "bash",
|
|
1892
|
+
conditions: [
|
|
1893
|
+
{ field: "command", op: "matches", value: "chmod\\s+(777|a\\+rwx)", flags: "i" }
|
|
1894
|
+
],
|
|
1895
|
+
verdict: "review",
|
|
1896
|
+
reason: "chmod 777 requires human approval (filesystem shield)"
|
|
1897
|
+
},
|
|
1898
|
+
{
|
|
1899
|
+
name: "shield:filesystem:review-write-etc",
|
|
1900
|
+
tool: "bash",
|
|
1901
|
+
conditions: [
|
|
1902
|
+
{
|
|
1903
|
+
field: "command",
|
|
1904
|
+
op: "matches",
|
|
1905
|
+
value: "(tee|\\bcp\\b|\\bmv\\b|install|>+)\\s+.*\\/etc\\/"
|
|
1906
|
+
}
|
|
1907
|
+
],
|
|
1908
|
+
verdict: "review",
|
|
1909
|
+
reason: "Writing to /etc requires human approval (filesystem shield)"
|
|
1910
|
+
}
|
|
1911
|
+
],
|
|
1912
|
+
dangerousWords: ["wipefs"]
|
|
1913
|
+
};
|
|
1914
|
+
github_default = {
|
|
1915
|
+
name: "github",
|
|
1916
|
+
description: "Protects GitHub repositories from destructive AI operations",
|
|
1917
|
+
aliases: ["git"],
|
|
1918
|
+
smartRules: [
|
|
1919
|
+
{
|
|
1920
|
+
name: "shield:github:review-delete-branch-remote",
|
|
1921
|
+
tool: "bash",
|
|
1922
|
+
conditions: [
|
|
1923
|
+
{ field: "command", op: "matches", value: "git\\s+push\\s+.*--delete", flags: "i" }
|
|
1924
|
+
],
|
|
1925
|
+
verdict: "review",
|
|
1926
|
+
reason: "Remote branch deletion requires human approval (GitHub shield)"
|
|
1927
|
+
},
|
|
1928
|
+
{
|
|
1929
|
+
name: "shield:github:block-delete-repo",
|
|
1930
|
+
tool: "*",
|
|
1931
|
+
conditions: [
|
|
1932
|
+
{ field: "command", op: "matches", value: "gh\\s+repo\\s+delete", flags: "i" }
|
|
1933
|
+
],
|
|
1934
|
+
verdict: "block",
|
|
1935
|
+
reason: "Repository deletion is irreversible \u2014 blocked by GitHub shield"
|
|
1936
|
+
}
|
|
1937
|
+
],
|
|
1938
|
+
dangerousWords: []
|
|
1939
|
+
};
|
|
1940
|
+
k8s_default = {
|
|
1941
|
+
name: "k8s",
|
|
1942
|
+
description: "Protects Kubernetes clusters from destructive AI operations",
|
|
1943
|
+
aliases: ["kubernetes", "kubectl"],
|
|
1944
|
+
smartRules: [
|
|
1945
|
+
{
|
|
1946
|
+
name: "shield:k8s:block-delete-namespace",
|
|
1947
|
+
tool: "*",
|
|
1948
|
+
conditions: [
|
|
1949
|
+
{
|
|
1950
|
+
field: "command",
|
|
1951
|
+
op: "matches",
|
|
1952
|
+
value: "kubectl\\s+delete\\s+(ns|namespace)\\s+",
|
|
1953
|
+
flags: "i"
|
|
1954
|
+
}
|
|
1955
|
+
],
|
|
1956
|
+
verdict: "block",
|
|
1957
|
+
reason: "Deleting a namespace destroys all resources inside it \u2014 blocked by k8s shield"
|
|
1958
|
+
},
|
|
1959
|
+
{
|
|
1960
|
+
name: "shield:k8s:block-delete-all",
|
|
1961
|
+
tool: "*",
|
|
1962
|
+
conditions: [
|
|
1963
|
+
{
|
|
1964
|
+
field: "command",
|
|
1965
|
+
op: "matches",
|
|
1966
|
+
value: "kubectl\\s+delete\\s+.*--all\\b",
|
|
1967
|
+
flags: "i"
|
|
1968
|
+
}
|
|
1969
|
+
],
|
|
1970
|
+
verdict: "block",
|
|
1971
|
+
reason: "kubectl delete --all is irreversible \u2014 blocked by k8s shield"
|
|
1972
|
+
},
|
|
1973
|
+
{
|
|
1974
|
+
name: "shield:k8s:block-helm-uninstall",
|
|
1975
|
+
tool: "*",
|
|
1976
|
+
conditions: [
|
|
1977
|
+
{
|
|
1978
|
+
field: "command",
|
|
1979
|
+
op: "matches",
|
|
1980
|
+
value: "helm\\s+(uninstall|delete|del)\\s+",
|
|
1981
|
+
flags: "i"
|
|
1982
|
+
}
|
|
1983
|
+
],
|
|
1984
|
+
verdict: "block",
|
|
1985
|
+
reason: "helm uninstall removes a release and its resources \u2014 blocked by k8s shield"
|
|
1986
|
+
},
|
|
1987
|
+
{
|
|
1988
|
+
name: "shield:k8s:review-scale-zero",
|
|
1989
|
+
tool: "*",
|
|
1990
|
+
conditions: [
|
|
1991
|
+
{
|
|
1992
|
+
field: "command",
|
|
1993
|
+
op: "matches",
|
|
1994
|
+
value: "kubectl\\s+scale\\s+.*--replicas=0",
|
|
1995
|
+
flags: "i"
|
|
1996
|
+
}
|
|
1997
|
+
],
|
|
1998
|
+
verdict: "review",
|
|
1999
|
+
reason: "Scaling to zero takes down a workload and requires human approval (k8s shield)"
|
|
2000
|
+
},
|
|
2001
|
+
{
|
|
2002
|
+
name: "shield:k8s:review-delete-deployment",
|
|
2003
|
+
tool: "*",
|
|
2004
|
+
conditions: [
|
|
2005
|
+
{
|
|
2006
|
+
field: "command",
|
|
2007
|
+
op: "matches",
|
|
2008
|
+
value: "kubectl\\s+delete\\s+(deployment|deploy|statefulset|sts|daemonset|ds)\\s+",
|
|
2009
|
+
flags: "i"
|
|
2010
|
+
}
|
|
2011
|
+
],
|
|
2012
|
+
verdict: "review",
|
|
2013
|
+
reason: "Deleting a workload requires human approval (k8s shield)"
|
|
2014
|
+
},
|
|
2015
|
+
{
|
|
2016
|
+
name: "shield:k8s:review-apply-force",
|
|
2017
|
+
tool: "*",
|
|
2018
|
+
conditions: [
|
|
2019
|
+
{
|
|
2020
|
+
field: "command",
|
|
2021
|
+
op: "matches",
|
|
2022
|
+
value: "kubectl\\s+(apply|replace)\\s+.*--force",
|
|
2023
|
+
flags: "i"
|
|
2024
|
+
}
|
|
2025
|
+
],
|
|
2026
|
+
verdict: "review",
|
|
2027
|
+
reason: "Force-apply overwrites live resources and requires human approval (k8s shield)"
|
|
2028
|
+
}
|
|
2029
|
+
],
|
|
2030
|
+
dangerousWords: []
|
|
2031
|
+
};
|
|
2032
|
+
mongodb_default = {
|
|
2033
|
+
name: "mongodb",
|
|
2034
|
+
description: "Protects MongoDB databases from destructive AI operations",
|
|
2035
|
+
aliases: ["mongo"],
|
|
2036
|
+
smartRules: [
|
|
2037
|
+
{
|
|
2038
|
+
name: "shield:mongodb:block-drop-database",
|
|
2039
|
+
tool: "*",
|
|
2040
|
+
conditions: [
|
|
2041
|
+
{
|
|
2042
|
+
field: "command",
|
|
2043
|
+
op: "matches",
|
|
2044
|
+
value: "\\.dropDatabase\\s*\\(",
|
|
2045
|
+
flags: "i"
|
|
2046
|
+
}
|
|
2047
|
+
],
|
|
2048
|
+
verdict: "block",
|
|
2049
|
+
reason: "dropDatabase is irreversible \u2014 blocked by MongoDB shield"
|
|
2050
|
+
},
|
|
2051
|
+
{
|
|
2052
|
+
name: "shield:mongodb:block-drop-collection",
|
|
2053
|
+
tool: "*",
|
|
2054
|
+
conditions: [
|
|
2055
|
+
{
|
|
2056
|
+
field: "command",
|
|
2057
|
+
op: "matches",
|
|
2058
|
+
value: "\\.drop\\s*\\(|db\\.getCollection\\([^)]+\\)\\.drop\\s*\\(",
|
|
2059
|
+
flags: "i"
|
|
2060
|
+
}
|
|
2061
|
+
],
|
|
2062
|
+
verdict: "block",
|
|
2063
|
+
reason: "Collection drop is irreversible \u2014 blocked by MongoDB shield"
|
|
2064
|
+
},
|
|
2065
|
+
{
|
|
2066
|
+
name: "shield:mongodb:block-delete-many-empty-filter",
|
|
2067
|
+
tool: "*",
|
|
2068
|
+
conditions: [
|
|
2069
|
+
{
|
|
2070
|
+
field: "command",
|
|
2071
|
+
op: "matches",
|
|
2072
|
+
value: "\\.deleteMany\\s*\\(\\s*\\{\\s*\\}\\s*\\)",
|
|
2073
|
+
flags: "i"
|
|
2074
|
+
}
|
|
2075
|
+
],
|
|
2076
|
+
verdict: "block",
|
|
2077
|
+
reason: "deleteMany({}) with empty filter wipes the entire collection \u2014 blocked by MongoDB shield"
|
|
2078
|
+
},
|
|
2079
|
+
{
|
|
2080
|
+
name: "shield:mongodb:review-delete-many",
|
|
2081
|
+
tool: "*",
|
|
2082
|
+
conditions: [
|
|
2083
|
+
{
|
|
2084
|
+
field: "command",
|
|
2085
|
+
op: "matches",
|
|
2086
|
+
value: "\\.deleteMany\\s*\\(",
|
|
2087
|
+
flags: "i"
|
|
2088
|
+
}
|
|
2089
|
+
],
|
|
2090
|
+
verdict: "review",
|
|
2091
|
+
reason: "deleteMany requires human approval (MongoDB shield)"
|
|
2092
|
+
},
|
|
2093
|
+
{
|
|
2094
|
+
name: "shield:mongodb:review-drop-index",
|
|
2095
|
+
tool: "*",
|
|
2096
|
+
conditions: [
|
|
2097
|
+
{
|
|
2098
|
+
field: "command",
|
|
2099
|
+
op: "matches",
|
|
2100
|
+
value: "\\.dropIndex\\s*\\(|\\.dropIndexes\\s*\\(",
|
|
2101
|
+
flags: "i"
|
|
2102
|
+
}
|
|
2103
|
+
],
|
|
2104
|
+
verdict: "review",
|
|
2105
|
+
reason: "Index drops affect query performance and require human approval (MongoDB shield)"
|
|
2106
|
+
}
|
|
2107
|
+
],
|
|
2108
|
+
dangerousWords: ["dropDatabase", "dropCollection", "mongodrop"]
|
|
2109
|
+
};
|
|
2110
|
+
postgres_default = {
|
|
2111
|
+
name: "postgres",
|
|
2112
|
+
description: "Protects PostgreSQL databases from destructive AI operations",
|
|
2113
|
+
aliases: ["pg", "postgresql"],
|
|
2114
|
+
smartRules: [
|
|
2115
|
+
{
|
|
2116
|
+
name: "shield:postgres:block-drop-table",
|
|
2117
|
+
tool: "*",
|
|
2118
|
+
conditions: [{ field: "sql", op: "matches", value: "DROP\\s+TABLE", flags: "i" }],
|
|
2119
|
+
verdict: "block",
|
|
2120
|
+
reason: "DROP TABLE is irreversible \u2014 blocked by Postgres shield"
|
|
2121
|
+
},
|
|
2122
|
+
{
|
|
2123
|
+
name: "shield:postgres:block-truncate",
|
|
2124
|
+
tool: "*",
|
|
2125
|
+
conditions: [
|
|
2126
|
+
{ field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }
|
|
2127
|
+
],
|
|
2128
|
+
verdict: "block",
|
|
2129
|
+
reason: "TRUNCATE is irreversible \u2014 blocked by Postgres shield"
|
|
2130
|
+
},
|
|
2131
|
+
{
|
|
2132
|
+
name: "shield:postgres:block-drop-column",
|
|
2133
|
+
tool: "*",
|
|
2134
|
+
conditions: [
|
|
2135
|
+
{ field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
|
|
2136
|
+
],
|
|
2137
|
+
verdict: "block",
|
|
2138
|
+
reason: "DROP COLUMN is irreversible \u2014 blocked by Postgres shield"
|
|
2139
|
+
},
|
|
2140
|
+
{
|
|
2141
|
+
name: "shield:postgres:review-grant-revoke",
|
|
2142
|
+
tool: "*",
|
|
2143
|
+
conditions: [
|
|
2144
|
+
{ field: "sql", op: "matches", value: "\\b(GRANT|REVOKE)\\b", flags: "i" }
|
|
2145
|
+
],
|
|
2146
|
+
verdict: "review",
|
|
2147
|
+
reason: "Permission changes require human approval (Postgres shield)"
|
|
2148
|
+
}
|
|
2149
|
+
],
|
|
2150
|
+
dangerousWords: ["dropdb", "pg_dropcluster"]
|
|
2151
|
+
};
|
|
2152
|
+
project_jail_default = {
|
|
2153
|
+
name: "project-jail",
|
|
2154
|
+
description: "Restricts AI agents from reading sensitive credential files outside the current project",
|
|
2155
|
+
aliases: ["jail"],
|
|
2156
|
+
smartRules: [
|
|
2157
|
+
{
|
|
2158
|
+
name: "shield:project-jail:block-read-ssh",
|
|
2159
|
+
tool: "bash",
|
|
2160
|
+
conditions: [
|
|
2161
|
+
{
|
|
2162
|
+
field: "command",
|
|
2163
|
+
op: "matches",
|
|
2164
|
+
value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*[\\/\\\\]\\.ssh[\\/\\\\]",
|
|
2165
|
+
flags: "i"
|
|
2166
|
+
}
|
|
2167
|
+
],
|
|
2168
|
+
verdict: "block",
|
|
2169
|
+
reason: "Reading SSH private keys is blocked by project-jail shield"
|
|
2170
|
+
},
|
|
2171
|
+
{
|
|
2172
|
+
name: "shield:project-jail:block-read-aws",
|
|
2173
|
+
tool: "bash",
|
|
2174
|
+
conditions: [
|
|
2175
|
+
{
|
|
2176
|
+
field: "command",
|
|
2177
|
+
op: "matches",
|
|
2178
|
+
value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*[\\/\\\\]\\.aws[\\/\\\\]",
|
|
2179
|
+
flags: "i"
|
|
2180
|
+
}
|
|
2181
|
+
],
|
|
2182
|
+
verdict: "block",
|
|
2183
|
+
reason: "Reading AWS credentials is blocked by project-jail shield"
|
|
2184
|
+
},
|
|
2185
|
+
{
|
|
2186
|
+
name: "shield:project-jail:block-read-env",
|
|
2187
|
+
tool: "bash",
|
|
2188
|
+
conditions: [
|
|
2189
|
+
{
|
|
2190
|
+
field: "command",
|
|
2191
|
+
op: "matches",
|
|
2192
|
+
value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*\\.env(\\.local|\\.production|\\.staging)?\\b",
|
|
2193
|
+
flags: "i"
|
|
2194
|
+
}
|
|
2195
|
+
],
|
|
2196
|
+
verdict: "block",
|
|
2197
|
+
reason: "Reading .env files is blocked by project-jail shield"
|
|
2198
|
+
},
|
|
2199
|
+
{
|
|
2200
|
+
name: "shield:project-jail:block-read-credentials",
|
|
2201
|
+
tool: "bash",
|
|
2202
|
+
conditions: [
|
|
2203
|
+
{
|
|
2204
|
+
field: "command",
|
|
2205
|
+
op: "matches",
|
|
2206
|
+
value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*(credentials\\.json|\\.netrc|\\.npmrc|\\.docker[\\/\\\\]config\\.json|gcloud[\\/\\\\]credentials)",
|
|
2207
|
+
flags: "i"
|
|
2208
|
+
}
|
|
2209
|
+
],
|
|
2210
|
+
verdict: "block",
|
|
2211
|
+
reason: "Reading credential files is blocked by project-jail shield"
|
|
2212
|
+
}
|
|
2213
|
+
],
|
|
2214
|
+
dangerousWords: []
|
|
2215
|
+
};
|
|
2216
|
+
redis_default = {
|
|
2217
|
+
name: "redis",
|
|
2218
|
+
description: "Protects Redis instances from destructive AI operations",
|
|
2219
|
+
aliases: [],
|
|
2220
|
+
smartRules: [
|
|
2221
|
+
{
|
|
2222
|
+
name: "shield:redis:block-flushall",
|
|
2223
|
+
tool: "*",
|
|
2224
|
+
conditions: [
|
|
2225
|
+
{
|
|
2226
|
+
field: "command",
|
|
2227
|
+
op: "matches",
|
|
2228
|
+
value: "\\bFLUSHALL\\b",
|
|
2229
|
+
flags: "i"
|
|
2230
|
+
}
|
|
2231
|
+
],
|
|
2232
|
+
verdict: "block",
|
|
2233
|
+
reason: "FLUSHALL deletes every key in every database \u2014 blocked by Redis shield"
|
|
2234
|
+
},
|
|
2235
|
+
{
|
|
2236
|
+
name: "shield:redis:block-flushdb",
|
|
2237
|
+
tool: "*",
|
|
2238
|
+
conditions: [
|
|
2239
|
+
{
|
|
2240
|
+
field: "command",
|
|
2241
|
+
op: "matches",
|
|
2242
|
+
value: "\\bFLUSHDB\\b",
|
|
2243
|
+
flags: "i"
|
|
2244
|
+
}
|
|
2245
|
+
],
|
|
2246
|
+
verdict: "block",
|
|
2247
|
+
reason: "FLUSHDB deletes all keys in the current database \u2014 blocked by Redis shield"
|
|
2248
|
+
},
|
|
2249
|
+
{
|
|
2250
|
+
name: "shield:redis:block-config-resetstat",
|
|
2251
|
+
tool: "*",
|
|
2252
|
+
conditions: [
|
|
2253
|
+
{
|
|
2254
|
+
field: "command",
|
|
2255
|
+
op: "matches",
|
|
2256
|
+
value: "\\bCONFIG\\s+RESETSTAT\\b",
|
|
2257
|
+
flags: "i"
|
|
2258
|
+
}
|
|
2259
|
+
],
|
|
2260
|
+
verdict: "block",
|
|
2261
|
+
reason: "CONFIG RESETSTAT resets server statistics irreversibly \u2014 blocked by Redis shield"
|
|
2262
|
+
},
|
|
2263
|
+
{
|
|
2264
|
+
name: "shield:redis:review-config-set",
|
|
2265
|
+
tool: "*",
|
|
2266
|
+
conditions: [
|
|
2267
|
+
{
|
|
2268
|
+
field: "command",
|
|
2269
|
+
op: "matches",
|
|
2270
|
+
value: "\\bCONFIG\\s+SET\\b",
|
|
2271
|
+
flags: "i"
|
|
2272
|
+
}
|
|
2273
|
+
],
|
|
2274
|
+
verdict: "review",
|
|
2275
|
+
reason: "CONFIG SET changes live server configuration and requires human approval (Redis shield)"
|
|
2276
|
+
},
|
|
2277
|
+
{
|
|
2278
|
+
name: "shield:redis:review-del-wildcard",
|
|
2279
|
+
tool: "*",
|
|
2280
|
+
conditions: [
|
|
2281
|
+
{
|
|
2282
|
+
field: "command",
|
|
2283
|
+
op: "matches",
|
|
2284
|
+
value: "\\bDEL\\b.*[*?\\[]|redis-cli.*--scan.*\\|.*xargs.*del",
|
|
2285
|
+
flags: "i"
|
|
2286
|
+
}
|
|
2287
|
+
],
|
|
2288
|
+
verdict: "review",
|
|
2289
|
+
reason: "Wildcard key deletion requires human approval (Redis shield)"
|
|
2290
|
+
}
|
|
2291
|
+
],
|
|
2292
|
+
dangerousWords: ["FLUSHALL", "FLUSHDB"]
|
|
2293
|
+
};
|
|
2294
|
+
BUILTIN_SHIELDS = {
|
|
2295
|
+
[aws_default.name]: aws_default,
|
|
2296
|
+
[bash_safe_default.name]: bash_safe_default,
|
|
2297
|
+
[docker_default.name]: docker_default,
|
|
2298
|
+
[filesystem_default.name]: filesystem_default,
|
|
2299
|
+
[github_default.name]: github_default,
|
|
2300
|
+
[k8s_default.name]: k8s_default,
|
|
2301
|
+
[mongodb_default.name]: mongodb_default,
|
|
2302
|
+
[postgres_default.name]: postgres_default,
|
|
2303
|
+
[project_jail_default.name]: project_jail_default,
|
|
2304
|
+
[redis_default.name]: redis_default
|
|
2305
|
+
};
|
|
2306
|
+
assertBuiltinShieldRegexesAreSafe();
|
|
2307
|
+
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;
|
|
2308
|
+
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;
|
|
2309
|
+
FILE_TOOLS = /* @__PURE__ */ new Set([
|
|
2310
|
+
"read",
|
|
2311
|
+
"read_file",
|
|
2312
|
+
"edit",
|
|
2313
|
+
"edit_file",
|
|
2314
|
+
"write",
|
|
2315
|
+
"write_file",
|
|
2316
|
+
"multiedit",
|
|
2317
|
+
"grep",
|
|
2318
|
+
"grep_search",
|
|
2319
|
+
"glob",
|
|
2320
|
+
"list_files"
|
|
2321
|
+
]);
|
|
2322
|
+
PII_EMAIL_RE = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/;
|
|
2323
|
+
PII_SSN_RE = /\b\d{3}-\d{2}-\d{4}\b/;
|
|
2324
|
+
PII_PHONE_RE = /\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]\d{3}[-.\s]\d{4}\b/;
|
|
2325
|
+
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/;
|
|
2326
|
+
LONG_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
|
|
2327
|
+
}
|
|
2328
|
+
});
|
|
2329
|
+
|
|
2330
|
+
// src/dlp.ts
|
|
2331
|
+
var init_dlp = __esm({
|
|
2332
|
+
"src/dlp.ts"() {
|
|
2333
|
+
"use strict";
|
|
2334
|
+
init_dist();
|
|
2335
|
+
init_dist();
|
|
2336
|
+
}
|
|
2337
|
+
});
|
|
2338
|
+
|
|
2339
|
+
// src/cli/commands/blast.ts
|
|
2340
|
+
import chalk from "chalk";
|
|
2341
|
+
import fs from "fs";
|
|
2342
|
+
import path from "path";
|
|
2343
|
+
import os from "os";
|
|
2344
|
+
function buildSensitivePaths(home, cwd) {
|
|
2345
|
+
return [
|
|
2346
|
+
{
|
|
2347
|
+
full: path.join(home, ".ssh", "id_rsa"),
|
|
2348
|
+
label: "~/.ssh/id_rsa",
|
|
2349
|
+
description: "RSA private key \u2014 grants SSH access to your servers",
|
|
2350
|
+
score: 20
|
|
2351
|
+
},
|
|
2352
|
+
{
|
|
2353
|
+
full: path.join(home, ".ssh", "id_ed25519"),
|
|
2354
|
+
label: "~/.ssh/id_ed25519",
|
|
2355
|
+
description: "Ed25519 private key \u2014 grants SSH access to your servers",
|
|
2356
|
+
score: 20
|
|
2357
|
+
},
|
|
2358
|
+
{
|
|
2359
|
+
full: path.join(home, ".ssh", "id_ecdsa"),
|
|
2360
|
+
label: "~/.ssh/id_ecdsa",
|
|
2361
|
+
description: "ECDSA private key \u2014 grants SSH access to your servers",
|
|
2362
|
+
score: 20
|
|
2363
|
+
},
|
|
2364
|
+
{
|
|
2365
|
+
full: path.join(home, ".aws", "credentials"),
|
|
2366
|
+
label: "~/.aws/credentials",
|
|
2367
|
+
description: "AWS access keys \u2014 full cloud account access",
|
|
2368
|
+
score: 20
|
|
2369
|
+
},
|
|
2370
|
+
{
|
|
2371
|
+
full: path.join(home, ".aws", "config"),
|
|
2372
|
+
label: "~/.aws/config",
|
|
2373
|
+
description: "AWS configuration \u2014 account and region settings",
|
|
2374
|
+
score: 5
|
|
2375
|
+
},
|
|
2376
|
+
{
|
|
2377
|
+
full: path.join(home, ".config", "gcloud", "credentials.db"),
|
|
2378
|
+
label: "~/.config/gcloud/credentials.db",
|
|
2379
|
+
description: "Google Cloud credentials",
|
|
2380
|
+
score: 15
|
|
2381
|
+
},
|
|
2382
|
+
{
|
|
2383
|
+
full: path.join(home, ".docker", "config.json"),
|
|
2384
|
+
label: "~/.docker/config.json",
|
|
2385
|
+
description: "Docker registry auth tokens",
|
|
2386
|
+
score: 10
|
|
2387
|
+
},
|
|
2388
|
+
{
|
|
2389
|
+
full: path.join(home, ".netrc"),
|
|
2390
|
+
label: "~/.netrc",
|
|
2391
|
+
description: "FTP/HTTP credentials in plain text",
|
|
2392
|
+
score: 15
|
|
2393
|
+
},
|
|
2394
|
+
{
|
|
2395
|
+
full: path.join(home, ".npmrc"),
|
|
2396
|
+
label: "~/.npmrc",
|
|
2397
|
+
description: "npm auth token \u2014 can publish packages as you",
|
|
2398
|
+
score: 10
|
|
2399
|
+
},
|
|
2400
|
+
{
|
|
2401
|
+
full: path.join(home, ".node9", "credentials.json"),
|
|
2402
|
+
label: "~/.node9/credentials.json",
|
|
2403
|
+
description: "Node9 cloud API key",
|
|
2404
|
+
score: 10
|
|
2405
|
+
},
|
|
2406
|
+
{
|
|
2407
|
+
full: path.join(cwd, ".env"),
|
|
2408
|
+
label: ".env (current folder)",
|
|
2409
|
+
description: "App secrets \u2014 database passwords, API keys",
|
|
2410
|
+
score: 20
|
|
2411
|
+
},
|
|
2412
|
+
{
|
|
2413
|
+
full: path.join(cwd, ".env.local"),
|
|
2414
|
+
label: ".env.local (current folder)",
|
|
2415
|
+
description: "Local overrides \u2014 often contains real credentials",
|
|
2416
|
+
score: 15
|
|
2417
|
+
},
|
|
2418
|
+
{
|
|
2419
|
+
full: path.join(cwd, ".env.production"),
|
|
2420
|
+
label: ".env.production (current folder)",
|
|
2421
|
+
description: "Production secrets",
|
|
2422
|
+
score: 20
|
|
2423
|
+
}
|
|
2424
|
+
];
|
|
2425
|
+
}
|
|
2426
|
+
function isReadable(filePath) {
|
|
2427
|
+
try {
|
|
2428
|
+
fs.accessSync(filePath, fs.constants.R_OK);
|
|
2429
|
+
return true;
|
|
2430
|
+
} catch {
|
|
2431
|
+
return false;
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
function runBlast() {
|
|
2435
|
+
const home = os.homedir();
|
|
2436
|
+
const cwd = process.cwd();
|
|
2437
|
+
const paths = buildSensitivePaths(home, cwd);
|
|
2438
|
+
let scoreDeduction = 0;
|
|
2439
|
+
const reachable = [];
|
|
2440
|
+
for (const p of paths) {
|
|
2441
|
+
if (fs.existsSync(p.full) && isReadable(p.full)) {
|
|
2442
|
+
reachable.push(p);
|
|
2443
|
+
scoreDeduction += p.score;
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
const envFindings = [];
|
|
2447
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
2448
|
+
if (!value) continue;
|
|
2449
|
+
const match = scanArgs({ [key]: value });
|
|
2450
|
+
if (match) {
|
|
2451
|
+
envFindings.push({ key, patternName: match.patternName });
|
|
2452
|
+
scoreDeduction += 10;
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
return { reachable, envFindings, score: Math.max(0, 100 - scoreDeduction) };
|
|
2456
|
+
}
|
|
2457
|
+
var init_blast = __esm({
|
|
2458
|
+
"src/cli/commands/blast.ts"() {
|
|
2459
|
+
"use strict";
|
|
2460
|
+
init_dlp();
|
|
2461
|
+
}
|
|
2462
|
+
});
|
|
2463
|
+
|
|
2464
|
+
// src/auth/daemon.ts
|
|
2465
|
+
import fs2 from "fs";
|
|
2466
|
+
import path2 from "path";
|
|
2467
|
+
import os2 from "os";
|
|
2468
|
+
function getInternalToken() {
|
|
2469
|
+
try {
|
|
2470
|
+
const pidFile = path2.join(os2.homedir(), ".node9", "daemon.pid");
|
|
2471
|
+
if (!fs2.existsSync(pidFile)) return null;
|
|
2472
|
+
const data = JSON.parse(fs2.readFileSync(pidFile, "utf-8"));
|
|
2473
|
+
process.kill(data.pid, 0);
|
|
2474
|
+
return data.internalToken ?? null;
|
|
2475
|
+
} catch {
|
|
2476
|
+
return null;
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
var ACTIVITY_SOCKET_PATH, DAEMON_PORT, DAEMON_HOST;
|
|
2480
|
+
var init_daemon = __esm({
|
|
2481
|
+
"src/auth/daemon.ts"() {
|
|
2482
|
+
"use strict";
|
|
2483
|
+
ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path2.join(os2.tmpdir(), "node9-activity.sock");
|
|
2484
|
+
DAEMON_PORT = 7391;
|
|
2485
|
+
DAEMON_HOST = "127.0.0.1";
|
|
2486
|
+
}
|
|
2487
|
+
});
|
|
2488
|
+
|
|
2489
|
+
// src/config-schema.ts
|
|
2490
|
+
import { z } from "zod";
|
|
2491
|
+
var noNewlines, SmartConditionSchema, SmartRuleSchema, ConfigFileSchema;
|
|
2492
|
+
var init_config_schema = __esm({
|
|
2493
|
+
"src/config-schema.ts"() {
|
|
2494
|
+
"use strict";
|
|
2495
|
+
noNewlines = z.string().refine((s) => !s.includes("\n") && !s.includes("\r"), {
|
|
2496
|
+
message: "Value must not contain literal newline characters (use \\n instead)"
|
|
2497
|
+
});
|
|
2498
|
+
SmartConditionSchema = z.object({
|
|
2499
|
+
field: z.string().min(1, "Condition field must not be empty"),
|
|
2500
|
+
op: z.enum(
|
|
2501
|
+
[
|
|
2502
|
+
"matches",
|
|
2503
|
+
"notMatches",
|
|
2504
|
+
"contains",
|
|
2505
|
+
"notContains",
|
|
2506
|
+
"exists",
|
|
2507
|
+
"notExists",
|
|
2508
|
+
"matchesGlob",
|
|
2509
|
+
"notMatchesGlob"
|
|
2510
|
+
],
|
|
2511
|
+
{
|
|
2512
|
+
errorMap: () => ({
|
|
2513
|
+
message: "op must be one of: matches, notMatches, contains, notContains, exists, notExists, matchesGlob, notMatchesGlob"
|
|
2514
|
+
})
|
|
2515
|
+
}
|
|
2516
|
+
),
|
|
2517
|
+
value: z.string().optional(),
|
|
2518
|
+
flags: z.string().optional()
|
|
2519
|
+
}).refine(
|
|
2520
|
+
(c) => {
|
|
2521
|
+
if (c.op === "matchesGlob" || c.op === "notMatchesGlob") return c.value !== void 0;
|
|
2522
|
+
return true;
|
|
2523
|
+
},
|
|
2524
|
+
{ message: "matchesGlob and notMatchesGlob conditions require a value field" }
|
|
2525
|
+
);
|
|
2526
|
+
SmartRuleSchema = z.object({
|
|
2527
|
+
name: z.string().optional(),
|
|
2528
|
+
tool: z.string().min(1, "Smart rule tool must not be empty"),
|
|
2529
|
+
conditions: z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
|
|
2530
|
+
conditionMode: z.enum(["all", "any"]).optional(),
|
|
2531
|
+
verdict: z.enum(["allow", "review", "block"], {
|
|
2532
|
+
errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
|
|
2533
|
+
}),
|
|
2534
|
+
reason: z.string().optional(),
|
|
2535
|
+
description: z.string().optional(),
|
|
2536
|
+
// Unknown predicate names are filtered out rather than failing the whole rule.
|
|
2537
|
+
// Failing the whole z.array() would cause sanitizeConfig to drop the entire
|
|
2538
|
+
// `policy` top-level key, silently disabling ALL smart rules in the config.
|
|
2539
|
+
dependsOnState: z.array(z.string()).transform(
|
|
2540
|
+
(arr) => arr.filter(
|
|
2541
|
+
(p) => p === "no_test_passed_since_last_edit"
|
|
2542
|
+
)
|
|
2543
|
+
).optional(),
|
|
2544
|
+
recoveryCommand: z.string().optional()
|
|
2545
|
+
});
|
|
2546
|
+
ConfigFileSchema = z.object({
|
|
2547
|
+
version: z.string().optional(),
|
|
2548
|
+
settings: z.object({
|
|
2549
|
+
mode: z.enum(["standard", "strict", "audit", "observe"]).optional(),
|
|
2550
|
+
autoStartDaemon: z.boolean().optional(),
|
|
2551
|
+
enableUndo: z.boolean().optional(),
|
|
2552
|
+
enableHookLogDebug: z.boolean().optional(),
|
|
2553
|
+
approvalTimeoutMs: z.number().nonnegative().optional(),
|
|
2554
|
+
approvalTimeoutSeconds: z.number().nonnegative().optional(),
|
|
2555
|
+
flightRecorder: z.boolean().optional(),
|
|
2556
|
+
approvers: z.object({
|
|
2557
|
+
native: z.boolean().optional(),
|
|
2558
|
+
browser: z.boolean().optional(),
|
|
2559
|
+
cloud: z.boolean().optional(),
|
|
2560
|
+
terminal: z.boolean().optional()
|
|
2561
|
+
}).optional(),
|
|
2562
|
+
environment: z.string().optional(),
|
|
2563
|
+
slackEnabled: z.boolean().optional(),
|
|
2564
|
+
enableTrustSessions: z.boolean().optional(),
|
|
2565
|
+
allowGlobalPause: z.boolean().optional(),
|
|
2566
|
+
auditHashArgs: z.boolean().optional(),
|
|
2567
|
+
agentPolicy: z.enum(["require_approval", "block_on_rules"]).optional(),
|
|
2568
|
+
cloudSyncIntervalHours: z.number().positive().optional()
|
|
2569
|
+
}).optional(),
|
|
2570
|
+
policy: z.object({
|
|
2571
|
+
sandboxPaths: z.array(z.string()).optional(),
|
|
2572
|
+
dangerousWords: z.array(noNewlines).optional(),
|
|
2573
|
+
ignoredTools: z.array(z.string()).optional(),
|
|
2574
|
+
toolInspection: z.record(z.string()).optional(),
|
|
2575
|
+
smartRules: z.array(SmartRuleSchema).optional(),
|
|
2576
|
+
snapshot: z.object({
|
|
2577
|
+
tools: z.array(z.string()).optional(),
|
|
2578
|
+
onlyPaths: z.array(z.string()).optional(),
|
|
2579
|
+
ignorePaths: z.array(z.string()).optional()
|
|
2580
|
+
}).optional(),
|
|
2581
|
+
dlp: z.object({
|
|
2582
|
+
enabled: z.boolean().optional(),
|
|
2583
|
+
scanIgnoredTools: z.boolean().optional()
|
|
2584
|
+
}).optional(),
|
|
2585
|
+
loopDetection: z.object({
|
|
2586
|
+
enabled: z.boolean().optional(),
|
|
2587
|
+
threshold: z.number().min(2).optional(),
|
|
2588
|
+
windowSeconds: z.number().min(10).optional()
|
|
2589
|
+
}).optional(),
|
|
2590
|
+
skillPinning: z.object({
|
|
2591
|
+
enabled: z.boolean().optional(),
|
|
2592
|
+
mode: z.enum(["warn", "block"]).optional(),
|
|
2593
|
+
roots: z.array(z.string()).optional()
|
|
2594
|
+
}).optional()
|
|
2595
|
+
}).optional(),
|
|
2596
|
+
environments: z.record(z.object({ requireApproval: z.boolean().optional() })).optional()
|
|
2597
|
+
}).strict({ message: "Config contains unknown top-level keys" });
|
|
2598
|
+
}
|
|
2599
|
+
});
|
|
2600
|
+
|
|
2601
|
+
// src/shields.ts
|
|
2602
|
+
import fs3 from "fs";
|
|
2603
|
+
import path3 from "path";
|
|
2604
|
+
import os3 from "os";
|
|
2605
|
+
function loadUserShields() {
|
|
2606
|
+
const result = {};
|
|
2607
|
+
let entries;
|
|
2608
|
+
try {
|
|
2609
|
+
entries = fs3.readdirSync(USER_SHIELDS_DIR).filter((f) => f.endsWith(".json"));
|
|
2610
|
+
} catch (err) {
|
|
2611
|
+
if (err.code !== "ENOENT") {
|
|
2612
|
+
process.stderr.write(
|
|
2613
|
+
`[node9] Could not read user shields dir ${USER_SHIELDS_DIR}: ${String(err)}
|
|
2614
|
+
`
|
|
2615
|
+
);
|
|
2616
|
+
}
|
|
2617
|
+
return result;
|
|
2618
|
+
}
|
|
2619
|
+
for (const file of entries) {
|
|
2620
|
+
const filePath = path3.join(USER_SHIELDS_DIR, file);
|
|
2621
|
+
try {
|
|
2622
|
+
const raw = JSON.parse(fs3.readFileSync(filePath, "utf-8"));
|
|
2623
|
+
const v = validateShieldDefinition(raw);
|
|
2624
|
+
if ("error" in v) {
|
|
2625
|
+
process.stderr.write(`[node9] ${v.error}: ${filePath}
|
|
2626
|
+
`);
|
|
2627
|
+
continue;
|
|
2628
|
+
}
|
|
2629
|
+
result[v.ok.name] = v.ok;
|
|
2630
|
+
} catch (err) {
|
|
2631
|
+
process.stderr.write(`[node9] Failed to load user shield ${file}: ${String(err)}
|
|
2632
|
+
`);
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
return result;
|
|
2636
|
+
}
|
|
2637
|
+
function buildSHIELDS() {
|
|
2638
|
+
return { ...BUILTIN_SHIELDS, ...loadUserShields() };
|
|
2639
|
+
}
|
|
2640
|
+
function readShieldsFile() {
|
|
2641
|
+
try {
|
|
2642
|
+
const raw = fs3.readFileSync(SHIELDS_STATE_FILE, "utf-8");
|
|
2643
|
+
if (!raw.trim()) return { active: [] };
|
|
2644
|
+
const parsed = JSON.parse(raw);
|
|
2645
|
+
const active = Array.isArray(parsed.active) ? parsed.active.filter(
|
|
2646
|
+
(e) => typeof e === "string" && e.length > 0 && e in SHIELDS
|
|
2647
|
+
) : [];
|
|
2648
|
+
const { overrides, warnings } = validateOverrides(parsed.overrides);
|
|
2649
|
+
for (const w of warnings) {
|
|
2650
|
+
process.stderr.write(`[node9] Warning: ${w}
|
|
2651
|
+
`);
|
|
2652
|
+
}
|
|
2653
|
+
return { active, overrides };
|
|
2654
|
+
} catch (err) {
|
|
2655
|
+
if (err.code !== "ENOENT") {
|
|
2656
|
+
process.stderr.write(`[node9] Warning: could not read shields state: ${String(err)}
|
|
2657
|
+
`);
|
|
2658
|
+
}
|
|
2659
|
+
return { active: [] };
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
function readActiveShields() {
|
|
2663
|
+
return readShieldsFile().active;
|
|
2664
|
+
}
|
|
2665
|
+
var USER_SHIELDS_DIR, SHIELDS, SHIELDS_STATE_FILE;
|
|
2666
|
+
var init_shields = __esm({
|
|
2667
|
+
"src/shields.ts"() {
|
|
2668
|
+
"use strict";
|
|
2669
|
+
init_dist();
|
|
2670
|
+
init_dist();
|
|
2671
|
+
USER_SHIELDS_DIR = path3.join(os3.homedir(), ".node9", "shields");
|
|
2672
|
+
SHIELDS = buildSHIELDS();
|
|
2673
|
+
SHIELDS_STATE_FILE = path3.join(os3.homedir(), ".node9", "shields.json");
|
|
2674
|
+
}
|
|
2675
|
+
});
|
|
2676
|
+
|
|
2677
|
+
// src/config/index.ts
|
|
2678
|
+
var init_config = __esm({
|
|
2679
|
+
"src/config/index.ts"() {
|
|
2680
|
+
"use strict";
|
|
2681
|
+
init_config_schema();
|
|
2682
|
+
init_shields();
|
|
2683
|
+
}
|
|
2684
|
+
});
|
|
2685
|
+
|
|
2686
|
+
// src/audit/hasher.ts
|
|
2687
|
+
var init_hasher = __esm({
|
|
2688
|
+
"src/audit/hasher.ts"() {
|
|
2689
|
+
"use strict";
|
|
2690
|
+
}
|
|
2691
|
+
});
|
|
2692
|
+
|
|
2693
|
+
// src/audit/index.ts
|
|
2694
|
+
import path4 from "path";
|
|
2695
|
+
import os4 from "os";
|
|
2696
|
+
var LOCAL_AUDIT_LOG, HOOK_DEBUG_LOG;
|
|
2697
|
+
var init_audit = __esm({
|
|
2698
|
+
"src/audit/index.ts"() {
|
|
2699
|
+
"use strict";
|
|
2700
|
+
init_hasher();
|
|
2701
|
+
LOCAL_AUDIT_LOG = path4.join(os4.homedir(), ".node9", "audit.log");
|
|
2702
|
+
HOOK_DEBUG_LOG = path4.join(os4.homedir(), ".node9", "hook-debug.log");
|
|
2703
|
+
}
|
|
2704
|
+
});
|
|
2705
|
+
|
|
2706
|
+
// src/pricing/litellm.ts
|
|
2707
|
+
function normalizeModel(raw) {
|
|
2708
|
+
return raw.replace(/-\d{8}$/, "").toLowerCase();
|
|
2709
|
+
}
|
|
2710
|
+
function pricingFor(model) {
|
|
2711
|
+
const norm = normalizeModel(model);
|
|
2712
|
+
const sources = [];
|
|
2713
|
+
if (memCache) sources.push(memCache);
|
|
2714
|
+
sources.push(BUNDLED_PRICING);
|
|
2715
|
+
for (const source of sources) {
|
|
2716
|
+
const exact = source[norm];
|
|
2717
|
+
if (exact) return exact;
|
|
2718
|
+
let best = null;
|
|
2719
|
+
for (const key of Object.keys(source)) {
|
|
2720
|
+
if (norm.startsWith(key.toLowerCase()) && (best === null || key.length > best.length)) {
|
|
2721
|
+
best = key;
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
if (best) return source[best];
|
|
2725
|
+
}
|
|
2726
|
+
return null;
|
|
2727
|
+
}
|
|
2728
|
+
var BUNDLED_PRICING, TTL_MS, memCache;
|
|
2729
|
+
var init_litellm = __esm({
|
|
2730
|
+
"src/pricing/litellm.ts"() {
|
|
2731
|
+
"use strict";
|
|
2732
|
+
init_audit();
|
|
2733
|
+
BUNDLED_PRICING = {
|
|
2734
|
+
// Anthropic
|
|
2735
|
+
"claude-opus-4": [5e-6, 25e-6, 625e-8, 5e-7],
|
|
2736
|
+
"claude-opus-4-1": [5e-6, 25e-6, 625e-8, 5e-7],
|
|
2737
|
+
"claude-opus-4-5": [5e-6, 25e-6, 625e-8, 5e-7],
|
|
2738
|
+
"claude-opus-4-6": [5e-6, 25e-6, 625e-8, 5e-7],
|
|
2739
|
+
"claude-opus-4-7": [5e-6, 25e-6, 625e-8, 5e-7],
|
|
2740
|
+
"claude-sonnet-4": [3e-6, 15e-6, 375e-8, 3e-7],
|
|
2741
|
+
"claude-sonnet-4-5": [3e-6, 15e-6, 375e-8, 3e-7],
|
|
2742
|
+
"claude-sonnet-4-6": [3e-6, 15e-6, 375e-8, 3e-7],
|
|
2743
|
+
"claude-haiku-4": [8e-7, 4e-6, 1e-6, 8e-8],
|
|
2744
|
+
"claude-haiku-4-5": [8e-7, 4e-6, 1e-6, 8e-8],
|
|
2745
|
+
"claude-3-7-sonnet": [3e-6, 15e-6, 375e-8, 3e-7],
|
|
2746
|
+
"claude-3-5-sonnet": [3e-6, 15e-6, 375e-8, 3e-7],
|
|
2747
|
+
"claude-3-5-haiku": [8e-7, 4e-6, 1e-6, 8e-8],
|
|
2748
|
+
"claude-3-haiku": [25e-8, 125e-8, 3e-7, 3e-8],
|
|
2749
|
+
// OpenAI
|
|
2750
|
+
"gpt-4o": [5e-6, 15e-6, 0, 25e-7],
|
|
2751
|
+
"gpt-4o-mini": [15e-8, 6e-7, 0, 75e-9],
|
|
2752
|
+
"gpt-5": [1e-5, 3e-5, 0, 5e-6],
|
|
2753
|
+
// Google
|
|
2754
|
+
"gemini-2.0-flash": [75e-9, 3e-7, 0, 0],
|
|
2755
|
+
"gemini-1.5-pro": [125e-8, 5e-6, 0, 0]
|
|
2756
|
+
};
|
|
2757
|
+
TTL_MS = 24 * 60 * 60 * 1e3;
|
|
2758
|
+
memCache = null;
|
|
2759
|
+
}
|
|
2760
|
+
});
|
|
2761
|
+
|
|
2762
|
+
// src/costSync.ts
|
|
2763
|
+
import fs4 from "fs";
|
|
2764
|
+
import path5 from "path";
|
|
2765
|
+
import os5 from "os";
|
|
2766
|
+
function decodeProjectDirName(dirName) {
|
|
2767
|
+
return dirName.replace(/-/g, "/");
|
|
2768
|
+
}
|
|
2769
|
+
function parseJSONLFile(filePath, fallbackWorkingDir) {
|
|
2770
|
+
const runId = path5.basename(filePath, ".jsonl");
|
|
2771
|
+
let content;
|
|
2772
|
+
try {
|
|
2773
|
+
content = fs4.readFileSync(filePath, "utf8");
|
|
2774
|
+
} catch {
|
|
2775
|
+
return /* @__PURE__ */ new Map();
|
|
2776
|
+
}
|
|
2777
|
+
const daily = /* @__PURE__ */ new Map();
|
|
2778
|
+
for (const line of content.split("\n")) {
|
|
2779
|
+
if (!line.trim()) continue;
|
|
2780
|
+
let row;
|
|
2781
|
+
try {
|
|
2782
|
+
row = JSON.parse(line);
|
|
2783
|
+
} catch {
|
|
2784
|
+
continue;
|
|
2785
|
+
}
|
|
2786
|
+
if (row["type"] !== "assistant") continue;
|
|
2787
|
+
const msg = row["message"];
|
|
2788
|
+
if (!msg?.["usage"] || typeof msg["model"] !== "string") continue;
|
|
2789
|
+
const usage = msg["usage"];
|
|
2790
|
+
const model = msg["model"];
|
|
2791
|
+
const timestamp = row["timestamp"];
|
|
2792
|
+
if (typeof timestamp !== "string" || timestamp.length < 10) continue;
|
|
2793
|
+
const date = timestamp.slice(0, 10);
|
|
2794
|
+
const p = pricingFor(model);
|
|
2795
|
+
if (!p) continue;
|
|
2796
|
+
const inp = Number(usage["input_tokens"] ?? 0);
|
|
2797
|
+
const out = Number(usage["output_tokens"] ?? 0);
|
|
2798
|
+
const cw = Number(usage["cache_creation_input_tokens"] ?? 0);
|
|
2799
|
+
const cr = Number(usage["cache_read_input_tokens"] ?? 0);
|
|
2800
|
+
const cost = inp * p[0] + out * p[1] + cw * p[2] + cr * p[3];
|
|
2801
|
+
const rowCwd = typeof row["cwd"] === "string" ? row["cwd"] : null;
|
|
2802
|
+
const workingDir = rowCwd && rowCwd.startsWith("/") ? rowCwd : fallbackWorkingDir;
|
|
2803
|
+
const norm = normalizeModel(model);
|
|
2804
|
+
const key = `${date}::${norm}::${workingDir}::${runId}`;
|
|
2805
|
+
const prev = daily.get(key);
|
|
2806
|
+
if (prev) {
|
|
2807
|
+
prev.costUSD += cost;
|
|
2808
|
+
prev.inputTokens += inp;
|
|
2809
|
+
prev.outputTokens += out;
|
|
2810
|
+
prev.cacheWriteTokens += cw;
|
|
2811
|
+
prev.cacheReadTokens += cr;
|
|
2812
|
+
} else {
|
|
2813
|
+
daily.set(key, {
|
|
2814
|
+
date,
|
|
2815
|
+
model: norm,
|
|
2816
|
+
workingDir,
|
|
2817
|
+
runId,
|
|
2818
|
+
costUSD: cost,
|
|
2819
|
+
inputTokens: inp,
|
|
2820
|
+
outputTokens: out,
|
|
2821
|
+
cacheWriteTokens: cw,
|
|
2822
|
+
cacheReadTokens: cr
|
|
2823
|
+
});
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
return daily;
|
|
2827
|
+
}
|
|
2828
|
+
function collectEntries() {
|
|
2829
|
+
const projectsDir = path5.join(os5.homedir(), ".claude", "projects");
|
|
2830
|
+
if (!fs4.existsSync(projectsDir)) return [];
|
|
2831
|
+
const combined = /* @__PURE__ */ new Map();
|
|
2832
|
+
let dirs;
|
|
2833
|
+
try {
|
|
2834
|
+
dirs = fs4.readdirSync(projectsDir);
|
|
2835
|
+
} catch {
|
|
2836
|
+
return [];
|
|
2837
|
+
}
|
|
2838
|
+
for (const dir of dirs) {
|
|
2839
|
+
const dirPath = path5.join(projectsDir, dir);
|
|
2840
|
+
try {
|
|
2841
|
+
if (!fs4.statSync(dirPath).isDirectory()) continue;
|
|
2842
|
+
} catch {
|
|
2843
|
+
continue;
|
|
2844
|
+
}
|
|
2845
|
+
let files;
|
|
2846
|
+
try {
|
|
2847
|
+
files = fs4.readdirSync(dirPath).filter((f) => f.endsWith(".jsonl"));
|
|
2848
|
+
} catch {
|
|
2849
|
+
continue;
|
|
2850
|
+
}
|
|
2851
|
+
const fallbackWorkingDir = decodeProjectDirName(dir);
|
|
2852
|
+
for (const file of files) {
|
|
2853
|
+
const entries = parseJSONLFile(path5.join(dirPath, file), fallbackWorkingDir);
|
|
2854
|
+
for (const [key, e] of entries) {
|
|
2855
|
+
const prev = combined.get(key);
|
|
2856
|
+
if (prev) {
|
|
2857
|
+
prev.costUSD += e.costUSD;
|
|
2858
|
+
prev.inputTokens += e.inputTokens;
|
|
2859
|
+
prev.outputTokens += e.outputTokens;
|
|
2860
|
+
prev.cacheWriteTokens += e.cacheWriteTokens;
|
|
2861
|
+
prev.cacheReadTokens += e.cacheReadTokens;
|
|
2862
|
+
} else {
|
|
2863
|
+
combined.set(key, { ...e });
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2868
|
+
return [...combined.values()];
|
|
2869
|
+
}
|
|
2870
|
+
var SYNC_INTERVAL_MS;
|
|
2871
|
+
var init_costSync = __esm({
|
|
2872
|
+
"src/costSync.ts"() {
|
|
2873
|
+
"use strict";
|
|
2874
|
+
init_config();
|
|
2875
|
+
init_audit();
|
|
2876
|
+
init_litellm();
|
|
2877
|
+
SYNC_INTERVAL_MS = 10 * 60 * 1e3;
|
|
2878
|
+
}
|
|
2879
|
+
});
|
|
2880
|
+
|
|
2881
|
+
// src/daemon/scan-watermark.ts
|
|
2882
|
+
function extractFindingsFromLine(line, sessionId, lineIndex) {
|
|
2883
|
+
if (!line || typeof line !== "object") return [];
|
|
2884
|
+
const findings = [];
|
|
2885
|
+
const obj = line;
|
|
2886
|
+
const candidates = [];
|
|
2887
|
+
if (obj.message && typeof obj.message === "object") {
|
|
2888
|
+
const msg = obj.message;
|
|
2889
|
+
if (typeof msg.content === "string") candidates.push(msg.content);
|
|
2890
|
+
else if (Array.isArray(msg.content)) candidates.push(msg.content);
|
|
2891
|
+
}
|
|
2892
|
+
if (typeof obj.toolUseResult === "string") candidates.push(obj.toolUseResult);
|
|
2893
|
+
if (obj.input && typeof obj.input === "object") candidates.push(obj.input);
|
|
2894
|
+
for (const candidate of candidates) {
|
|
2895
|
+
const wrapped = typeof candidate === "string" ? { content: candidate } : candidate;
|
|
2896
|
+
const hit = scanArgs(wrapped);
|
|
2897
|
+
if (hit) {
|
|
2898
|
+
findings.push({
|
|
2899
|
+
type: "dlp",
|
|
2900
|
+
patternName: hit.patternName,
|
|
2901
|
+
sessionId,
|
|
2902
|
+
lineIndex
|
|
2903
|
+
});
|
|
2904
|
+
}
|
|
2905
|
+
if (typeof candidate === "string" && candidate.length > 0) {
|
|
2906
|
+
const piiHits = detectPii(candidate);
|
|
2907
|
+
for (const patternName of piiHits) {
|
|
2908
|
+
findings.push({
|
|
2909
|
+
type: "pii",
|
|
2910
|
+
patternName,
|
|
2911
|
+
sessionId,
|
|
2912
|
+
lineIndex
|
|
2913
|
+
});
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
const ctx = {
|
|
2918
|
+
sessionId,
|
|
2919
|
+
lineIndex,
|
|
2920
|
+
project: "",
|
|
2921
|
+
agent: "claude",
|
|
2922
|
+
rules: [],
|
|
2923
|
+
toolInspection: { bash: "command", execute_bash: "command" },
|
|
2924
|
+
dlpEnabled: false
|
|
2925
|
+
// line-level DLP runs above already
|
|
2926
|
+
};
|
|
2927
|
+
const message = line.message;
|
|
2928
|
+
if (message && typeof message === "object") {
|
|
2929
|
+
const content = message.content;
|
|
2930
|
+
if (Array.isArray(content)) {
|
|
2931
|
+
for (const block of content) {
|
|
2932
|
+
if (!block || typeof block !== "object") continue;
|
|
2933
|
+
const b = block;
|
|
2934
|
+
if (b.type === "tool_result") {
|
|
2935
|
+
const c = b.content;
|
|
2936
|
+
const len = typeof c === "string" ? c.length : Array.isArray(c) ? JSON.stringify(c).length : 0;
|
|
2937
|
+
if (len > LONG_OUTPUT_THRESHOLD_BYTES2) {
|
|
2938
|
+
findings.push({
|
|
2939
|
+
type: "long-output-redacted",
|
|
2940
|
+
sessionId,
|
|
2941
|
+
lineIndex
|
|
2942
|
+
});
|
|
2943
|
+
}
|
|
2944
|
+
continue;
|
|
2945
|
+
}
|
|
2946
|
+
if (b.type !== "tool_use") continue;
|
|
2947
|
+
const toolName = typeof b.name === "string" ? b.name : "";
|
|
2948
|
+
const input = b.input ?? {};
|
|
2949
|
+
const call = {
|
|
2950
|
+
toolName,
|
|
2951
|
+
args: input,
|
|
2952
|
+
timestamp: typeof obj.timestamp === "string" ? obj.timestamp : ""
|
|
2953
|
+
};
|
|
2954
|
+
const canonical = extractCanonicalFindings(call, ctx);
|
|
2955
|
+
for (const cf of canonical) {
|
|
2956
|
+
const sf = toScanFinding(cf);
|
|
2957
|
+
if (sf) findings.push(sf);
|
|
2958
|
+
}
|
|
2959
|
+
}
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2962
|
+
return findings;
|
|
2963
|
+
}
|
|
2964
|
+
var MAX_LINE_BYTES, LONG_OUTPUT_THRESHOLD_BYTES2;
|
|
2965
|
+
var init_scan_watermark = __esm({
|
|
2966
|
+
"src/daemon/scan-watermark.ts"() {
|
|
2967
|
+
"use strict";
|
|
2968
|
+
init_dlp();
|
|
2969
|
+
init_dist();
|
|
2970
|
+
MAX_LINE_BYTES = 2 * 1024 * 1024;
|
|
2971
|
+
LONG_OUTPUT_THRESHOLD_BYTES2 = LONG_OUTPUT_THRESHOLD_BYTES;
|
|
2972
|
+
}
|
|
2973
|
+
});
|
|
2974
|
+
|
|
2975
|
+
// src/tui/dashboard/data.ts
|
|
2976
|
+
import fs5 from "fs";
|
|
2977
|
+
import os6 from "os";
|
|
2978
|
+
import path6 from "path";
|
|
2979
|
+
import http from "http";
|
|
2980
|
+
function auditLogPath() {
|
|
2981
|
+
return path6.join(os6.homedir(), ".node9", "audit.log");
|
|
2982
|
+
}
|
|
2983
|
+
function readAuditEntries() {
|
|
2984
|
+
const p = auditLogPath();
|
|
2985
|
+
if (!fs5.existsSync(p)) return [];
|
|
2986
|
+
try {
|
|
2987
|
+
const lines = fs5.readFileSync(p, "utf8").split("\n");
|
|
2988
|
+
const out = [];
|
|
2989
|
+
for (const line of lines) {
|
|
2990
|
+
if (!line.trim()) continue;
|
|
2991
|
+
try {
|
|
2992
|
+
const e = JSON.parse(line);
|
|
2993
|
+
if (e && typeof e.ts === "string") out.push(e);
|
|
2994
|
+
} catch {
|
|
2995
|
+
}
|
|
2996
|
+
}
|
|
2997
|
+
return out;
|
|
2998
|
+
} catch {
|
|
2999
|
+
return [];
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
function aggregateAudit(entries, startMs, endMs = Date.now()) {
|
|
3003
|
+
const inWindow = entries.filter((e) => {
|
|
3004
|
+
if (e.source === "post-hook" || e.source === "response-dlp") return false;
|
|
3005
|
+
const t = Date.parse(e.ts);
|
|
3006
|
+
return t >= startMs && t <= endMs;
|
|
3007
|
+
});
|
|
3008
|
+
let allow = 0;
|
|
3009
|
+
let block = 0;
|
|
3010
|
+
let review = 0;
|
|
3011
|
+
let loops = 0;
|
|
3012
|
+
let dlpHits = 0;
|
|
3013
|
+
const sessionSet = /* @__PURE__ */ new Set();
|
|
3014
|
+
const mcpSet = /* @__PURE__ */ new Set();
|
|
3015
|
+
const toolMap = /* @__PURE__ */ new Map();
|
|
3016
|
+
const blockMap = /* @__PURE__ */ new Map();
|
|
3017
|
+
const shellMap = /* @__PURE__ */ new Map();
|
|
3018
|
+
for (const e of inWindow) {
|
|
3019
|
+
if (e.sessionId) sessionSet.add(e.sessionId);
|
|
3020
|
+
if (e.mcpServer) mcpSet.add(e.mcpServer);
|
|
3021
|
+
const isAllow = e.decision === "allow" || e.decision === "observe-allow";
|
|
3022
|
+
const isReview = e.decision === "review";
|
|
3023
|
+
const isLoop = e.checkedBy === "loop-detected";
|
|
3024
|
+
const isDlp = !!(e.checkedBy && e.checkedBy.toLowerCase().includes("dlp"));
|
|
3025
|
+
if (isAllow) allow++;
|
|
3026
|
+
else if (isReview) review++;
|
|
3027
|
+
else block++;
|
|
3028
|
+
if (isLoop) loops++;
|
|
3029
|
+
if (isDlp) dlpHits++;
|
|
3030
|
+
const t = toolMap.get(e.tool) ?? { calls: 0, blocked: 0 };
|
|
3031
|
+
t.calls++;
|
|
3032
|
+
if (!isAllow && !isReview) t.blocked++;
|
|
3033
|
+
toolMap.set(e.tool, t);
|
|
3034
|
+
if (!isAllow && e.checkedBy) {
|
|
3035
|
+
blockMap.set(e.checkedBy, (blockMap.get(e.checkedBy) ?? 0) + 1);
|
|
3036
|
+
}
|
|
3037
|
+
if (TEST_TOOLS.has(e.tool) && typeof e.args?.command === "string") {
|
|
3038
|
+
const head = e.args.command.trim().split(/\s+/)[0];
|
|
3039
|
+
if (head && /^[a-zA-Z0-9._-]+$/.test(head)) {
|
|
3040
|
+
const s = shellMap.get(head) ?? { count: 0, blocked: 0 };
|
|
3041
|
+
s.count++;
|
|
3042
|
+
if (!isAllow && !isReview) s.blocked++;
|
|
3043
|
+
shellMap.set(head, s);
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
3046
|
+
}
|
|
3047
|
+
return {
|
|
3048
|
+
total: inWindow.length,
|
|
3049
|
+
allow,
|
|
3050
|
+
block,
|
|
3051
|
+
review,
|
|
3052
|
+
loops,
|
|
3053
|
+
dlpHits,
|
|
3054
|
+
sessions: sessionSet.size,
|
|
3055
|
+
mcpServers: mcpSet.size,
|
|
3056
|
+
mcpCalls: [...inWindow].filter((e) => !!e.mcpServer).length,
|
|
3057
|
+
byTool: [...toolMap.entries()].sort((a, b) => b[1].calls - a[1].calls).slice(0, 5).map(([tool, v]) => ({ tool, calls: v.calls, blocked: v.blocked })),
|
|
3058
|
+
byBlock: [...blockMap.entries()].sort((a, b) => b[1] - a[1]).slice(0, 6).map(([rule, count]) => ({ rule, count })),
|
|
3059
|
+
byShell: [...shellMap.entries()].sort((a, b) => b[1].count - a[1].count).slice(0, 5).map(([cmd, v]) => ({ cmd, count: v.count, blocked: v.blocked }))
|
|
3060
|
+
};
|
|
3061
|
+
}
|
|
3062
|
+
function compactPath(p) {
|
|
3063
|
+
if (!p) return p;
|
|
3064
|
+
const looksLikePath = p.startsWith("/") || p.startsWith("~") || p.includes("/");
|
|
3065
|
+
if (!looksLikePath) return p;
|
|
3066
|
+
const segs = p.split("/").filter((s) => s.length > 0);
|
|
3067
|
+
if (segs.length <= 3) return p;
|
|
3068
|
+
return ".../" + segs.slice(-2).join("/");
|
|
3069
|
+
}
|
|
3070
|
+
function loadCostEntries() {
|
|
3071
|
+
return new Promise((resolve) => {
|
|
3072
|
+
setImmediate(() => {
|
|
3073
|
+
try {
|
|
3074
|
+
resolve(collectEntries());
|
|
3075
|
+
} catch {
|
|
3076
|
+
resolve([]);
|
|
3077
|
+
}
|
|
3078
|
+
});
|
|
3079
|
+
});
|
|
3080
|
+
}
|
|
3081
|
+
function entryKey(e) {
|
|
3082
|
+
return `${e.date}|${e.model}|${e.workingDir ?? ""}|${e.runId ?? ""}`;
|
|
3083
|
+
}
|
|
3084
|
+
function subtractCostBaseline(entries, baseline) {
|
|
3085
|
+
return entries.map((e) => {
|
|
3086
|
+
const b = baseline.get(entryKey(e));
|
|
3087
|
+
if (!b) return e;
|
|
3088
|
+
return {
|
|
3089
|
+
...e,
|
|
3090
|
+
costUSD: Math.max(0, e.costUSD - b.costUSD),
|
|
3091
|
+
inputTokens: Math.max(0, e.inputTokens - b.inputTokens),
|
|
3092
|
+
outputTokens: Math.max(0, e.outputTokens - b.outputTokens),
|
|
3093
|
+
cacheReadTokens: Math.max(0, e.cacheReadTokens - b.cacheReadTokens),
|
|
3094
|
+
cacheWriteTokens: Math.max(0, e.cacheWriteTokens - b.cacheWriteTokens)
|
|
3095
|
+
};
|
|
3096
|
+
});
|
|
3097
|
+
}
|
|
3098
|
+
function buildCostBaseline(entries) {
|
|
3099
|
+
const map = /* @__PURE__ */ new Map();
|
|
3100
|
+
for (const e of entries) map.set(entryKey(e), { ...e });
|
|
3101
|
+
return map;
|
|
3102
|
+
}
|
|
3103
|
+
function aggregateCost(entries, startMs, endMs = Date.now()) {
|
|
3104
|
+
let totalUSD = 0;
|
|
3105
|
+
let inputTokens = 0;
|
|
3106
|
+
let outputTokens = 0;
|
|
3107
|
+
let cacheReadTokens = 0;
|
|
3108
|
+
let cacheWriteTokens = 0;
|
|
3109
|
+
const byModelMap = /* @__PURE__ */ new Map();
|
|
3110
|
+
const dayMs = 864e5;
|
|
3111
|
+
for (const e of entries) {
|
|
3112
|
+
const t = Date.parse(e.date);
|
|
3113
|
+
if (Number.isNaN(t)) continue;
|
|
3114
|
+
if (t + dayMs < startMs) continue;
|
|
3115
|
+
if (t > endMs) continue;
|
|
3116
|
+
totalUSD += e.costUSD;
|
|
3117
|
+
inputTokens += e.inputTokens;
|
|
3118
|
+
outputTokens += e.outputTokens;
|
|
3119
|
+
cacheReadTokens += e.cacheReadTokens;
|
|
3120
|
+
cacheWriteTokens += e.cacheWriteTokens;
|
|
3121
|
+
const m = byModelMap.get(e.model) ?? { costUSD: 0, calls: 0 };
|
|
3122
|
+
m.costUSD += e.costUSD;
|
|
3123
|
+
m.calls += 1;
|
|
3124
|
+
byModelMap.set(e.model, m);
|
|
3125
|
+
}
|
|
3126
|
+
return {
|
|
3127
|
+
totalUSD,
|
|
3128
|
+
inputTokens,
|
|
3129
|
+
outputTokens,
|
|
3130
|
+
cacheReadTokens,
|
|
3131
|
+
cacheWriteTokens,
|
|
3132
|
+
byModel: [...byModelMap.entries()].sort((a, b) => b[1].costUSD - a[1].costUSD).map(([model, v]) => ({ model, costUSD: v.costUSD, calls: v.calls })),
|
|
3133
|
+
loaded: true
|
|
3134
|
+
};
|
|
3135
|
+
}
|
|
3136
|
+
function loadScanSignals() {
|
|
3137
|
+
return new Promise((resolve) => {
|
|
3138
|
+
setImmediate(() => {
|
|
3139
|
+
try {
|
|
3140
|
+
resolve({ loaded: true, ...walkClaudeJsonlsForSignals() });
|
|
3141
|
+
} catch {
|
|
3142
|
+
resolve({ loaded: true, ...EMPTY_SIGNALS });
|
|
3143
|
+
}
|
|
3144
|
+
});
|
|
3145
|
+
});
|
|
3146
|
+
}
|
|
3147
|
+
function walkClaudeJsonlsForSignals() {
|
|
3148
|
+
const counts = { ...EMPTY_SIGNALS };
|
|
3149
|
+
const projectsDir = path6.join(os6.homedir(), ".claude", "projects");
|
|
3150
|
+
if (!fs5.existsSync(projectsDir)) return counts;
|
|
3151
|
+
let dirs;
|
|
3152
|
+
try {
|
|
3153
|
+
dirs = fs5.readdirSync(projectsDir);
|
|
3154
|
+
} catch {
|
|
3155
|
+
return counts;
|
|
3156
|
+
}
|
|
3157
|
+
for (const dir of dirs) {
|
|
3158
|
+
const dirPath = path6.join(projectsDir, dir);
|
|
3159
|
+
try {
|
|
3160
|
+
if (!fs5.statSync(dirPath).isDirectory()) continue;
|
|
3161
|
+
} catch {
|
|
3162
|
+
continue;
|
|
3163
|
+
}
|
|
3164
|
+
let files;
|
|
3165
|
+
try {
|
|
3166
|
+
files = fs5.readdirSync(dirPath).filter((f) => f.endsWith(".jsonl"));
|
|
3167
|
+
} catch {
|
|
3168
|
+
continue;
|
|
3169
|
+
}
|
|
3170
|
+
for (const file of files) {
|
|
3171
|
+
const sessionId = file.replace(/\.jsonl$/, "");
|
|
3172
|
+
const filePath = path6.join(dirPath, file);
|
|
3173
|
+
let content;
|
|
3174
|
+
try {
|
|
3175
|
+
content = fs5.readFileSync(filePath, "utf8");
|
|
3176
|
+
} catch {
|
|
3177
|
+
continue;
|
|
3178
|
+
}
|
|
3179
|
+
const lines = content.split("\n");
|
|
3180
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3181
|
+
const line = lines[i];
|
|
3182
|
+
if (!line.trim()) continue;
|
|
3183
|
+
let parsed;
|
|
3184
|
+
try {
|
|
3185
|
+
parsed = JSON.parse(line);
|
|
3186
|
+
} catch {
|
|
3187
|
+
continue;
|
|
3188
|
+
}
|
|
3189
|
+
let findings;
|
|
3190
|
+
try {
|
|
3191
|
+
findings = extractFindingsFromLine(parsed, sessionId, i);
|
|
3192
|
+
} catch {
|
|
3193
|
+
continue;
|
|
3194
|
+
}
|
|
3195
|
+
for (const f of findings) {
|
|
3196
|
+
switch (f.type) {
|
|
3197
|
+
case "pii":
|
|
3198
|
+
counts.pii++;
|
|
3199
|
+
break;
|
|
3200
|
+
case "sensitive-file-read":
|
|
3201
|
+
counts.sensitiveFileRead++;
|
|
3202
|
+
break;
|
|
3203
|
+
case "privilege-escalation":
|
|
3204
|
+
counts.privilegeEscalation++;
|
|
3205
|
+
break;
|
|
3206
|
+
case "destructive-op":
|
|
3207
|
+
counts.destructiveOp++;
|
|
3208
|
+
break;
|
|
3209
|
+
case "pipe-to-shell":
|
|
3210
|
+
counts.pipeToShell++;
|
|
3211
|
+
break;
|
|
3212
|
+
case "eval-of-remote":
|
|
3213
|
+
counts.evalOfRemote++;
|
|
3214
|
+
break;
|
|
3215
|
+
case "long-output-redacted":
|
|
3216
|
+
counts.longOutputRedacted++;
|
|
3217
|
+
break;
|
|
3218
|
+
// 'dlp', 'loop', 'network-exfil' tracked via other paths
|
|
3219
|
+
// (audit / session-level) — skip here to avoid double-count.
|
|
3220
|
+
default:
|
|
3221
|
+
break;
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3224
|
+
}
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
return counts;
|
|
3228
|
+
}
|
|
3229
|
+
function loadShieldStatus() {
|
|
3230
|
+
try {
|
|
3231
|
+
const all = Object.keys(SHIELDS).sort();
|
|
3232
|
+
const activeSet = new Set(readActiveShields());
|
|
3233
|
+
const active = all.filter((n) => activeSet.has(n));
|
|
3234
|
+
const inactive = all.filter((n) => !activeSet.has(n));
|
|
3235
|
+
return { active, inactive };
|
|
3236
|
+
} catch {
|
|
3237
|
+
return { active: [], inactive: [] };
|
|
3238
|
+
}
|
|
3239
|
+
}
|
|
3240
|
+
function loadBlast() {
|
|
3241
|
+
try {
|
|
3242
|
+
const r = runBlast();
|
|
3243
|
+
return {
|
|
3244
|
+
score: r.score,
|
|
3245
|
+
paths: r.reachable.slice(0, 5).map((f) => shortenPath(f.full)),
|
|
3246
|
+
envFindings: r.envFindings.length
|
|
3247
|
+
};
|
|
3248
|
+
} catch {
|
|
3249
|
+
return { score: 100, paths: [], envFindings: 0 };
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
function shortenPath(p) {
|
|
3253
|
+
const home = os6.homedir();
|
|
3254
|
+
return p.startsWith(home) ? p.replace(home, "~") : p;
|
|
3255
|
+
}
|
|
3256
|
+
function mapResultStatus(status) {
|
|
3257
|
+
if (typeof status !== "string") return void 0;
|
|
3258
|
+
if (status === "allow" || status === "observe-allow" || status === "trust") return "allow";
|
|
3259
|
+
if (status === "dlp" || status === "block" || status === "denied" || status === "deny" || status === "timeout") {
|
|
3260
|
+
return "block";
|
|
3261
|
+
}
|
|
3262
|
+
if (status === "review") return "review";
|
|
3263
|
+
return void 0;
|
|
3264
|
+
}
|
|
3265
|
+
function normalizeTs(ts) {
|
|
3266
|
+
if (typeof ts === "string" && ts.length > 0) return ts;
|
|
3267
|
+
if (typeof ts === "number" && Number.isFinite(ts)) {
|
|
3268
|
+
return new Date(ts).toISOString();
|
|
3269
|
+
}
|
|
3270
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
3271
|
+
}
|
|
3272
|
+
function submitDecision(id, decision) {
|
|
3273
|
+
return new Promise((resolve) => {
|
|
3274
|
+
const token = getInternalToken();
|
|
3275
|
+
if (!token) {
|
|
3276
|
+
resolve({ ok: false, error: "daemon not running" });
|
|
3277
|
+
return;
|
|
3278
|
+
}
|
|
3279
|
+
const bodyObj = { decision, source: "dashboard" };
|
|
3280
|
+
if (decision === "trust") bodyObj.persist = true;
|
|
3281
|
+
const body = JSON.stringify(bodyObj);
|
|
3282
|
+
const req = http.request(
|
|
3283
|
+
{
|
|
3284
|
+
hostname: DAEMON_HOST,
|
|
3285
|
+
port: DAEMON_PORT,
|
|
3286
|
+
path: `/decision/${encodeURIComponent(id)}`,
|
|
3287
|
+
method: "POST",
|
|
3288
|
+
headers: {
|
|
3289
|
+
"Content-Type": "application/json",
|
|
3290
|
+
"Content-Length": Buffer.byteLength(body),
|
|
3291
|
+
"X-Node9-Internal": token
|
|
3292
|
+
}
|
|
3293
|
+
},
|
|
3294
|
+
(res) => {
|
|
3295
|
+
res.resume();
|
|
3296
|
+
if (res.statusCode === 200 || res.statusCode === 409) {
|
|
3297
|
+
resolve({ ok: true });
|
|
3298
|
+
} else {
|
|
3299
|
+
resolve({ ok: false, error: `daemon returned ${res.statusCode}` });
|
|
3300
|
+
}
|
|
3301
|
+
}
|
|
3302
|
+
);
|
|
3303
|
+
req.on("error", (err) => resolve({ ok: false, error: err.message }));
|
|
3304
|
+
req.end(body);
|
|
3305
|
+
});
|
|
3306
|
+
}
|
|
3307
|
+
function isValidForensicEvent(e) {
|
|
3308
|
+
if (!e || typeof e !== "object") return false;
|
|
3309
|
+
const ev = e;
|
|
3310
|
+
return typeof ev.id === "string" && typeof ev.sessionId === "string" && typeof ev.category === "string" && FORENSIC_CATEGORIES.has(ev.category);
|
|
3311
|
+
}
|
|
3312
|
+
function applyForensicEvent(agg, ev) {
|
|
3313
|
+
const next = { ...agg };
|
|
3314
|
+
switch (ev.category) {
|
|
3315
|
+
case "pii":
|
|
3316
|
+
next.pii++;
|
|
3317
|
+
break;
|
|
3318
|
+
case "sensitive-file-read":
|
|
3319
|
+
next.sensitiveFileRead++;
|
|
3320
|
+
break;
|
|
3321
|
+
case "privilege-escalation":
|
|
3322
|
+
next.privilegeEscalation++;
|
|
3323
|
+
break;
|
|
3324
|
+
case "destructive-op":
|
|
3325
|
+
next.destructiveOp++;
|
|
3326
|
+
break;
|
|
3327
|
+
case "pipe-to-shell":
|
|
3328
|
+
next.pipeToShell++;
|
|
3329
|
+
break;
|
|
3330
|
+
case "eval-of-remote":
|
|
3331
|
+
next.evalOfRemote++;
|
|
3332
|
+
break;
|
|
3333
|
+
case "long-output-redacted":
|
|
3334
|
+
next.longOutputRedacted++;
|
|
3335
|
+
break;
|
|
3336
|
+
default:
|
|
3337
|
+
break;
|
|
3338
|
+
}
|
|
3339
|
+
return next;
|
|
3340
|
+
}
|
|
3341
|
+
function subscribeToSse(onEvent, onResolve, onForensic, onError) {
|
|
3342
|
+
const token = getInternalToken();
|
|
3343
|
+
if (!token) {
|
|
3344
|
+
onError("daemon not running (no ~/.node9/daemon.pid). Run: node9 daemon start");
|
|
3345
|
+
return () => {
|
|
3346
|
+
};
|
|
3347
|
+
}
|
|
3348
|
+
let req;
|
|
3349
|
+
let reconnectTimer;
|
|
3350
|
+
let aborted = false;
|
|
3351
|
+
let backoffMs = SSE_BACKOFF_INITIAL_MS;
|
|
3352
|
+
const scheduleReconnect = (reason) => {
|
|
3353
|
+
if (aborted) return;
|
|
3354
|
+
const wait = backoffMs;
|
|
3355
|
+
onError(`${reason}; reconnecting in ${Math.round(wait / 1e3)}s\u2026`);
|
|
3356
|
+
backoffMs = Math.min(backoffMs * 2, SSE_BACKOFF_MAX_MS);
|
|
3357
|
+
reconnectTimer = setTimeout(() => {
|
|
3358
|
+
reconnectTimer = void 0;
|
|
3359
|
+
connect();
|
|
3360
|
+
}, wait);
|
|
3361
|
+
};
|
|
3362
|
+
const connect = () => {
|
|
3363
|
+
if (aborted) return;
|
|
3364
|
+
req = http.get(
|
|
3365
|
+
`http://${DAEMON_HOST}:${DAEMON_PORT}/events`,
|
|
3366
|
+
{ headers: { "X-Node9-Internal": token, Accept: "text/event-stream" } },
|
|
3367
|
+
(res) => {
|
|
3368
|
+
if (res.statusCode !== 200) {
|
|
3369
|
+
res.resume();
|
|
3370
|
+
scheduleReconnect(`daemon /events returned ${res.statusCode}`);
|
|
3371
|
+
return;
|
|
3372
|
+
}
|
|
3373
|
+
backoffMs = SSE_BACKOFF_INITIAL_MS;
|
|
3374
|
+
let buf = "";
|
|
3375
|
+
let currentEvent = "";
|
|
3376
|
+
res.setEncoding("utf8");
|
|
3377
|
+
res.on("data", (chunk) => {
|
|
3378
|
+
buf += chunk;
|
|
3379
|
+
let idx;
|
|
3380
|
+
while ((idx = buf.indexOf("\n")) !== -1) {
|
|
3381
|
+
const line = buf.slice(0, idx).replace(/\r$/, "");
|
|
3382
|
+
buf = buf.slice(idx + 1);
|
|
3383
|
+
if (line.startsWith("event:")) {
|
|
3384
|
+
currentEvent = line.slice(6).trim();
|
|
3385
|
+
} else if (line.startsWith("data:")) {
|
|
3386
|
+
const raw = line.slice(5).trim();
|
|
3387
|
+
if (!raw) continue;
|
|
3388
|
+
try {
|
|
3389
|
+
if (currentEvent === "forensic") {
|
|
3390
|
+
const fEvent = JSON.parse(raw);
|
|
3391
|
+
if (isValidForensicEvent(fEvent)) {
|
|
3392
|
+
onForensic(fEvent);
|
|
3393
|
+
}
|
|
3394
|
+
continue;
|
|
3395
|
+
}
|
|
3396
|
+
const data = JSON.parse(raw);
|
|
3397
|
+
if (currentEvent === "activity-result" || currentEvent === "remove") {
|
|
3398
|
+
if (typeof data.id === "string") {
|
|
3399
|
+
const verdict = mapResultStatus(data.status ?? data.decision);
|
|
3400
|
+
onResolve(data.id, verdict);
|
|
3401
|
+
}
|
|
3402
|
+
continue;
|
|
3403
|
+
}
|
|
3404
|
+
const evt = toActivityEvent(currentEvent, data);
|
|
3405
|
+
if (evt) onEvent(evt);
|
|
3406
|
+
} catch {
|
|
3407
|
+
}
|
|
3408
|
+
}
|
|
3409
|
+
}
|
|
3410
|
+
});
|
|
3411
|
+
res.on("end", () => {
|
|
3412
|
+
if (!aborted) scheduleReconnect("daemon disconnected");
|
|
3413
|
+
});
|
|
3414
|
+
}
|
|
3415
|
+
);
|
|
3416
|
+
req.on("error", (err) => {
|
|
3417
|
+
if (!aborted) scheduleReconnect(`connection failed: ${err.message}`);
|
|
3418
|
+
});
|
|
3419
|
+
};
|
|
3420
|
+
connect();
|
|
3421
|
+
return () => {
|
|
3422
|
+
aborted = true;
|
|
3423
|
+
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
3424
|
+
if (req) req.destroy();
|
|
3425
|
+
};
|
|
3426
|
+
}
|
|
3427
|
+
function toActivityEvent(eventName, data) {
|
|
3428
|
+
const payload = data.activity ?? data;
|
|
3429
|
+
const ts = normalizeTs(payload.ts);
|
|
3430
|
+
if (eventName === "snapshot") {
|
|
3431
|
+
return {
|
|
3432
|
+
kind: "snapshot",
|
|
3433
|
+
id: payload.id ?? `${ts}-snapshot`,
|
|
3434
|
+
ts,
|
|
3435
|
+
hash: payload.hash ?? "",
|
|
3436
|
+
summary: payload.argsSummary ?? payload.tool ?? "snapshot",
|
|
3437
|
+
fileCount: typeof payload.fileCount === "number" ? payload.fileCount : 0
|
|
3438
|
+
};
|
|
3439
|
+
}
|
|
3440
|
+
if (eventName !== "activity" && eventName !== "add") return null;
|
|
3441
|
+
const toolName = payload.tool ?? payload.toolName;
|
|
3442
|
+
if (!toolName) return null;
|
|
3443
|
+
const verdict = (() => {
|
|
3444
|
+
const d = payload.decision ?? payload.status;
|
|
3445
|
+
if (d === "allow" || d === "observe-allow" || d === "trust") return "allow";
|
|
3446
|
+
if (d === "block" || d === "deny" || d === "denied" || d === "dlp") return "block";
|
|
3447
|
+
if (d === "review") return "review";
|
|
3448
|
+
return "pending";
|
|
3449
|
+
})();
|
|
3450
|
+
let preview;
|
|
3451
|
+
if (typeof payload.args?.command === "string" && payload.args.command.length > 0) {
|
|
3452
|
+
preview = payload.args.command.replace(/\s+/g, " ").slice(0, 70);
|
|
3453
|
+
} else if (typeof payload.args?.file_path === "string" && payload.args.file_path.length > 0) {
|
|
3454
|
+
preview = compactPath(payload.args.file_path).slice(0, 70);
|
|
3455
|
+
} else if (typeof payload.args?.path === "string" && payload.args.path.length > 0) {
|
|
3456
|
+
preview = compactPath(payload.args.path).slice(0, 70);
|
|
3457
|
+
} else {
|
|
3458
|
+
preview = JSON.stringify(payload.args ?? {}).replace(/\s+/g, " ").slice(0, 70);
|
|
3459
|
+
}
|
|
3460
|
+
return {
|
|
3461
|
+
kind: "tool",
|
|
3462
|
+
id: payload.id ?? `${ts}-${toolName}`,
|
|
3463
|
+
ts,
|
|
3464
|
+
tool: toolName,
|
|
3465
|
+
agent: payload.agent,
|
|
3466
|
+
preview,
|
|
3467
|
+
verdict,
|
|
3468
|
+
reason: payload.reason,
|
|
3469
|
+
checkedBy: payload.checkedBy,
|
|
3470
|
+
sessionId: payload.sessionId,
|
|
3471
|
+
mcpServer: payload.mcpServer,
|
|
3472
|
+
// Only `add` SSE events are sent to the approval channel — those
|
|
3473
|
+
// are real "queued for human approval" entries. `activity` events
|
|
3474
|
+
// with status:'pending' look identical at the wire level (no
|
|
3475
|
+
// decision, transient) but should NOT pop the notification card.
|
|
3476
|
+
isApprovalRequest: eventName === "add"
|
|
3477
|
+
};
|
|
3478
|
+
}
|
|
3479
|
+
var TEST_TOOLS, EMPTY_SIGNALS, FORENSIC_CATEGORIES, SSE_BACKOFF_INITIAL_MS, SSE_BACKOFF_MAX_MS;
|
|
3480
|
+
var init_data = __esm({
|
|
3481
|
+
"src/tui/dashboard/data.ts"() {
|
|
3482
|
+
"use strict";
|
|
3483
|
+
init_blast();
|
|
3484
|
+
init_daemon();
|
|
3485
|
+
init_costSync();
|
|
3486
|
+
init_shields();
|
|
3487
|
+
init_scan_watermark();
|
|
3488
|
+
TEST_TOOLS = /* @__PURE__ */ new Set(["Bash", "bash"]);
|
|
3489
|
+
EMPTY_SIGNALS = {
|
|
3490
|
+
pii: 0,
|
|
3491
|
+
sensitiveFileRead: 0,
|
|
3492
|
+
privilegeEscalation: 0,
|
|
3493
|
+
destructiveOp: 0,
|
|
3494
|
+
pipeToShell: 0,
|
|
3495
|
+
evalOfRemote: 0,
|
|
3496
|
+
longOutputRedacted: 0
|
|
3497
|
+
};
|
|
3498
|
+
FORENSIC_CATEGORIES = /* @__PURE__ */ new Set([
|
|
3499
|
+
"dlp",
|
|
3500
|
+
"pii",
|
|
3501
|
+
"sensitive-file-read",
|
|
3502
|
+
"privilege-escalation",
|
|
3503
|
+
"network-exfil",
|
|
3504
|
+
"pipe-to-shell",
|
|
3505
|
+
"eval-of-remote",
|
|
3506
|
+
"destructive-op",
|
|
3507
|
+
"loop",
|
|
3508
|
+
"long-output-redacted"
|
|
3509
|
+
]);
|
|
3510
|
+
SSE_BACKOFF_INITIAL_MS = 1e3;
|
|
3511
|
+
SSE_BACKOFF_MAX_MS = 3e4;
|
|
3512
|
+
}
|
|
3513
|
+
});
|
|
3514
|
+
|
|
3515
|
+
// src/tui/dashboard/health.ts
|
|
3516
|
+
function computeHealthBadge(input) {
|
|
3517
|
+
const reasons = [];
|
|
3518
|
+
let severity = "secure";
|
|
3519
|
+
if (input.agg.dlpHits > 0) {
|
|
3520
|
+
severity = "critical";
|
|
3521
|
+
reasons.push(`${input.agg.dlpHits} DLP`);
|
|
3522
|
+
}
|
|
3523
|
+
if (input.agg.loops > 0) {
|
|
3524
|
+
severity = "critical";
|
|
3525
|
+
reasons.push(`${input.agg.loops} loops`);
|
|
3526
|
+
}
|
|
3527
|
+
const liveSevere = input.forensicAgg.privilegeEscalation + input.forensicAgg.destructiveOp + input.forensicAgg.evalOfRemote;
|
|
3528
|
+
const histSevere = input.scanSignals ? input.scanSignals.privilegeEscalation + input.scanSignals.destructiveOp + input.scanSignals.evalOfRemote : 0;
|
|
3529
|
+
const totalSevere = liveSevere + histSevere;
|
|
3530
|
+
if (totalSevere > 0) {
|
|
3531
|
+
severity = "critical";
|
|
3532
|
+
reasons.push(`${totalSevere} severe forensic`);
|
|
3533
|
+
}
|
|
3534
|
+
if (input.blast.score < 25) {
|
|
3535
|
+
severity = "critical";
|
|
3536
|
+
reasons.push(`score ${input.blast.score}/100`);
|
|
3537
|
+
}
|
|
3538
|
+
if (severity !== "critical") {
|
|
3539
|
+
const liveWarn = input.forensicAgg.pii + input.forensicAgg.sensitiveFileRead + input.forensicAgg.pipeToShell + input.forensicAgg.longOutputRedacted;
|
|
3540
|
+
const histWarn = input.scanSignals ? input.scanSignals.pii + input.scanSignals.sensitiveFileRead + input.scanSignals.pipeToShell + input.scanSignals.longOutputRedacted : 0;
|
|
3541
|
+
const totalWarn = liveWarn + histWarn;
|
|
3542
|
+
if (totalWarn > 0) {
|
|
3543
|
+
severity = "warning";
|
|
3544
|
+
reasons.push(`${totalWarn} forensic`);
|
|
3545
|
+
}
|
|
3546
|
+
if (input.blast.paths.length > 0) {
|
|
3547
|
+
severity = "warning";
|
|
3548
|
+
reasons.push(`${input.blast.paths.length} paths`);
|
|
3549
|
+
}
|
|
3550
|
+
if (input.shieldStatus && input.shieldStatus.inactive.length > 0) {
|
|
3551
|
+
severity = "warning";
|
|
3552
|
+
reasons.push(`${input.shieldStatus.inactive.length} shields off`);
|
|
3553
|
+
}
|
|
3554
|
+
if (input.blast.score >= 25 && input.blast.score < 50) {
|
|
3555
|
+
severity = "warning";
|
|
3556
|
+
reasons.push(`score ${input.blast.score}/100`);
|
|
3557
|
+
}
|
|
3558
|
+
}
|
|
3559
|
+
const cappedReasons = reasons.slice(0, 3);
|
|
3560
|
+
return {
|
|
3561
|
+
severity,
|
|
3562
|
+
reasons: cappedReasons,
|
|
3563
|
+
hint: severity === "secure" ? void 0 : "see node9 scan"
|
|
3564
|
+
};
|
|
3565
|
+
}
|
|
3566
|
+
var init_health = __esm({
|
|
3567
|
+
"src/tui/dashboard/health.ts"() {
|
|
3568
|
+
"use strict";
|
|
3569
|
+
}
|
|
3570
|
+
});
|
|
3571
|
+
|
|
3572
|
+
// src/tui/dashboard/format.ts
|
|
3573
|
+
function formatCost(usd) {
|
|
3574
|
+
if (usd === 0) return "$0";
|
|
3575
|
+
if (usd < 1) return `$${usd.toFixed(2)}`;
|
|
3576
|
+
if (usd < 100) return `$${usd.toFixed(2)}`;
|
|
3577
|
+
if (usd < 1e4) return `$${Math.round(usd).toLocaleString()}`;
|
|
3578
|
+
return `$${(usd / 1e3).toFixed(1)}K`;
|
|
3579
|
+
}
|
|
3580
|
+
function formatTokens(n) {
|
|
3581
|
+
if (n < 1e3) return `${n}`;
|
|
3582
|
+
if (n < 1e6) return `${(n / 1e3).toFixed(n < 1e4 ? 1 : 0)}K`;
|
|
3583
|
+
return `${(n / 1e6).toFixed(1)}M`;
|
|
3584
|
+
}
|
|
3585
|
+
function formatPct(pct) {
|
|
3586
|
+
if (!Number.isFinite(pct)) return "\u2014";
|
|
3587
|
+
return `${Math.round(pct)}%`;
|
|
3588
|
+
}
|
|
3589
|
+
function cacheHitRate(cost) {
|
|
3590
|
+
const denom = cost.cacheReadTokens + cost.inputTokens;
|
|
3591
|
+
if (denom <= 0) return 0;
|
|
3592
|
+
return cost.cacheReadTokens / denom * 100;
|
|
3593
|
+
}
|
|
3594
|
+
function shortenModel(model) {
|
|
3595
|
+
return model.replace(/^claude-/, "").replace(/-2025\d{4}$/, "");
|
|
3596
|
+
}
|
|
3597
|
+
function truncate(s, width) {
|
|
3598
|
+
return s.length <= width ? s : s.slice(0, width - 1) + "\u2026";
|
|
3599
|
+
}
|
|
3600
|
+
function localTimeOf(ts) {
|
|
3601
|
+
let d;
|
|
3602
|
+
if (typeof ts === "number" && Number.isFinite(ts)) {
|
|
3603
|
+
d = new Date(ts);
|
|
3604
|
+
} else if (typeof ts === "string" && ts.length > 0) {
|
|
3605
|
+
d = new Date(ts);
|
|
3606
|
+
} else {
|
|
3607
|
+
return "--:--:--";
|
|
3608
|
+
}
|
|
3609
|
+
if (Number.isNaN(d.getTime())) return "--:--:--";
|
|
3610
|
+
const hh = String(d.getHours()).padStart(2, "0");
|
|
3611
|
+
const mm = String(d.getMinutes()).padStart(2, "0");
|
|
3612
|
+
const ss = String(d.getSeconds()).padStart(2, "0");
|
|
3613
|
+
return `${hh}:${mm}:${ss}`;
|
|
3614
|
+
}
|
|
3615
|
+
var init_format = __esm({
|
|
3616
|
+
"src/tui/dashboard/format.ts"() {
|
|
3617
|
+
"use strict";
|
|
3618
|
+
}
|
|
3619
|
+
});
|
|
3620
|
+
|
|
3621
|
+
// src/tui/dashboard/panels.tsx
|
|
3622
|
+
import { Box, Text } from "ink";
|
|
3623
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3624
|
+
function Header(props) {
|
|
3625
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "row", justifyContent: "space-between", paddingX: 1, children: [
|
|
3626
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
3627
|
+
/* @__PURE__ */ jsx(Text, { color: COL.brand, bold: true, children: "\u{1F6E1} node9 dashboard" }),
|
|
3628
|
+
/* @__PURE__ */ jsx(Text, { children: " " }),
|
|
3629
|
+
/* @__PURE__ */ jsx(Text, { color: props.connected ? COL.live : COL.liveOff, children: "\u25CF" }),
|
|
3630
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: props.connected ? " live" : " offline" }),
|
|
3631
|
+
props.lastAgent ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` ${props.lastAgent}` }) : null
|
|
3632
|
+
] }),
|
|
3633
|
+
/* @__PURE__ */ jsx(Box, { children: renderHealthBadge(props.health) })
|
|
3634
|
+
] });
|
|
3635
|
+
}
|
|
3636
|
+
function renderHealthBadge(h) {
|
|
3637
|
+
if (h.severity === "secure") {
|
|
3638
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3639
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " \xB7 " }),
|
|
3640
|
+
/* @__PURE__ */ jsx(Text, { color: COL.live, children: "\u2713 secure" })
|
|
3641
|
+
] });
|
|
3642
|
+
}
|
|
3643
|
+
const icon = h.severity === "critical" ? "\u{1F6D1}" : "\u26A0";
|
|
3644
|
+
const color = h.severity === "critical" ? COL.liveOff : COL.panelHigh;
|
|
3645
|
+
const summary = h.reasons.length > 0 ? h.reasons.join(", ") : "risk";
|
|
3646
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3647
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " \xB7 " }),
|
|
3648
|
+
/* @__PURE__ */ jsx(Text, { color, bold: true, children: `${icon} ` }),
|
|
3649
|
+
/* @__PURE__ */ jsx(Text, { color, children: summary })
|
|
3650
|
+
] });
|
|
3651
|
+
}
|
|
3652
|
+
function HighLevel(props) {
|
|
3653
|
+
const { agg, cost } = props;
|
|
3654
|
+
const blockColor = agg.block > 0 ? COL.liveOff : COL.textDim;
|
|
3655
|
+
return /* @__PURE__ */ jsxs(
|
|
3656
|
+
Box,
|
|
3657
|
+
{
|
|
3658
|
+
flexDirection: "column",
|
|
3659
|
+
borderStyle: "round",
|
|
3660
|
+
borderColor: COL.panelHigh,
|
|
3661
|
+
paddingX: 1,
|
|
3662
|
+
marginX: 1,
|
|
3663
|
+
children: [
|
|
3664
|
+
/* @__PURE__ */ jsxs(Text, { wrap: "truncate-end", children: [
|
|
3665
|
+
/* @__PURE__ */ jsx(Text, { color: COL.brand, bold: true, children: "HIGH LEVEL" }),
|
|
3666
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: ` \xB7 ${labelFor(props.window)}` })
|
|
3667
|
+
] }),
|
|
3668
|
+
/* @__PURE__ */ jsxs(Text, { wrap: "truncate-end", children: [
|
|
3669
|
+
!cost ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "cost loading\u2026 " }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3670
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: formatCost(cost.totalUSD) }),
|
|
3671
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " cost \xB7 " }),
|
|
3672
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: formatTokens(cost.inputTokens + cost.outputTokens) }),
|
|
3673
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " tokens \xB7 " }),
|
|
3674
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: formatPct(cacheHitRate(cost)) }),
|
|
3675
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " cache " })
|
|
3676
|
+
] }),
|
|
3677
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: agg.allow.toLocaleString() }),
|
|
3678
|
+
/* @__PURE__ */ jsx(Text, { color: "#5BF58C", children: " \u2713 allow " }),
|
|
3679
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: blockColor, children: `${agg.block} ` }),
|
|
3680
|
+
/* @__PURE__ */ jsx(Text, { color: COL.liveOff, children: "\u{1F6D1} block " }),
|
|
3681
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: agg.review }),
|
|
3682
|
+
/* @__PURE__ */ jsx(Text, { color: COL.panelHigh, children: " \u{1F7E1} review " }),
|
|
3683
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: agg.total.toLocaleString() }),
|
|
3684
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " events" })
|
|
3685
|
+
] }),
|
|
3686
|
+
/* @__PURE__ */ jsxs(Text, { wrap: "truncate-end", children: [
|
|
3687
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: agg.sessions }),
|
|
3688
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " sessions \xB7 " }),
|
|
3689
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: props.mcpPinned }),
|
|
3690
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: ` MCP (${agg.mcpCalls} calls) \xB7 ` }),
|
|
3691
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: props.skillsPinned }),
|
|
3692
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " skills pinned" })
|
|
3693
|
+
] })
|
|
3694
|
+
]
|
|
3695
|
+
}
|
|
3696
|
+
);
|
|
3697
|
+
}
|
|
3698
|
+
function labelFor(w) {
|
|
3699
|
+
switch (w) {
|
|
3700
|
+
case "now":
|
|
3701
|
+
return "since dashboard opened";
|
|
3702
|
+
case "1d":
|
|
3703
|
+
return "last 24 hours";
|
|
3704
|
+
case "7d":
|
|
3705
|
+
return "last 7 days";
|
|
3706
|
+
case "30d":
|
|
3707
|
+
return "last 30 days";
|
|
3708
|
+
case "60d":
|
|
3709
|
+
return "last 60 days";
|
|
3710
|
+
}
|
|
3711
|
+
}
|
|
3712
|
+
function NotificationArea(props) {
|
|
3713
|
+
const { notification } = props;
|
|
3714
|
+
const borderColor = (() => {
|
|
3715
|
+
switch (notification.kind) {
|
|
3716
|
+
case "approval":
|
|
3717
|
+
return COL.brand;
|
|
3718
|
+
case "resolved":
|
|
3719
|
+
return notification.outcome === "allow" ? "#5BF58C" : notification.outcome === "deny" ? COL.liveOff : COL.panelHigh;
|
|
3720
|
+
case "forensic":
|
|
3721
|
+
return COL.liveOff;
|
|
3722
|
+
case "block":
|
|
3723
|
+
return COL.liveOff;
|
|
3724
|
+
case "review":
|
|
3725
|
+
case "loop":
|
|
3726
|
+
return COL.panelHigh;
|
|
3727
|
+
case "idle":
|
|
3728
|
+
return COL.textDim;
|
|
3729
|
+
}
|
|
3730
|
+
})();
|
|
3731
|
+
return /* @__PURE__ */ jsx(
|
|
3732
|
+
Box,
|
|
3733
|
+
{
|
|
3734
|
+
flexDirection: "column",
|
|
3735
|
+
borderStyle: "round",
|
|
3736
|
+
borderColor,
|
|
3737
|
+
paddingX: 1,
|
|
3738
|
+
marginX: 1,
|
|
3739
|
+
height: NOTIFICATION_HEIGHT,
|
|
3740
|
+
children: renderNotificationBody(notification)
|
|
3741
|
+
}
|
|
3742
|
+
);
|
|
3743
|
+
}
|
|
3744
|
+
function renderNotificationBody(n) {
|
|
3745
|
+
if (n.kind === "approval") return renderApproval(n.event, n.status);
|
|
3746
|
+
if (n.kind === "resolved") return renderResolved(n.event, n.outcome);
|
|
3747
|
+
if (n.kind === "forensic") return renderForensic(n.category, n.sessionId);
|
|
3748
|
+
if (n.kind === "block") return renderEventInfo(n.event, "\u{1F6D1} BLOCKED", COL.liveOff, n.ageMs);
|
|
3749
|
+
if (n.kind === "review") return renderEventInfo(n.event, "\u{1F7E1} REVIEWED", COL.panelHigh, n.ageMs);
|
|
3750
|
+
if (n.kind === "loop")
|
|
3751
|
+
return renderEventInfo(n.event, "\u{1F501} LOOP DETECTED", COL.panelHigh, n.ageMs);
|
|
3752
|
+
return renderIdle(n.blastScore);
|
|
3753
|
+
}
|
|
3754
|
+
function renderForensic(category, sessionId) {
|
|
3755
|
+
const label = (() => {
|
|
3756
|
+
switch (category) {
|
|
3757
|
+
case "privilege-escalation":
|
|
3758
|
+
return "PRIVILEGE ESCALATION";
|
|
3759
|
+
case "destructive-op":
|
|
3760
|
+
return "DESTRUCTIVE OPERATION";
|
|
3761
|
+
case "eval-of-remote":
|
|
3762
|
+
return "EVAL OF REMOTE CONTENT";
|
|
3763
|
+
default:
|
|
3764
|
+
return category.toUpperCase().replace(/-/g, " ");
|
|
3765
|
+
}
|
|
3766
|
+
})();
|
|
3767
|
+
const sid = sessionId ? `\xB7${sessionId.slice(0, 8)}` : "";
|
|
3768
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3769
|
+
/* @__PURE__ */ jsxs(Text, { wrap: "truncate-end", children: [
|
|
3770
|
+
/* @__PURE__ */ jsx(Text, { color: COL.liveOff, bold: true, children: `\u{1F6D1} CRITICAL ` }),
|
|
3771
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: label }),
|
|
3772
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: ` ${sid}` })
|
|
3773
|
+
] }),
|
|
3774
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, wrap: "truncate-end", children: "forensic finding \xB7 see [2] scan for details (Claude session)" })
|
|
3775
|
+
] });
|
|
3776
|
+
}
|
|
3777
|
+
function renderApproval(event, status) {
|
|
3778
|
+
if (event.kind !== "tool") return null;
|
|
3779
|
+
const agent = event.agent ? capitalize(event.agent) : "agent";
|
|
3780
|
+
const sid = event.sessionId ? `\xB7${event.sessionId.slice(0, 4)}` : "";
|
|
3781
|
+
const subject = `${event.tool} ${event.preview}`;
|
|
3782
|
+
const actionLine = (() => {
|
|
3783
|
+
if (status.kind === "idle") {
|
|
3784
|
+
return /* @__PURE__ */ jsxs(Text, { wrap: "truncate-end", children: [
|
|
3785
|
+
event.checkedBy ? /* @__PURE__ */ jsx(
|
|
3786
|
+
Text,
|
|
3787
|
+
{
|
|
3788
|
+
dimColor: true,
|
|
3789
|
+
children: `${event.checkedBy}${event.reason ? ` \u2014 ${event.reason}` : ""} `
|
|
3790
|
+
}
|
|
3791
|
+
) : null,
|
|
3792
|
+
/* @__PURE__ */ jsx(Text, { color: "#5BF58C", children: "[a]" }),
|
|
3793
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "llow " }),
|
|
3794
|
+
/* @__PURE__ */ jsx(Text, { color: COL.liveOff, children: "[d]" }),
|
|
3795
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "eny " }),
|
|
3796
|
+
/* @__PURE__ */ jsx(Text, { color: COL.panelHigh, children: "[t]" }),
|
|
3797
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "rust " }),
|
|
3798
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "[Esc]" })
|
|
3799
|
+
] });
|
|
3800
|
+
}
|
|
3801
|
+
if (status.kind === "sending") return /* @__PURE__ */ jsx(Text, { dimColor: true, children: "sending decision\u2026" });
|
|
3802
|
+
if (status.kind === "ok") {
|
|
3803
|
+
const v = status.verdict;
|
|
3804
|
+
const color = v === "allow" ? "#5BF58C" : v === "deny" ? COL.liveOff : COL.panelHigh;
|
|
3805
|
+
const label = v === "allow" ? "\u2713 approved" : v === "deny" ? "\u2717 denied" : "\u2605 trusted";
|
|
3806
|
+
return /* @__PURE__ */ jsx(Text, { color, children: label });
|
|
3807
|
+
}
|
|
3808
|
+
return /* @__PURE__ */ jsx(Text, { color: COL.liveOff, children: `\u26A0 failed: ${status.message} (retry [a/d/t] or [Esc])` });
|
|
3809
|
+
})();
|
|
3810
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3811
|
+
/* @__PURE__ */ jsxs(Text, { wrap: "truncate-end", children: [
|
|
3812
|
+
/* @__PURE__ */ jsx(Text, { color: COL.brand, bold: true, children: "\u26A0 APPROVAL" }),
|
|
3813
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: ` \xB7 ${agent}${sid} ` }),
|
|
3814
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: subject })
|
|
3815
|
+
] }),
|
|
3816
|
+
actionLine
|
|
3817
|
+
] });
|
|
3818
|
+
}
|
|
3819
|
+
function renderResolved(event, outcome) {
|
|
3820
|
+
if (event.kind !== "tool") return null;
|
|
3821
|
+
const agent = event.agent ? capitalize(event.agent) : "agent";
|
|
3822
|
+
const subject = `${event.tool} ${event.preview}`;
|
|
3823
|
+
const color = outcome === "allow" ? "#5BF58C" : outcome === "deny" ? COL.liveOff : COL.panelHigh;
|
|
3824
|
+
const label = outcome === "allow" ? "\u2713 approved" : outcome === "deny" ? "\u2717 denied" : "\u2605 trusted";
|
|
3825
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3826
|
+
/* @__PURE__ */ jsxs(Text, { wrap: "truncate-end", children: [
|
|
3827
|
+
/* @__PURE__ */ jsx(Text, { color, bold: true, children: label }),
|
|
3828
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: ` \xB7 ${agent} ` }),
|
|
3829
|
+
/* @__PURE__ */ jsx(Text, { children: subject })
|
|
3830
|
+
] }),
|
|
3831
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "(dismissing\u2026)" })
|
|
3832
|
+
] });
|
|
3833
|
+
}
|
|
3834
|
+
function renderEventInfo(event, label, color, ageMs) {
|
|
3835
|
+
if (event.kind !== "tool") return null;
|
|
3836
|
+
const ago = ageMs < 1e3 ? "just now" : `${Math.floor(ageMs / 1e3)}s ago`;
|
|
3837
|
+
const agent = event.agent ? capitalize(event.agent) : "agent";
|
|
3838
|
+
const subject = `${event.tool} ${event.preview}`;
|
|
3839
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3840
|
+
/* @__PURE__ */ jsxs(Text, { wrap: "truncate-end", children: [
|
|
3841
|
+
/* @__PURE__ */ jsx(Text, { color, bold: true, children: label }),
|
|
3842
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: ` \xB7 ${agent} ` }),
|
|
3843
|
+
/* @__PURE__ */ jsx(Text, { children: subject })
|
|
3844
|
+
] }),
|
|
3845
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, wrap: "truncate-end", children: event.checkedBy ? `rule: ${event.checkedBy} \xB7 ${ago}` : ago })
|
|
3846
|
+
] });
|
|
3847
|
+
}
|
|
3848
|
+
function renderIdle(blastScore) {
|
|
3849
|
+
const scoreColor = blastScore >= 80 ? "#5BF58C" : blastScore >= 50 ? COL.panelHigh : COL.liveOff;
|
|
3850
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3851
|
+
/* @__PURE__ */ jsxs(Text, { wrap: "truncate-end", children: [
|
|
3852
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2713 no recent alerts \xB7 blast " }),
|
|
3853
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: scoreColor, children: `${blastScore}/100` })
|
|
3854
|
+
] }),
|
|
3855
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "(approvals + recent blocks/loops appear here)" })
|
|
3856
|
+
] });
|
|
3857
|
+
}
|
|
3858
|
+
function LiveLog(props) {
|
|
3859
|
+
const filtered = applyFilter(props.events, props.filter);
|
|
3860
|
+
const visible = filtered.slice(-props.maxRows);
|
|
3861
|
+
const padCount = Math.max(0, props.maxRows - visible.length);
|
|
3862
|
+
return /* @__PURE__ */ jsxs(
|
|
3863
|
+
Box,
|
|
3864
|
+
{
|
|
3865
|
+
flexDirection: "column",
|
|
3866
|
+
borderStyle: "round",
|
|
3867
|
+
borderColor: COL.panelLive,
|
|
3868
|
+
paddingX: 1,
|
|
3869
|
+
marginX: 1,
|
|
3870
|
+
flexGrow: 1,
|
|
3871
|
+
children: [
|
|
3872
|
+
/* @__PURE__ */ jsxs(Text, { wrap: "truncate-end", children: [
|
|
3873
|
+
/* @__PURE__ */ jsx(Text, { color: COL.brand, bold: true, children: "LIVE" }),
|
|
3874
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: ` \xB7 last ${props.maxRows} events` }),
|
|
3875
|
+
props.filter || props.filterInputMode ? /* @__PURE__ */ jsxs(Text, { children: [
|
|
3876
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " " }),
|
|
3877
|
+
/* @__PURE__ */ jsx(Text, { color: COL.panelHigh, children: props.filterInputMode ? `\u{1F50D} /${props.filter}_` : `\u{1F50D} /${props.filter}` }),
|
|
3878
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: props.filterInputMode ? " [Enter] apply [Esc] cancel" : ` ${filtered.length}/${props.events.length} matches [Esc] clear` })
|
|
3879
|
+
] }) : null
|
|
3880
|
+
] }),
|
|
3881
|
+
props.errorBanner ? /* @__PURE__ */ jsx(Text, { color: COL.liveOff, children: `\u26A0 ${props.errorBanner}` }) : null,
|
|
3882
|
+
Array.from(
|
|
3883
|
+
{ length: padCount },
|
|
3884
|
+
(_, i) => i === 0 && visible.length === 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: props.filter ? `(no events match "${props.filter}" \u2014 [Esc] to clear)` : "(no activity yet \u2014 agent must be running and daemon must be up)" }, `pad-${i}`) : /* @__PURE__ */ jsx(Text, { children: " " }, `pad-${i}`)
|
|
3885
|
+
),
|
|
3886
|
+
visible.map((e) => /* @__PURE__ */ jsx(ActivityRow, { event: e }, e.id))
|
|
3887
|
+
]
|
|
3888
|
+
}
|
|
3889
|
+
);
|
|
3890
|
+
}
|
|
3891
|
+
function applyFilter(events, filter) {
|
|
3892
|
+
if (!filter) return events;
|
|
3893
|
+
const needle = filter.toLowerCase();
|
|
3894
|
+
return events.filter((e) => {
|
|
3895
|
+
if (e.kind === "snapshot") {
|
|
3896
|
+
return e.hash.toLowerCase().includes(needle) || e.summary.toLowerCase().includes(needle);
|
|
3897
|
+
}
|
|
3898
|
+
if (e.tool.toLowerCase().includes(needle)) return true;
|
|
3899
|
+
if (e.agent && e.agent.toLowerCase().includes(needle)) return true;
|
|
3900
|
+
if (e.preview.toLowerCase().includes(needle)) return true;
|
|
3901
|
+
if (e.checkedBy && e.checkedBy.toLowerCase().includes(needle)) return true;
|
|
3902
|
+
if (e.verdict.toLowerCase().includes(needle)) return true;
|
|
3903
|
+
return false;
|
|
3904
|
+
});
|
|
3905
|
+
}
|
|
3906
|
+
function ActivityRow({ event }) {
|
|
3907
|
+
const t = localTimeOf(event.ts);
|
|
3908
|
+
if (event.kind === "snapshot") {
|
|
3909
|
+
const filesSuffix = event.fileCount > 0 ? ` \xB7 ${event.fileCount} file${event.fileCount === 1 ? "" : "s"}` : "";
|
|
3910
|
+
return /* @__PURE__ */ jsxs(Text, { wrap: "truncate-end", children: [
|
|
3911
|
+
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
3912
|
+
t,
|
|
3913
|
+
" "
|
|
3914
|
+
] }),
|
|
3915
|
+
/* @__PURE__ */ jsx(Text, { color: COL.agentClaude, children: "\u{1F4F8} snapshot" }),
|
|
3916
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: ` ${event.hash} ` }),
|
|
3917
|
+
/* @__PURE__ */ jsx(Text, { children: event.summary }),
|
|
3918
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: filesSuffix })
|
|
3919
|
+
] });
|
|
3920
|
+
}
|
|
3921
|
+
const agentLabel = event.agent ? `[${truncate(capitalize(event.agent), 8)}]`.padEnd(10) : " ".repeat(10);
|
|
3922
|
+
const isLoop = event.checkedBy === "loop-detected";
|
|
3923
|
+
const verdictIcon = isLoop ? "\u{1F501}" : event.verdict === "block" ? "\u{1F6D1}" : event.verdict === "review" ? "\u{1F7E1}" : event.verdict === "allow" ? "\u2713 " : "\u2026 ";
|
|
3924
|
+
const verdictColor = isLoop ? COL.panelHigh : event.verdict === "block" ? COL.liveOff : event.verdict === "review" ? COL.panelHigh : event.verdict === "allow" ? "#5BF58C" : COL.textDim;
|
|
3925
|
+
const agentLower = (event.agent ?? "").toLowerCase();
|
|
3926
|
+
const agentColor = agentLower.startsWith("claude") ? COL.agentClaude : agentLower.startsWith("gemini") ? COL.agentGemini : agentLower.startsWith("codex") ? COL.agentCodex : COL.agentShell;
|
|
3927
|
+
return /* @__PURE__ */ jsxs(Text, { wrap: "truncate-end", children: [
|
|
3928
|
+
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
3929
|
+
t,
|
|
3930
|
+
" "
|
|
3931
|
+
] }),
|
|
3932
|
+
/* @__PURE__ */ jsx(Text, { color: agentColor, children: agentLabel }),
|
|
3933
|
+
/* @__PURE__ */ jsx(Text, { children: " " }),
|
|
3934
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: truncate(event.tool, 14).padEnd(14) }),
|
|
3935
|
+
/* @__PURE__ */ jsxs(Text, { color: verdictColor, children: [
|
|
3936
|
+
verdictIcon,
|
|
3937
|
+
" "
|
|
3938
|
+
] }),
|
|
3939
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: event.preview })
|
|
3940
|
+
] });
|
|
3941
|
+
}
|
|
3942
|
+
function capitalize(s) {
|
|
3943
|
+
return s ? s.charAt(0).toUpperCase() + s.slice(1) : "";
|
|
3944
|
+
}
|
|
3945
|
+
function Report(props) {
|
|
3946
|
+
const { agg, cost } = props;
|
|
3947
|
+
const maxTool = Math.max(1, ...agg.byTool.map((t) => t.calls));
|
|
3948
|
+
const maxShell = Math.max(1, ...agg.byShell.map((s) => s.count));
|
|
3949
|
+
const topModels = (cost?.byModel ?? []).slice(0, 3);
|
|
3950
|
+
const maxModelCost = Math.max(1, ...topModels.map((m) => m.costUSD));
|
|
3951
|
+
return (
|
|
3952
|
+
// Fixed height so adding/removing model rows doesn't reflow the rest
|
|
3953
|
+
// of the dashboard. Worst case: tools or shell column = 6 rows
|
|
3954
|
+
// (1 header + 5 data). Plus title (1) + 2 borders = 9.
|
|
3955
|
+
/* @__PURE__ */ jsxs(
|
|
3956
|
+
Box,
|
|
3957
|
+
{
|
|
3958
|
+
flexDirection: "column",
|
|
3959
|
+
borderStyle: "round",
|
|
3960
|
+
borderColor: COL.panelReport,
|
|
3961
|
+
paddingX: 1,
|
|
3962
|
+
marginX: 1,
|
|
3963
|
+
height: REPORT_PANEL_HEIGHT,
|
|
3964
|
+
children: [
|
|
3965
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
3966
|
+
/* @__PURE__ */ jsx(Text, { color: COL.brand, bold: true, children: "REPORT" }),
|
|
3967
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: ` \xB7 ${labelFor(props.window)}` })
|
|
3968
|
+
] }),
|
|
3969
|
+
/* @__PURE__ */ jsxs(Box, { flexDirection: "row", children: [
|
|
3970
|
+
/* @__PURE__ */ jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [
|
|
3971
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, wrap: "truncate-end", children: "Tools".padEnd(13) + "calls".padStart(6) + "blk".padStart(5) }),
|
|
3972
|
+
agg.byTool.length === 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "(no tools)" }) : agg.byTool.map((t) => /* @__PURE__ */ jsxs(Text, { wrap: "truncate-end", children: [
|
|
3973
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: bar(t.calls, maxTool, 4) }),
|
|
3974
|
+
/* @__PURE__ */ jsx(Text, { children: ` ${truncate(t.tool, 11).padEnd(11)}` }),
|
|
3975
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: `${t.calls}`.padStart(5) }),
|
|
3976
|
+
/* @__PURE__ */ jsx(Text, { color: t.blocked > 0 ? COL.liveOff : COL.textDim, children: ` ${t.blocked}`.padStart(5) })
|
|
3977
|
+
] }, t.tool))
|
|
3978
|
+
] }),
|
|
3979
|
+
/* @__PURE__ */ jsxs(Box, { flexDirection: "column", flexGrow: 1, marginLeft: 2, children: [
|
|
3980
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, wrap: "truncate-end", children: "Shell".padEnd(13) + "calls".padStart(6) + "blk".padStart(5) }),
|
|
3981
|
+
agg.byShell.length === 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "(no shell)" }) : agg.byShell.map((s) => /* @__PURE__ */ jsxs(Text, { wrap: "truncate-end", children: [
|
|
3982
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: bar(s.count, maxShell, 4) }),
|
|
3983
|
+
/* @__PURE__ */ jsx(Text, { children: ` ${truncate(s.cmd, 11).padEnd(11)}` }),
|
|
3984
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: `${s.count}`.padStart(5) }),
|
|
3985
|
+
/* @__PURE__ */ jsx(Text, { color: s.blocked > 0 ? COL.liveOff : COL.textDim, children: ` ${s.blocked}`.padStart(5) })
|
|
3986
|
+
] }, s.cmd))
|
|
3987
|
+
] }),
|
|
3988
|
+
/* @__PURE__ */ jsxs(Box, { flexDirection: "column", flexGrow: 1, marginLeft: 2, children: [
|
|
3989
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, wrap: "truncate-end", children: "Models".padEnd(15) + "cost".padStart(8) }),
|
|
3990
|
+
topModels.length === 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "(cost loading\u2026)" }) : topModels.map((m) => /* @__PURE__ */ jsxs(Text, { wrap: "truncate-end", children: [
|
|
3991
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: bar(m.costUSD, maxModelCost, 4) }),
|
|
3992
|
+
/* @__PURE__ */ jsx(Text, { children: ` ${truncate(shortenModel(m.model), 13).padEnd(13)}` }),
|
|
3993
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: formatCost(m.costUSD).padStart(7) })
|
|
3994
|
+
] }, m.model))
|
|
3995
|
+
] })
|
|
3996
|
+
] })
|
|
3997
|
+
]
|
|
3998
|
+
}
|
|
3999
|
+
)
|
|
4000
|
+
);
|
|
4001
|
+
}
|
|
4002
|
+
function bar(value, max, width) {
|
|
4003
|
+
if (max <= 0 || width <= 0) return "\u2591".repeat(width);
|
|
4004
|
+
const filled = Math.max(1, Math.round(value / max * width));
|
|
4005
|
+
return "\u2588".repeat(filled) + "\u2591".repeat(Math.max(0, width - filled));
|
|
4006
|
+
}
|
|
4007
|
+
function Risk(props) {
|
|
4008
|
+
const dlpHits = props.agg.dlpHits;
|
|
4009
|
+
const loopHits = props.agg.loops;
|
|
4010
|
+
const scoreColor = props.blast.score >= 80 ? "#5BF58C" : props.blast.score >= 50 ? COL.panelHigh : COL.liveOff;
|
|
4011
|
+
return /* @__PURE__ */ jsxs(
|
|
4012
|
+
Box,
|
|
4013
|
+
{
|
|
4014
|
+
flexDirection: "column",
|
|
4015
|
+
borderStyle: "round",
|
|
4016
|
+
borderColor: COL.panelRisk,
|
|
4017
|
+
paddingX: 1,
|
|
4018
|
+
marginX: 1,
|
|
4019
|
+
height: RISK_PANEL_HEIGHT,
|
|
4020
|
+
children: [
|
|
4021
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
4022
|
+
/* @__PURE__ */ jsx(Text, { color: COL.brand, bold: true, children: "Live security" }),
|
|
4023
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: ` \xB7 ${labelFor(props.window)}` })
|
|
4024
|
+
] }),
|
|
4025
|
+
/* @__PURE__ */ jsxs(Text, { wrap: "truncate-end", children: [
|
|
4026
|
+
/* @__PURE__ */ jsx(Text, { color: COL.liveOff, children: "\u{1F511} " }),
|
|
4027
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: dlpHits }),
|
|
4028
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " DLP \xB7 " }),
|
|
4029
|
+
/* @__PURE__ */ jsx(Text, { color: COL.panelHigh, children: "\u{1F501} " }),
|
|
4030
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: loopHits }),
|
|
4031
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " loops \xB7 " }),
|
|
4032
|
+
/* @__PURE__ */ jsx(Text, { color: COL.liveOff, children: "\u{1F52D} " }),
|
|
4033
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: props.blast.paths.length }),
|
|
4034
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " paths \xB7 score " }),
|
|
4035
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: scoreColor, children: `${props.blast.score}/100` })
|
|
4036
|
+
] }),
|
|
4037
|
+
/* @__PURE__ */ jsxs(Text, { wrap: "truncate-end", children: [
|
|
4038
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: props.forensicAgg.pii }),
|
|
4039
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " pii \xB7 " }),
|
|
4040
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: props.forensicAgg.sensitiveFileRead }),
|
|
4041
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " read \xB7 " }),
|
|
4042
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: props.forensicAgg.privilegeEscalation }),
|
|
4043
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " priv \xB7 " }),
|
|
4044
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: props.forensicAgg.destructiveOp }),
|
|
4045
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " dest \xB7 " }),
|
|
4046
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: props.forensicAgg.evalOfRemote }),
|
|
4047
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " eval \xB7 " }),
|
|
4048
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: props.forensicAgg.pipeToShell }),
|
|
4049
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " pipe \xB7 " }),
|
|
4050
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: props.forensicAgg.longOutputRedacted }),
|
|
4051
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " long (Claude)" })
|
|
4052
|
+
] }),
|
|
4053
|
+
/* @__PURE__ */ jsxs(Text, { wrap: "truncate-end", children: [
|
|
4054
|
+
/* @__PURE__ */ jsx(Text, { color: COL.live, children: "\u{1F6E1} " }),
|
|
4055
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: props.shieldStatus ? props.shieldStatus.active.length : "\u2026" }),
|
|
4056
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " active \xB7 " }),
|
|
4057
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: props.shieldStatus ? props.shieldStatus.inactive.length : "\u2026" }),
|
|
4058
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " inactive" })
|
|
4059
|
+
] })
|
|
4060
|
+
]
|
|
4061
|
+
}
|
|
4062
|
+
);
|
|
4063
|
+
}
|
|
4064
|
+
function StatusBar(props) {
|
|
4065
|
+
const realtimeActive = props.view === "realtime";
|
|
4066
|
+
const reportActive = props.view === "report";
|
|
4067
|
+
const refreshedAt = localTimeOf(props.lastRefreshAt);
|
|
4068
|
+
return /* @__PURE__ */ jsxs(Box, { paddingX: 1, children: [
|
|
4069
|
+
/* @__PURE__ */ jsx(Text, { color: realtimeActive ? COL.brand : void 0, bold: realtimeActive, children: `[1] realtime ${realtimeActive ? "\u25CF" : "\u25CB"} ` }),
|
|
4070
|
+
/* @__PURE__ */ jsx(Text, { color: reportActive ? COL.brand : void 0, bold: reportActive, children: `[2] report ${reportActive ? "\u25CF" : "\u25CB"} ` }),
|
|
4071
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "\xB7 " }),
|
|
4072
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: `[r] refresh (${refreshedAt}) ` }),
|
|
4073
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "[?] help " }),
|
|
4074
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "[q] quit" })
|
|
4075
|
+
] });
|
|
4076
|
+
}
|
|
4077
|
+
function ReportView(props) {
|
|
4078
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", flexGrow: 1, paddingX: 1, children: [
|
|
4079
|
+
/* @__PURE__ */ jsxs(Box, { paddingY: 1, children: [
|
|
4080
|
+
/* @__PURE__ */ jsx(Text, { color: COL.brand, bold: true, children: "REPORT" }),
|
|
4081
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: ` \xB7 period ${props.period}` })
|
|
4082
|
+
] }),
|
|
4083
|
+
/* @__PURE__ */ jsxs(Box, { flexDirection: "column", gap: 0, children: [
|
|
4084
|
+
/* @__PURE__ */ jsx(ReportSectionStub, { label: "Security", hint: "leaks \xB7 blocks \xB7 loops \xB7 forensic 90d" }),
|
|
4085
|
+
/* @__PURE__ */ jsx(ReportSectionStub, { label: "Activity", hint: "top tools \xB7 top blocks \xB7 daily/hourly" }),
|
|
4086
|
+
/* @__PURE__ */ jsx(ReportSectionStub, { label: "Cost", hint: "per model \xB7 per day \xB7 cache hit" }),
|
|
4087
|
+
/* @__PURE__ */ jsx(ReportSectionStub, { label: "Coverage", hint: "inactive shields \xB7 reachable paths" })
|
|
4088
|
+
] }),
|
|
4089
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "(report sections land in phases 4\u20138 \u2014 press [1] to return to realtime)" }) })
|
|
4090
|
+
] });
|
|
4091
|
+
}
|
|
4092
|
+
function ReportSectionStub(props) {
|
|
4093
|
+
return /* @__PURE__ */ jsxs(
|
|
4094
|
+
Box,
|
|
4095
|
+
{
|
|
4096
|
+
borderStyle: "round",
|
|
4097
|
+
borderColor: COL.textDim,
|
|
4098
|
+
paddingX: 1,
|
|
4099
|
+
marginX: 1,
|
|
4100
|
+
flexDirection: "column",
|
|
4101
|
+
children: [
|
|
4102
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
4103
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: props.label }),
|
|
4104
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: ` \u2014 ${props.hint}` })
|
|
4105
|
+
] }),
|
|
4106
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "(coming soon)" })
|
|
4107
|
+
]
|
|
4108
|
+
}
|
|
4109
|
+
);
|
|
4110
|
+
}
|
|
4111
|
+
var COL, NOTIFICATION_HEIGHT, REPORT_PANEL_HEIGHT, RISK_PANEL_HEIGHT;
|
|
4112
|
+
var init_panels = __esm({
|
|
4113
|
+
"src/tui/dashboard/panels.tsx"() {
|
|
4114
|
+
"use strict";
|
|
4115
|
+
init_format();
|
|
4116
|
+
COL = {
|
|
4117
|
+
brand: "#FF8C42",
|
|
4118
|
+
// orange — brand
|
|
4119
|
+
live: "#5BF58C",
|
|
4120
|
+
// green — connected status
|
|
4121
|
+
liveOff: "#F55B5B",
|
|
4122
|
+
// red — disconnected
|
|
4123
|
+
panelLive: "#5B9EF5",
|
|
4124
|
+
// blue — Live panel border
|
|
4125
|
+
panelHigh: "#F5C85B",
|
|
4126
|
+
// yellow — High Level panel border
|
|
4127
|
+
panelReport: "#5BF5A0",
|
|
4128
|
+
// green — Report panel border
|
|
4129
|
+
panelRisk: "#FF6B6B",
|
|
4130
|
+
// red — Risk panel border
|
|
4131
|
+
agentClaude: "#5BF5E0",
|
|
4132
|
+
// cyan
|
|
4133
|
+
agentGemini: "#5B9EF5",
|
|
4134
|
+
// blue
|
|
4135
|
+
agentCodex: "#E05BF5",
|
|
4136
|
+
// magenta
|
|
4137
|
+
agentShell: "#F5C85B",
|
|
4138
|
+
// yellow
|
|
4139
|
+
textDim: "#888888"
|
|
4140
|
+
};
|
|
4141
|
+
NOTIFICATION_HEIGHT = 4;
|
|
4142
|
+
REPORT_PANEL_HEIGHT = 9;
|
|
4143
|
+
RISK_PANEL_HEIGHT = 6;
|
|
4144
|
+
}
|
|
4145
|
+
});
|
|
4146
|
+
|
|
4147
|
+
// src/tui/dashboard/App.tsx
|
|
4148
|
+
var App_exports = {};
|
|
4149
|
+
__export(App_exports, {
|
|
4150
|
+
App: () => App
|
|
4151
|
+
});
|
|
4152
|
+
import React, { useEffect, useMemo, useState } from "react";
|
|
4153
|
+
import fs6 from "fs";
|
|
4154
|
+
import os7 from "os";
|
|
4155
|
+
import path7 from "path";
|
|
4156
|
+
import { Box as Box2, Text as Text2, useApp, useInput, useStdout } from "ink";
|
|
4157
|
+
import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
4158
|
+
function App() {
|
|
4159
|
+
const { exit } = useApp();
|
|
4160
|
+
const { stdout } = useStdout();
|
|
4161
|
+
const [openedAt] = useState(() => Date.now());
|
|
4162
|
+
const [view, setView] = useState("realtime");
|
|
4163
|
+
const [lastRefreshAt, setLastRefreshAt] = useState(() => Date.now());
|
|
4164
|
+
const [reportPeriod] = useState("7d");
|
|
4165
|
+
const [window] = useState("now");
|
|
4166
|
+
const [events, setEvents] = useState([]);
|
|
4167
|
+
const [sseError, setSseError] = useState();
|
|
4168
|
+
const [agg, setAgg] = useState(null);
|
|
4169
|
+
const [blast, setBlast] = useState(null);
|
|
4170
|
+
const [shieldStatus, setShieldStatus] = useState(null);
|
|
4171
|
+
const [scanSignals, setScanSignals] = useState(null);
|
|
4172
|
+
const [sessionForensicAgg, setSessionForensicAgg] = useState(() => ({
|
|
4173
|
+
...EMPTY_SESSION_FORENSIC
|
|
4174
|
+
}));
|
|
4175
|
+
const [recentForensic, setRecentForensic] = useState(null);
|
|
4176
|
+
const forensicNotifyCooldownRef = React.useRef(
|
|
4177
|
+
/* @__PURE__ */ new Map()
|
|
4178
|
+
);
|
|
4179
|
+
const [costEntries, setCostEntries] = useState(null);
|
|
4180
|
+
const [skillsPinned] = useState(() => readSkillsPinned());
|
|
4181
|
+
const [mcpPinned] = useState(() => readMcpPinned());
|
|
4182
|
+
const [pendingApproval, setPendingApproval] = useState(null);
|
|
4183
|
+
const [approvalStatus, setApprovalStatus] = useState({ kind: "idle" });
|
|
4184
|
+
const [resolvedApproval, setResolvedApproval] = useState(null);
|
|
4185
|
+
const [tick, setTick] = useState(0);
|
|
4186
|
+
useEffect(() => {
|
|
4187
|
+
const id = setInterval(() => setTick((t) => t + 1), 1e3);
|
|
4188
|
+
return () => clearInterval(id);
|
|
4189
|
+
}, []);
|
|
4190
|
+
const [filter, setFilter] = useState("");
|
|
4191
|
+
const [filterInputMode, setFilterInputMode] = useState(false);
|
|
4192
|
+
const [termRows, setTermRows] = useState(stdout?.rows ?? 24);
|
|
4193
|
+
useEffect(() => {
|
|
4194
|
+
if (!stdout) return void 0;
|
|
4195
|
+
const onResize = () => setTermRows(stdout.rows ?? 24);
|
|
4196
|
+
stdout.on("resize", onResize);
|
|
4197
|
+
return () => {
|
|
4198
|
+
stdout.off("resize", onResize);
|
|
4199
|
+
};
|
|
4200
|
+
}, [stdout]);
|
|
4201
|
+
const liveMaxRows = Math.max(LIVE_MIN_ROWS, termRows - FIXED_PANELS_HEIGHT);
|
|
4202
|
+
useEffect(() => {
|
|
4203
|
+
const teardown = subscribeToSse(
|
|
4204
|
+
(e) => {
|
|
4205
|
+
if (e.kind === "tool" && e.isApprovalRequest) {
|
|
4206
|
+
setPendingApproval(e);
|
|
4207
|
+
setApprovalStatus({ kind: "idle" });
|
|
4208
|
+
setSseError(void 0);
|
|
4209
|
+
return;
|
|
4210
|
+
}
|
|
4211
|
+
setEvents((prev) => {
|
|
4212
|
+
const next = [...prev, e];
|
|
4213
|
+
return next.length > LIVE_BUFFER_CAP ? next.slice(next.length - LIVE_BUFFER_CAP) : next;
|
|
4214
|
+
});
|
|
4215
|
+
setSseError(void 0);
|
|
4216
|
+
if (e.kind === "tool" && e.verdict === "review") {
|
|
4217
|
+
setPendingApproval(e);
|
|
4218
|
+
setApprovalStatus({ kind: "idle" });
|
|
4219
|
+
}
|
|
4220
|
+
},
|
|
4221
|
+
(resolvedId, finalVerdict) => {
|
|
4222
|
+
if (finalVerdict) {
|
|
4223
|
+
setEvents(
|
|
4224
|
+
(prev) => prev.map(
|
|
4225
|
+
(e) => e.kind === "tool" && e.id === resolvedId ? { ...e, verdict: finalVerdict } : e
|
|
4226
|
+
)
|
|
4227
|
+
);
|
|
4228
|
+
}
|
|
4229
|
+
setPendingApproval((prev) => {
|
|
4230
|
+
if (!prev || prev.id !== resolvedId) return prev;
|
|
4231
|
+
if (finalVerdict && prev.kind === "tool") {
|
|
4232
|
+
const outcome = finalVerdict === "allow" ? "allow" : finalVerdict === "block" ? "deny" : null;
|
|
4233
|
+
if (outcome) {
|
|
4234
|
+
setResolvedApproval({
|
|
4235
|
+
event: prev,
|
|
4236
|
+
outcome,
|
|
4237
|
+
resolvedAt: Date.now()
|
|
4238
|
+
});
|
|
4239
|
+
}
|
|
4240
|
+
}
|
|
4241
|
+
return null;
|
|
4242
|
+
});
|
|
4243
|
+
},
|
|
4244
|
+
(forensicEvent) => {
|
|
4245
|
+
setSessionForensicAgg((prev) => applyForensicEvent(prev, forensicEvent));
|
|
4246
|
+
if (forensicEvent.severity === "critical") {
|
|
4247
|
+
const now = Date.now();
|
|
4248
|
+
const cooldown = forensicNotifyCooldownRef.current;
|
|
4249
|
+
const lastFired = cooldown.get(forensicEvent.category) ?? 0;
|
|
4250
|
+
if (now - lastFired >= FORENSIC_NOTIFY_COOLDOWN_MS) {
|
|
4251
|
+
cooldown.set(forensicEvent.category, now);
|
|
4252
|
+
setRecentForensic({
|
|
4253
|
+
category: forensicEvent.category,
|
|
4254
|
+
sessionId: forensicEvent.sessionId,
|
|
4255
|
+
firedAt: now
|
|
4256
|
+
});
|
|
4257
|
+
}
|
|
4258
|
+
}
|
|
4259
|
+
},
|
|
4260
|
+
(msg) => setSseError(msg)
|
|
4261
|
+
);
|
|
4262
|
+
return teardown;
|
|
4263
|
+
}, []);
|
|
4264
|
+
useEffect(() => {
|
|
4265
|
+
const recompute = () => {
|
|
4266
|
+
const entries = readAuditEntries();
|
|
4267
|
+
const startMs = windowStartMs(window, openedAt);
|
|
4268
|
+
setAgg(aggregateAudit(entries, startMs));
|
|
4269
|
+
};
|
|
4270
|
+
recompute();
|
|
4271
|
+
const id = setInterval(recompute, AUDIT_REFRESH_MS);
|
|
4272
|
+
return () => clearInterval(id);
|
|
4273
|
+
}, [window, openedAt]);
|
|
4274
|
+
useEffect(() => {
|
|
4275
|
+
setBlast(loadBlast());
|
|
4276
|
+
const id = setInterval(() => setBlast(loadBlast()), BLAST_REFRESH_MS);
|
|
4277
|
+
return () => clearInterval(id);
|
|
4278
|
+
}, []);
|
|
4279
|
+
useEffect(() => {
|
|
4280
|
+
setShieldStatus(loadShieldStatus());
|
|
4281
|
+
const id = setInterval(() => setShieldStatus(loadShieldStatus()), BLAST_REFRESH_MS);
|
|
4282
|
+
return () => clearInterval(id);
|
|
4283
|
+
}, []);
|
|
4284
|
+
useEffect(() => {
|
|
4285
|
+
let cancelled = false;
|
|
4286
|
+
const loadAndSet = () => {
|
|
4287
|
+
loadScanSignals().then((s) => {
|
|
4288
|
+
if (!cancelled) setScanSignals(s);
|
|
4289
|
+
});
|
|
4290
|
+
};
|
|
4291
|
+
loadAndSet();
|
|
4292
|
+
const id = setInterval(loadAndSet, COST_REFRESH_MS);
|
|
4293
|
+
return () => {
|
|
4294
|
+
cancelled = true;
|
|
4295
|
+
clearInterval(id);
|
|
4296
|
+
};
|
|
4297
|
+
}, []);
|
|
4298
|
+
const costBaselineRef = React.useRef(null);
|
|
4299
|
+
useEffect(() => {
|
|
4300
|
+
let cancelled = false;
|
|
4301
|
+
const loadAndSet = () => {
|
|
4302
|
+
loadCostEntries().then((entries) => {
|
|
4303
|
+
if (cancelled) return;
|
|
4304
|
+
if (costBaselineRef.current === null) {
|
|
4305
|
+
costBaselineRef.current = buildCostBaseline(entries);
|
|
4306
|
+
}
|
|
4307
|
+
setCostEntries(entries);
|
|
4308
|
+
});
|
|
4309
|
+
};
|
|
4310
|
+
loadAndSet();
|
|
4311
|
+
const id = setInterval(loadAndSet, COST_REFRESH_MS);
|
|
4312
|
+
return () => {
|
|
4313
|
+
cancelled = true;
|
|
4314
|
+
clearInterval(id);
|
|
4315
|
+
};
|
|
4316
|
+
}, []);
|
|
4317
|
+
const costSnapshot = useMemo(() => {
|
|
4318
|
+
if (!costEntries) return null;
|
|
4319
|
+
const baseline = costBaselineRef.current ?? /* @__PURE__ */ new Map();
|
|
4320
|
+
const adjusted = subtractCostBaseline(costEntries, baseline);
|
|
4321
|
+
return aggregateCost(adjusted, windowStartMs(window, openedAt));
|
|
4322
|
+
}, [costEntries, window, openedAt]);
|
|
4323
|
+
useInput((input, key) => {
|
|
4324
|
+
if (filterInputMode) {
|
|
4325
|
+
if (key.escape) {
|
|
4326
|
+
setFilter("");
|
|
4327
|
+
setFilterInputMode(false);
|
|
4328
|
+
return;
|
|
4329
|
+
}
|
|
4330
|
+
if (key.return) {
|
|
4331
|
+
setFilterInputMode(false);
|
|
4332
|
+
return;
|
|
4333
|
+
}
|
|
4334
|
+
if (key.backspace || key.delete) {
|
|
4335
|
+
setFilter((prev) => prev.slice(0, -1));
|
|
4336
|
+
return;
|
|
4337
|
+
}
|
|
4338
|
+
if (input && input.length === 1 && input.charCodeAt(0) >= 32) {
|
|
4339
|
+
setFilter((prev) => prev + input);
|
|
4340
|
+
return;
|
|
4341
|
+
}
|
|
4342
|
+
return;
|
|
4343
|
+
}
|
|
4344
|
+
if (pendingApproval && approvalStatus.kind !== "sending") {
|
|
4345
|
+
if (input === "a" || input === "d" || input === "t") {
|
|
4346
|
+
const decision = input === "a" ? "allow" : input === "d" ? "deny" : "trust";
|
|
4347
|
+
const id = pendingApproval.id;
|
|
4348
|
+
const eventAtAction = pendingApproval;
|
|
4349
|
+
setApprovalStatus({ kind: "sending" });
|
|
4350
|
+
void submitDecision(id, decision).then((res) => {
|
|
4351
|
+
if (res.ok) {
|
|
4352
|
+
setApprovalStatus({ kind: "ok", verdict: decision });
|
|
4353
|
+
setResolvedApproval({
|
|
4354
|
+
event: eventAtAction,
|
|
4355
|
+
outcome: decision,
|
|
4356
|
+
resolvedAt: Date.now()
|
|
4357
|
+
});
|
|
4358
|
+
setTimeout(() => {
|
|
4359
|
+
setPendingApproval((prev) => prev && prev.id === id ? null : prev);
|
|
4360
|
+
setApprovalStatus({ kind: "idle" });
|
|
4361
|
+
}, 600);
|
|
4362
|
+
} else {
|
|
4363
|
+
setApprovalStatus({ kind: "error", message: res.error ?? "unknown" });
|
|
4364
|
+
}
|
|
4365
|
+
});
|
|
4366
|
+
return;
|
|
4367
|
+
}
|
|
4368
|
+
if (key.escape) {
|
|
4369
|
+
setPendingApproval(null);
|
|
4370
|
+
setApprovalStatus({ kind: "idle" });
|
|
4371
|
+
return;
|
|
4372
|
+
}
|
|
4373
|
+
return;
|
|
4374
|
+
}
|
|
4375
|
+
if (input === "/") {
|
|
4376
|
+
setFilterInputMode(true);
|
|
4377
|
+
return;
|
|
4378
|
+
}
|
|
4379
|
+
if (key.escape && filter) {
|
|
4380
|
+
setFilter("");
|
|
4381
|
+
return;
|
|
4382
|
+
}
|
|
4383
|
+
if (input === "q" || key.ctrl && input === "c") exit();
|
|
4384
|
+
else if (input === "1") {
|
|
4385
|
+
setView("realtime");
|
|
4386
|
+
} else if (input === "2") {
|
|
4387
|
+
setView("report");
|
|
4388
|
+
} else if (input === "r") {
|
|
4389
|
+
setLastRefreshAt(Date.now());
|
|
4390
|
+
const entries = readAuditEntries();
|
|
4391
|
+
setAgg(aggregateAudit(entries, windowStartMs(window, openedAt)));
|
|
4392
|
+
setBlast(loadBlast());
|
|
4393
|
+
setShieldStatus(loadShieldStatus());
|
|
4394
|
+
void loadCostEntries().then(setCostEntries);
|
|
4395
|
+
void loadScanSignals().then(setScanSignals);
|
|
4396
|
+
}
|
|
4397
|
+
});
|
|
4398
|
+
const lastToolEvent = useMemo(
|
|
4399
|
+
() => [...events].reverse().find((e) => e.kind === "tool"),
|
|
4400
|
+
[events]
|
|
4401
|
+
);
|
|
4402
|
+
const lastAgent = useMemo(() => {
|
|
4403
|
+
if (!lastToolEvent || !lastToolEvent.agent) return void 0;
|
|
4404
|
+
const a = lastToolEvent.agent;
|
|
4405
|
+
const sid = lastToolEvent.sessionId?.slice(0, 4);
|
|
4406
|
+
return sid ? `${capitalize2(a)}\xB7${sid}` : capitalize2(a);
|
|
4407
|
+
}, [lastToolEvent]);
|
|
4408
|
+
const lastEvent = events[events.length - 1];
|
|
4409
|
+
const stickyRef = React.useRef(null);
|
|
4410
|
+
const notification = useMemo(() => {
|
|
4411
|
+
if (pendingApproval) {
|
|
4412
|
+
stickyRef.current = null;
|
|
4413
|
+
return { kind: "approval", event: pendingApproval, status: approvalStatus };
|
|
4414
|
+
}
|
|
4415
|
+
if (resolvedApproval && Date.now() - resolvedApproval.resolvedAt < RESOLVED_HOLD_MS) {
|
|
4416
|
+
stickyRef.current = null;
|
|
4417
|
+
return {
|
|
4418
|
+
kind: "resolved",
|
|
4419
|
+
event: resolvedApproval.event,
|
|
4420
|
+
outcome: resolvedApproval.outcome
|
|
4421
|
+
};
|
|
4422
|
+
}
|
|
4423
|
+
if (recentForensic && Date.now() - recentForensic.firedAt < FORENSIC_DISPLAY_MS) {
|
|
4424
|
+
stickyRef.current = null;
|
|
4425
|
+
return {
|
|
4426
|
+
kind: "forensic",
|
|
4427
|
+
category: recentForensic.category,
|
|
4428
|
+
sessionId: recentForensic.sessionId,
|
|
4429
|
+
firedAt: recentForensic.firedAt
|
|
4430
|
+
};
|
|
4431
|
+
}
|
|
4432
|
+
const now = Date.now();
|
|
4433
|
+
let candidate = null;
|
|
4434
|
+
for (const e of [...events].reverse()) {
|
|
4435
|
+
if (e.kind !== "tool") continue;
|
|
4436
|
+
if (!isNotificationWorthy(e)) continue;
|
|
4437
|
+
const ageMs = now - Date.parse(e.ts);
|
|
4438
|
+
if (Number.isNaN(ageMs) || ageMs > NOTIFICATION_RECENT_WINDOW_MS) continue;
|
|
4439
|
+
if (e.checkedBy === "loop-detected") {
|
|
4440
|
+
candidate = { kind: "loop", event: e, ageMs };
|
|
4441
|
+
break;
|
|
4442
|
+
}
|
|
4443
|
+
if (e.verdict === "block") {
|
|
4444
|
+
candidate = { kind: "block", event: e, ageMs };
|
|
4445
|
+
break;
|
|
4446
|
+
}
|
|
4447
|
+
if (e.verdict === "review") {
|
|
4448
|
+
candidate = { kind: "review", event: e, ageMs };
|
|
4449
|
+
break;
|
|
4450
|
+
}
|
|
4451
|
+
}
|
|
4452
|
+
if (!candidate) {
|
|
4453
|
+
stickyRef.current = null;
|
|
4454
|
+
return { kind: "idle", blastScore: blast?.score ?? 100 };
|
|
4455
|
+
}
|
|
4456
|
+
const sticky = stickyRef.current;
|
|
4457
|
+
if (sticky && sticky.eventId !== candidate.event.id) {
|
|
4458
|
+
const stuckFor = now - sticky.firstShownAt;
|
|
4459
|
+
if (stuckFor < NOTIFICATION_STICKY_MS) {
|
|
4460
|
+
const stickyEvent = events.find((x) => x.kind === "tool" && x.id === sticky.eventId);
|
|
4461
|
+
if (stickyEvent && stickyEvent.kind === "tool") {
|
|
4462
|
+
const stickyAge = now - Date.parse(stickyEvent.ts);
|
|
4463
|
+
if (!Number.isNaN(stickyAge) && stickyAge <= NOTIFICATION_RECENT_WINDOW_MS) {
|
|
4464
|
+
return { kind: sticky.kind, event: stickyEvent, ageMs: stickyAge };
|
|
4465
|
+
}
|
|
4466
|
+
}
|
|
4467
|
+
}
|
|
4468
|
+
}
|
|
4469
|
+
if (!sticky || sticky.eventId !== candidate.event.id) {
|
|
4470
|
+
stickyRef.current = {
|
|
4471
|
+
kind: candidate.kind,
|
|
4472
|
+
eventId: candidate.event.id,
|
|
4473
|
+
firstShownAt: now
|
|
4474
|
+
};
|
|
4475
|
+
}
|
|
4476
|
+
return candidate;
|
|
4477
|
+
}, [pendingApproval, approvalStatus, resolvedApproval, recentForensic, events, blast, tick]);
|
|
4478
|
+
const healthBadge = useMemo(
|
|
4479
|
+
() => computeHealthBadge({
|
|
4480
|
+
agg: agg ?? {
|
|
4481
|
+
total: 0,
|
|
4482
|
+
allow: 0,
|
|
4483
|
+
block: 0,
|
|
4484
|
+
review: 0,
|
|
4485
|
+
loops: 0,
|
|
4486
|
+
dlpHits: 0,
|
|
4487
|
+
sessions: 0,
|
|
4488
|
+
mcpServers: 0,
|
|
4489
|
+
mcpCalls: 0,
|
|
4490
|
+
byTool: [],
|
|
4491
|
+
byBlock: [],
|
|
4492
|
+
byShell: []
|
|
4493
|
+
},
|
|
4494
|
+
blast: blast ?? { score: 100, paths: [], envFindings: 0 },
|
|
4495
|
+
scanSignals,
|
|
4496
|
+
shieldStatus,
|
|
4497
|
+
forensicAgg: sessionForensicAgg
|
|
4498
|
+
}),
|
|
4499
|
+
[agg, blast, scanSignals, shieldStatus, sessionForensicAgg]
|
|
4500
|
+
);
|
|
4501
|
+
const loading = !agg || !blast;
|
|
4502
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", height: "100%", overflow: "hidden", children: [
|
|
4503
|
+
/* @__PURE__ */ jsx2(
|
|
4504
|
+
Header,
|
|
4505
|
+
{
|
|
4506
|
+
connected: !sseError,
|
|
4507
|
+
lastAgent,
|
|
4508
|
+
lastTs: lastEvent?.ts,
|
|
4509
|
+
health: healthBadge
|
|
4510
|
+
}
|
|
4511
|
+
),
|
|
4512
|
+
loading ? /* @__PURE__ */ jsx2(Box2, { flexGrow: 1, paddingX: 1, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Loading dashboard\u2026" }) }) : view === "realtime" ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
4513
|
+
/* @__PURE__ */ jsx2(
|
|
4514
|
+
HighLevel,
|
|
4515
|
+
{
|
|
4516
|
+
window,
|
|
4517
|
+
agg,
|
|
4518
|
+
cost: costSnapshot,
|
|
4519
|
+
skillsPinned,
|
|
4520
|
+
mcpPinned
|
|
4521
|
+
}
|
|
4522
|
+
),
|
|
4523
|
+
/* @__PURE__ */ jsx2(NotificationArea, { notification }),
|
|
4524
|
+
/* @__PURE__ */ jsx2(
|
|
4525
|
+
LiveLog,
|
|
4526
|
+
{
|
|
4527
|
+
events,
|
|
4528
|
+
errorBanner: sseError,
|
|
4529
|
+
maxRows: liveMaxRows,
|
|
4530
|
+
filter,
|
|
4531
|
+
filterInputMode
|
|
4532
|
+
}
|
|
4533
|
+
),
|
|
4534
|
+
/* @__PURE__ */ jsx2(Report, { agg, cost: costSnapshot, window }),
|
|
4535
|
+
/* @__PURE__ */ jsx2(
|
|
4536
|
+
Risk,
|
|
4537
|
+
{
|
|
4538
|
+
agg,
|
|
4539
|
+
blast,
|
|
4540
|
+
shieldStatus,
|
|
4541
|
+
forensicAgg: sessionForensicAgg,
|
|
4542
|
+
window
|
|
4543
|
+
}
|
|
4544
|
+
)
|
|
4545
|
+
] }) : /* @__PURE__ */ jsx2(ReportView, { period: reportPeriod }),
|
|
4546
|
+
/* @__PURE__ */ jsx2(StatusBar, { view, lastRefreshAt })
|
|
4547
|
+
] });
|
|
4548
|
+
}
|
|
4549
|
+
function capitalize2(s) {
|
|
4550
|
+
return s ? s.charAt(0).toUpperCase() + s.slice(1) : "";
|
|
4551
|
+
}
|
|
4552
|
+
function isNotificationWorthy(e) {
|
|
4553
|
+
if (e.kind !== "tool") return false;
|
|
4554
|
+
if (!e.checkedBy) return e.verdict === "block" || e.verdict === "review";
|
|
4555
|
+
if (e.checkedBy.startsWith("observe-mode")) return false;
|
|
4556
|
+
if (e.checkedBy === "timeout" || e.checkedBy === "popup-timeout") return false;
|
|
4557
|
+
return true;
|
|
4558
|
+
}
|
|
4559
|
+
function readMcpPinned() {
|
|
4560
|
+
try {
|
|
4561
|
+
const p = path7.join(os7.homedir(), ".node9", "mcp-pins.json");
|
|
4562
|
+
if (!fs6.existsSync(p)) return 0;
|
|
4563
|
+
const parsed = JSON.parse(fs6.readFileSync(p, "utf8"));
|
|
4564
|
+
return parsed.servers ? Object.keys(parsed.servers).length : 0;
|
|
4565
|
+
} catch {
|
|
4566
|
+
return 0;
|
|
4567
|
+
}
|
|
4568
|
+
}
|
|
4569
|
+
function readSkillsPinned() {
|
|
4570
|
+
try {
|
|
4571
|
+
const p = path7.join(os7.homedir(), ".node9", "skill-pins.json");
|
|
4572
|
+
if (!fs6.existsSync(p)) return 0;
|
|
4573
|
+
const parsed = JSON.parse(fs6.readFileSync(p, "utf8"));
|
|
4574
|
+
return parsed.roots ? Object.keys(parsed.roots).length : 0;
|
|
4575
|
+
} catch {
|
|
4576
|
+
return 0;
|
|
4577
|
+
}
|
|
4578
|
+
}
|
|
4579
|
+
var LIVE_BUFFER_CAP, AUDIT_REFRESH_MS, BLAST_REFRESH_MS, FIXED_PANELS_HEIGHT, LIVE_MIN_ROWS, NOTIFICATION_RECENT_WINDOW_MS, RESOLVED_HOLD_MS, COST_REFRESH_MS, NOTIFICATION_STICKY_MS, FORENSIC_NOTIFY_COOLDOWN_MS, FORENSIC_DISPLAY_MS;
|
|
4580
|
+
var init_App = __esm({
|
|
4581
|
+
"src/tui/dashboard/App.tsx"() {
|
|
4582
|
+
"use strict";
|
|
4583
|
+
init_types();
|
|
4584
|
+
init_data();
|
|
4585
|
+
init_health();
|
|
4586
|
+
init_panels();
|
|
4587
|
+
LIVE_BUFFER_CAP = 100;
|
|
4588
|
+
AUDIT_REFRESH_MS = 3e4;
|
|
4589
|
+
BLAST_REFRESH_MS = 5 * 6e4;
|
|
4590
|
+
FIXED_PANELS_HEIGHT = 30;
|
|
4591
|
+
LIVE_MIN_ROWS = 1;
|
|
4592
|
+
NOTIFICATION_RECENT_WINDOW_MS = 6e4;
|
|
4593
|
+
RESOLVED_HOLD_MS = 5e3;
|
|
4594
|
+
COST_REFRESH_MS = 5 * 6e4;
|
|
4595
|
+
NOTIFICATION_STICKY_MS = 1e4;
|
|
4596
|
+
FORENSIC_NOTIFY_COOLDOWN_MS = 3e4;
|
|
4597
|
+
FORENSIC_DISPLAY_MS = 5e3;
|
|
4598
|
+
}
|
|
4599
|
+
});
|
|
4600
|
+
|
|
4601
|
+
// src/tui/dashboard/index.ts
|
|
4602
|
+
var ENTER_ALT_SCREEN = "\x1B[?1049h";
|
|
4603
|
+
var EXIT_ALT_SCREEN = "\x1B[?1049l";
|
|
4604
|
+
var altScreenActive = false;
|
|
4605
|
+
function enterAltScreen() {
|
|
4606
|
+
if (altScreenActive) return;
|
|
4607
|
+
process.stdout.write(ENTER_ALT_SCREEN);
|
|
4608
|
+
altScreenActive = true;
|
|
4609
|
+
}
|
|
4610
|
+
function exitAltScreen() {
|
|
4611
|
+
if (!altScreenActive) return;
|
|
4612
|
+
process.stdout.write(EXIT_ALT_SCREEN);
|
|
4613
|
+
altScreenActive = false;
|
|
4614
|
+
}
|
|
4615
|
+
async function startMonitor() {
|
|
4616
|
+
if (!process.stdin.isTTY) {
|
|
4617
|
+
process.stderr.write(
|
|
4618
|
+
"node9 monitor requires an interactive TTY (run it directly in your terminal).\n"
|
|
4619
|
+
);
|
|
4620
|
+
process.exit(1);
|
|
4621
|
+
}
|
|
4622
|
+
const React2 = await import("react");
|
|
4623
|
+
const { render } = await import("ink");
|
|
4624
|
+
const { App: App2 } = await Promise.resolve().then(() => (init_App(), App_exports));
|
|
4625
|
+
const cleanup = () => exitAltScreen();
|
|
4626
|
+
process.on("exit", cleanup);
|
|
4627
|
+
process.on("SIGINT", () => {
|
|
4628
|
+
cleanup();
|
|
4629
|
+
process.exit(130);
|
|
4630
|
+
});
|
|
4631
|
+
process.on("SIGTERM", () => {
|
|
4632
|
+
cleanup();
|
|
4633
|
+
process.exit(143);
|
|
4634
|
+
});
|
|
4635
|
+
process.on("uncaughtException", (err) => {
|
|
4636
|
+
cleanup();
|
|
4637
|
+
setImmediate(() => {
|
|
4638
|
+
throw err;
|
|
4639
|
+
});
|
|
4640
|
+
});
|
|
4641
|
+
enterAltScreen();
|
|
4642
|
+
try {
|
|
4643
|
+
const instance = render(React2.createElement(App2));
|
|
4644
|
+
await instance.waitUntilExit();
|
|
4645
|
+
} finally {
|
|
4646
|
+
exitAltScreen();
|
|
4647
|
+
}
|
|
4648
|
+
}
|
|
4649
|
+
export {
|
|
4650
|
+
startMonitor
|
|
4651
|
+
};
|