@node9/proxy 1.20.1 → 1.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1681 @@
1
+ // src/cli/render/ink/StaticScorecard.tsx
2
+ import { Box as Box11, render } from "ink";
3
+
4
+ // src/cli/render/ink/SeverityBand.tsx
5
+ import { Box, Text } from "ink";
6
+ import { jsx, jsxs } from "react/jsx-runtime";
7
+ function SeverityBand({ label, width }) {
8
+ const titleText = ` ${label} `;
9
+ const remaining = Math.max(2, width - titleText.length);
10
+ const leftDashes = "\u2501".repeat(Math.floor(remaining / 2));
11
+ const rightDashes = "\u2501".repeat(remaining - leftDashes.length);
12
+ return /* @__PURE__ */ jsxs(Box, { children: [
13
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: leftDashes }),
14
+ /* @__PURE__ */ jsx(Text, { bold: true, children: titleText }),
15
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: rightDashes })
16
+ ] });
17
+ }
18
+
19
+ // src/cli/render/ink/panels/Header.tsx
20
+ import { Box as Box2, Text as Text2 } from "ink";
21
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
22
+ function Header({ rangeLabel }) {
23
+ return /* @__PURE__ */ jsxs2(Box2, { children: [
24
+ /* @__PURE__ */ jsx2(Text2, { bold: true, children: "\u{1F6E1} node9 dashboard" }),
25
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " \xB7 " }),
26
+ /* @__PURE__ */ jsx2(Text2, { children: `scanned ${rangeLabel}` })
27
+ ] });
28
+ }
29
+
30
+ // src/cli/render/ink/panels/CostPanel.tsx
31
+ import { Box as Box3, Text as Text3 } from "ink";
32
+
33
+ // src/tui/dashboard/format.ts
34
+ function formatCost(usd) {
35
+ if (usd === 0) return "$0";
36
+ if (usd < 1) return `$${usd.toFixed(2)}`;
37
+ if (usd < 100) return `$${usd.toFixed(2)}`;
38
+ if (usd < 1e4) return `$${Math.round(usd).toLocaleString()}`;
39
+ return `$${(usd / 1e3).toFixed(1)}K`;
40
+ }
41
+
42
+ // src/cli/render/ink/panels/CostPanel.tsx
43
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
44
+ var LABEL_W = 16;
45
+ function CostPanel({ summary, width }) {
46
+ const total = summary.stats.totalCostUSD;
47
+ return /* @__PURE__ */ jsxs3(Box3, { borderStyle: "round", borderColor: "gray", paddingX: 1, flexDirection: "column", width, children: [
48
+ /* @__PURE__ */ jsx3(Text3, { bold: true, children: "COST" }),
49
+ /* @__PURE__ */ jsxs3(Box3, { children: [
50
+ /* @__PURE__ */ jsx3(Box3, { width: LABEL_W, children: /* @__PURE__ */ jsx3(Text3, { children: "Total" }) }),
51
+ /* @__PURE__ */ jsx3(Text3, { bold: true, children: formatCost(total) })
52
+ ] }),
53
+ summary.byAgent.map((agent) => /* @__PURE__ */ jsxs3(Box3, { children: [
54
+ /* @__PURE__ */ jsx3(Box3, { width: LABEL_W, children: /* @__PURE__ */ jsx3(Text3, { children: agent.label }) }),
55
+ /* @__PURE__ */ jsx3(Text3, { children: formatCost(agent.costUSD) })
56
+ ] }, agent.id)),
57
+ summary.loopWastedUSD > 0 ? /* @__PURE__ */ jsxs3(Box3, { children: [
58
+ /* @__PURE__ */ jsx3(Box3, { width: LABEL_W, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Wasted on loops" }) }),
59
+ /* @__PURE__ */ jsx3(Text3, { color: "yellow", children: "~" + formatCost(summary.loopWastedUSD) })
60
+ ] }) : null
61
+ ] });
62
+ }
63
+
64
+ // src/cli/render/ink/panels/ActivityPanel.tsx
65
+ import { Box as Box4, Text as Text4 } from "ink";
66
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
67
+ var LABEL_W2 = 16;
68
+ function fmtNum(n) {
69
+ return n.toLocaleString();
70
+ }
71
+ function countMcpFindings(scan) {
72
+ let n = 0;
73
+ for (const f of scan.findings) {
74
+ if (f.toolName.startsWith("mcp__")) n++;
75
+ }
76
+ return n;
77
+ }
78
+ function ActivityPanel({ summary, scan, width }) {
79
+ const { sessions, totalToolCalls, bashCalls, totalCostUSD } = summary.stats;
80
+ const perSession = sessions > 0 ? totalCostUSD / sessions : 0;
81
+ const toolsPerSession = sessions > 0 ? Math.round(totalToolCalls / sessions) : 0;
82
+ const mcpCount = countMcpFindings(scan);
83
+ const topAgent = summary.byAgent.slice().sort((a, b) => b.sessions - a.sessions)[0];
84
+ return /* @__PURE__ */ jsxs4(Box4, { borderStyle: "round", borderColor: "gray", paddingX: 1, flexDirection: "column", width, children: [
85
+ /* @__PURE__ */ jsx4(Text4, { bold: true, children: "ACTIVITY" }),
86
+ /* @__PURE__ */ jsxs4(Box4, { children: [
87
+ /* @__PURE__ */ jsx4(Box4, { width: LABEL_W2, children: /* @__PURE__ */ jsx4(Text4, { children: "Sessions" }) }),
88
+ /* @__PURE__ */ jsx4(Text4, { bold: true, children: fmtNum(sessions) })
89
+ ] }),
90
+ /* @__PURE__ */ jsxs4(Box4, { children: [
91
+ /* @__PURE__ */ jsx4(Box4, { width: LABEL_W2, children: /* @__PURE__ */ jsx4(Text4, { children: "Tools" }) }),
92
+ /* @__PURE__ */ jsx4(Text4, { children: fmtNum(totalToolCalls) })
93
+ ] }),
94
+ /* @__PURE__ */ jsxs4(Box4, { children: [
95
+ /* @__PURE__ */ jsx4(Box4, { width: LABEL_W2, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " Shell" }) }),
96
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: fmtNum(bashCalls) })
97
+ ] }),
98
+ mcpCount > 0 ? /* @__PURE__ */ jsxs4(Box4, { children: [
99
+ /* @__PURE__ */ jsx4(Box4, { width: LABEL_W2, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " MCP" }) }),
100
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: fmtNum(mcpCount) })
101
+ ] }) : null,
102
+ /* @__PURE__ */ jsxs4(Box4, { children: [
103
+ /* @__PURE__ */ jsx4(Box4, { width: LABEL_W2, children: /* @__PURE__ */ jsx4(Text4, { children: "Tools / session" }) }),
104
+ /* @__PURE__ */ jsx4(Text4, { children: "~" + fmtNum(toolsPerSession) })
105
+ ] }),
106
+ /* @__PURE__ */ jsxs4(Box4, { children: [
107
+ /* @__PURE__ */ jsx4(Box4, { width: LABEL_W2, children: /* @__PURE__ */ jsx4(Text4, { children: "Cost / session" }) }),
108
+ /* @__PURE__ */ jsx4(Text4, { children: "~$" + Math.round(perSession).toLocaleString() })
109
+ ] }),
110
+ topAgent ? /* @__PURE__ */ jsxs4(Box4, { children: [
111
+ /* @__PURE__ */ jsx4(Box4, { width: LABEL_W2, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Top agent" }) }),
112
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: `${topAgent.label} (${fmtNum(topAgent.sessions)})` })
113
+ ] }) : null
114
+ ] });
115
+ }
116
+
117
+ // src/cli/render/ink/panels/LeaksPanel.tsx
118
+ import { Box as Box5, Text as Text5 } from "ink";
119
+
120
+ // src/cli/render/scan-derive.ts
121
+ import chalk from "chalk";
122
+ import stringWidth from "string-width";
123
+ function topRulesByVerdict(sections, verdict, n) {
124
+ const matched = [];
125
+ for (const section of sections) {
126
+ for (const rule of section.rules) {
127
+ const matches = verdict === "block" ? rule.verdict === "block" : rule.verdict !== "block";
128
+ if (matches) matched.push({ name: rule.name, count: rule.findings.length });
129
+ }
130
+ }
131
+ return matched.sort((a, b) => b.count - a.count).slice(0, n);
132
+ }
133
+ function computeLoopWaste(loops, totalToolCalls) {
134
+ const wastedCalls = loops.reduce((s, l) => s + Math.max(0, l.count - 1), 0);
135
+ const wastePct = totalToolCalls > 0 ? Math.round(wastedCalls / totalToolCalls * 100) : 0;
136
+ return { wastedCalls, wastePct };
137
+ }
138
+ function rollupByShield(sections, topRulesPerShield = 3) {
139
+ const out = [];
140
+ for (const section of sections) {
141
+ if (section.sourceType !== "shield") continue;
142
+ if (!section.shieldKey) continue;
143
+ const totalCatches = section.blockedCount + section.reviewCount;
144
+ const topRuleLabels = [...section.rules].sort((a, b) => b.findings.length - a.findings.length).slice(0, topRulesPerShield).map((r) => r.findings.length > 1 ? `${r.name} \xD7${r.findings.length}` : r.name);
145
+ out.push({
146
+ shieldName: section.shieldKey,
147
+ totalCatches,
148
+ blockCatches: section.blockedCount,
149
+ reviewCatches: section.reviewCount,
150
+ topRuleLabels
151
+ });
152
+ }
153
+ return out.sort((a, b) => b.totalCatches - a.totalCatches);
154
+ }
155
+ function relativeDate(timestamp, now = /* @__PURE__ */ new Date()) {
156
+ const t = new Date(timestamp).getTime();
157
+ if (Number.isNaN(t)) return "?";
158
+ const days = Math.floor((now.getTime() - t) / 864e5);
159
+ if (days < 1) return "today";
160
+ if (days > 90) return "90d+";
161
+ return `${days}d`;
162
+ }
163
+
164
+ // src/cli/render/ink/panels/LeaksPanel.tsx
165
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
166
+ var ROW_LIMIT = 4;
167
+ function LeaksPanel({ summary, width }) {
168
+ const leaks = summary.leaks;
169
+ if (leaks.length === 0) return null;
170
+ const now = /* @__PURE__ */ new Date();
171
+ return /* @__PURE__ */ jsxs5(Box5, { borderStyle: "round", borderColor: "red", paddingX: 1, flexDirection: "column", width, children: [
172
+ /* @__PURE__ */ jsx5(Text5, { bold: true, color: "red", children: "CREDENTIAL LEAKS" }),
173
+ leaks.slice(0, ROW_LIMIT).map((leak, i) => /* @__PURE__ */ jsxs5(Box5, { children: [
174
+ /* @__PURE__ */ jsx5(Box5, { width: 5, children: /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: relativeDate(leak.timestamp, now).padStart(4) }) }),
175
+ /* @__PURE__ */ jsx5(Box5, { width: 16, children: /* @__PURE__ */ jsx5(Text5, { color: "red", bold: true, wrap: "truncate-end", children: leak.patternName }) }),
176
+ /* @__PURE__ */ jsx5(Box5, { width: 15, children: /* @__PURE__ */ jsx5(Text5, { dimColor: true, wrap: "truncate-end", children: `[${leak.toolName}]` }) }),
177
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, wrap: "truncate-end", children: leak.agent })
178
+ ] }, i)),
179
+ /* @__PURE__ */ jsxs5(Box5, { children: [
180
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "\u2192 " }),
181
+ /* @__PURE__ */ jsx5(Text5, { bold: true, color: "cyan", children: "DLP" }),
182
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " \xB7 " }),
183
+ /* @__PURE__ */ jsx5(Text5, { bold: true, color: "cyan", children: "node9 mask" }),
184
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, wrap: "truncate-end", children: " (runtime + cleanup)" })
185
+ ] })
186
+ ] });
187
+ }
188
+
189
+ // src/cli/render/ink/panels/BlockedPanel.tsx
190
+ import { Box as Box6, Text as Text6 } from "ink";
191
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
192
+ function originForRule(ruleName, sections) {
193
+ for (const section of sections) {
194
+ if (section.rules.some((r) => r.name === ruleName)) {
195
+ if (section.sourceType === "default") return "default";
196
+ if (section.sourceType === "shield") {
197
+ return `needs shield:${section.shieldKey ?? section.id}`;
198
+ }
199
+ }
200
+ }
201
+ return "";
202
+ }
203
+ var ROW_LIMIT2 = 12;
204
+ function BlockedPanel({ summary, width }) {
205
+ const rules = topRulesByVerdict(summary.sections, "block", ROW_LIMIT2);
206
+ if (rules.length === 0) return null;
207
+ return /* @__PURE__ */ jsxs6(Box6, { borderStyle: "round", borderColor: "red", paddingX: 1, flexDirection: "column", width, children: [
208
+ /* @__PURE__ */ jsx6(Text6, { bold: true, color: "red", children: "WOULD HAVE BLOCKED" }),
209
+ rules.map((rule, i) => /* @__PURE__ */ jsxs6(Box6, { children: [
210
+ /* @__PURE__ */ jsx6(Box6, { width: 3, children: /* @__PURE__ */ jsx6(Text6, { color: "red", children: "\u2717" }) }),
211
+ /* @__PURE__ */ jsx6(Box6, { width: 24, children: /* @__PURE__ */ jsx6(Text6, { bold: true, wrap: "truncate-end", children: rule.name }) }),
212
+ /* @__PURE__ */ jsx6(Box6, { width: 6, children: /* @__PURE__ */ jsx6(Text6, { bold: true, children: `\xD7${rule.count}` }) }),
213
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, wrap: "truncate-end", children: originForRule(rule.name, summary.sections) })
214
+ ] }, i)),
215
+ /* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, wrap: "truncate-end", children: "\u2192 install node9 + enable shields above" }) })
216
+ ] });
217
+ }
218
+
219
+ // src/cli/render/ink/panels/BlastRadiusPanel.tsx
220
+ import { Box as Box7, Text as Text7 } from "ink";
221
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
222
+ var ROW_LIMIT3 = 4;
223
+ function BlastRadiusPanel({
224
+ blast,
225
+ blastExposures,
226
+ width
227
+ }) {
228
+ if (blastExposures === 0) return null;
229
+ return /* @__PURE__ */ jsxs7(Box7, { borderStyle: "round", borderColor: "yellow", paddingX: 1, flexDirection: "column", width, children: [
230
+ /* @__PURE__ */ jsx7(Text7, { bold: true, color: "yellow", children: "BLAST RADIUS" }),
231
+ blast.reachable.slice(0, ROW_LIMIT3).map((path, i) => {
232
+ const desc = path.description.split(" \u2014 ")[0].split(/—|--/)[0].trim();
233
+ return /* @__PURE__ */ jsxs7(Box7, { children: [
234
+ /* @__PURE__ */ jsx7(Box7, { width: 3, children: /* @__PURE__ */ jsx7(Text7, { color: "red", children: "\u2717" }) }),
235
+ /* @__PURE__ */ jsx7(Box7, { width: 36, children: /* @__PURE__ */ jsx7(Text7, { children: path.label }) }),
236
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: desc })
237
+ ] }, i);
238
+ }),
239
+ blast.envFindings.slice(0, 3).map((env, i) => /* @__PURE__ */ jsxs7(Box7, { children: [
240
+ /* @__PURE__ */ jsx7(Box7, { width: 3, children: /* @__PURE__ */ jsx7(Text7, { color: "yellow", children: "\u26A0" }) }),
241
+ /* @__PURE__ */ jsx7(Text7, { children: env.key }),
242
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: ` (${env.patternName})` })
243
+ ] }, `env-${i}`)),
244
+ /* @__PURE__ */ jsxs7(Box7, { children: [
245
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "\u2192 " }),
246
+ /* @__PURE__ */ jsx7(Text7, { bold: true, color: "cyan", children: "project-jail" }),
247
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: " shield blocks agent reads of these paths" })
248
+ ] })
249
+ ] });
250
+ }
251
+
252
+ // src/cli/render/ink/panels/ReviewQueuePanel.tsx
253
+ import { Box as Box8, Text as Text8 } from "ink";
254
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
255
+ function originForRule2(ruleName, sections) {
256
+ for (const section of sections) {
257
+ if (section.rules.some((r) => r.name === ruleName)) {
258
+ if (section.sourceType === "default") return "default";
259
+ if (section.sourceType === "shield") {
260
+ return `needs shield:${section.shieldKey ?? section.id}`;
261
+ }
262
+ }
263
+ }
264
+ return "";
265
+ }
266
+ var ROW_LIMIT4 = 5;
267
+ function ReviewQueuePanel({ summary, width }) {
268
+ const rules = topRulesByVerdict(summary.sections, "review", ROW_LIMIT4);
269
+ if (rules.length === 0) return null;
270
+ return /* @__PURE__ */ jsxs8(Box8, { borderStyle: "round", borderColor: "gray", paddingX: 1, flexDirection: "column", width, children: [
271
+ /* @__PURE__ */ jsx8(Text8, { bold: true, children: "REVIEW QUEUE" }),
272
+ rules.map((rule, i) => /* @__PURE__ */ jsxs8(Box8, { children: [
273
+ /* @__PURE__ */ jsx8(Box8, { width: 22, children: /* @__PURE__ */ jsx8(Text8, { wrap: "truncate-end", children: rule.name }) }),
274
+ /* @__PURE__ */ jsx8(Box8, { width: 6, children: /* @__PURE__ */ jsx8(Text8, { bold: true, children: `\xD7${rule.count}` }) }),
275
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, wrap: "truncate-end", children: originForRule2(rule.name, summary.sections) })
276
+ ] }, i)),
277
+ /* @__PURE__ */ jsxs8(Box8, { children: [
278
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, wrap: "truncate-end", children: "\u2192 " }),
279
+ /* @__PURE__ */ jsx8(Text8, { bold: true, color: "cyan", wrap: "truncate-end", children: "runtime approval" })
280
+ ] })
281
+ ] });
282
+ }
283
+
284
+ // src/cli/render/ink/panels/AgentLoopsPanel.tsx
285
+ import { Box as Box9, Text as Text9 } from "ink";
286
+ import { Fragment, jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
287
+ var TOOL_ROW_LIMIT = 5;
288
+ var STUCK_ROW_LIMIT = 1;
289
+ function fmtNum2(n) {
290
+ return n.toLocaleString();
291
+ }
292
+ function trimRight(s, width) {
293
+ if (s.length <= width) return s;
294
+ return "\u2026" + s.slice(s.length - (width - 1));
295
+ }
296
+ function AgentLoopsPanel({ loopFindings, width }) {
297
+ if (loopFindings.length === 0) return null;
298
+ const byTool = /* @__PURE__ */ new Map();
299
+ let totalRepeats = 0;
300
+ for (const f of loopFindings) {
301
+ const repeats = Math.max(0, f.count - 1);
302
+ byTool.set(f.toolName, (byTool.get(f.toolName) ?? 0) + repeats);
303
+ totalRepeats += repeats;
304
+ }
305
+ const toolEntries = [...byTool.entries()].sort((a, b) => b[1] - a[1]).slice(0, TOOL_ROW_LIMIT);
306
+ const topStuck = [...loopFindings].sort((a, b) => b.count - a.count).slice(0, STUCK_ROW_LIMIT);
307
+ return /* @__PURE__ */ jsxs9(Box9, { borderStyle: "round", borderColor: "gray", paddingX: 1, flexDirection: "column", width, children: [
308
+ /* @__PURE__ */ jsx9(Text9, { bold: true, children: "AGENT LOOPS" }),
309
+ toolEntries.map(([tool, repeats]) => {
310
+ const pct = totalRepeats > 0 ? Math.round(repeats / totalRepeats * 100) : 0;
311
+ return /* @__PURE__ */ jsxs9(Box9, { children: [
312
+ /* @__PURE__ */ jsx9(Box9, { width: 10, children: /* @__PURE__ */ jsx9(Text9, { bold: true, children: tool }) }),
313
+ /* @__PURE__ */ jsx9(Box9, { width: 14, children: /* @__PURE__ */ jsx9(Text9, { children: `\xD7${fmtNum2(repeats)}` }) }),
314
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: `${pct}%` })
315
+ ] }, tool);
316
+ }),
317
+ topStuck.length > 0 ? /* @__PURE__ */ jsxs9(Fragment, { children: [
318
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Top stuck:" }),
319
+ topStuck.map((f, i) => {
320
+ const target = trimRight(f.commandPreview || f.toolName, 32);
321
+ return /* @__PURE__ */ jsxs9(Box9, { children: [
322
+ /* @__PURE__ */ jsx9(Box9, { width: 8, children: /* @__PURE__ */ jsx9(Text9, { bold: true, children: `\xD7${fmtNum2(f.count)}` }) }),
323
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, wrap: "truncate-end", children: target })
324
+ ] }, `stuck-${i}`);
325
+ })
326
+ ] }) : null,
327
+ /* @__PURE__ */ jsxs9(Box9, { children: [
328
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, wrap: "truncate-end", children: "\u2192 " }),
329
+ /* @__PURE__ */ jsx9(Text9, { bold: true, color: "cyan", wrap: "truncate-end", children: "live loop-detector" })
330
+ ] })
331
+ ] });
332
+ }
333
+
334
+ // src/cli/render/ink/panels/ShieldsPanel.tsx
335
+ import { Box as Box10, Text as Text10 } from "ink";
336
+
337
+ // src/protection.ts
338
+ var PROTECTIVE_SHIELD_DISCOUNTS = {
339
+ "project-jail": 0.7
340
+ };
341
+
342
+ // packages/policy-engine/dist/index.mjs
343
+ import safeRegex from "safe-regex2";
344
+ import mvdanSh from "mvdan-sh";
345
+ import pm from "picomatch";
346
+ import safeRegex2 from "safe-regex2";
347
+ import safeRegex3 from "safe-regex2";
348
+ var DLP_PATTERNS = [
349
+ // ── AWS ───────────────────────────────────────────────────────────────────
350
+ {
351
+ name: "AWS Access Key ID",
352
+ regex: /\b(?:A3T[A-Z0-9]|AKIA|ASIA|ABIA|ACCA)[A-Z2-7]{16}\b/,
353
+ severity: "block",
354
+ keywords: ["akia", "asia", "abia", "acca", "a3t"]
355
+ },
356
+ // ── GitHub ────────────────────────────────────────────────────────────────
357
+ {
358
+ name: "GitHub Token",
359
+ regex: /\bgh[pous]_[A-Za-z0-9]{36}\b/,
360
+ severity: "block",
361
+ keywords: ["ghp_", "gho_", "ghu_", "ghs_"],
362
+ minEntropy: 3
363
+ },
364
+ {
365
+ name: "GitHub Fine-Grained PAT",
366
+ regex: /\bgithub_pat_\w{82}\b/,
367
+ severity: "block",
368
+ keywords: ["github_pat_"]
369
+ },
370
+ // ── Slack ─────────────────────────────────────────────────────────────────
371
+ {
372
+ name: "Slack Bot Token",
373
+ // Real tokens are ~50–80 chars; lower bound 20 avoids false negatives on partial tokens
374
+ regex: /\bxoxb-[0-9A-Za-z-]{20,100}\b/,
375
+ severity: "block",
376
+ keywords: ["xoxb-"]
377
+ },
378
+ // ── Anthropic ─────────────────────────────────────────────────────────────
379
+ // Listed before OpenAI — Anthropic keys start with sk-ant- which would also
380
+ // match the broader OpenAI sk- pattern; more specific rules must come first.
381
+ {
382
+ name: "Anthropic API Key",
383
+ regex: /\bsk-ant-api03-[a-zA-Z0-9_-]{93}AA\b/,
384
+ severity: "block",
385
+ keywords: ["sk-ant-api03"]
386
+ },
387
+ {
388
+ name: "Anthropic Admin Key",
389
+ regex: /\bsk-ant-admin01-[a-zA-Z0-9_-]{93}AA\b/,
390
+ severity: "block",
391
+ keywords: ["sk-ant-admin01"]
392
+ },
393
+ // ── OpenAI ────────────────────────────────────────────────────────────────
394
+ {
395
+ name: "OpenAI API Key",
396
+ regex: /\bsk-[a-zA-Z0-9_-]{20,}\b/,
397
+ severity: "block",
398
+ keywords: ["sk-"],
399
+ minEntropy: 3.5
400
+ },
401
+ // ── Stripe ────────────────────────────────────────────────────────────────
402
+ {
403
+ name: "Stripe Secret Key",
404
+ regex: /\bsk_(?:live|test)_[0-9a-zA-Z]{24}\b/,
405
+ severity: "block",
406
+ keywords: ["sk_live_", "sk_test_"]
407
+ },
408
+ // ── GCP ───────────────────────────────────────────────────────────────────
409
+ {
410
+ name: "GCP API Key",
411
+ regex: /\bAIza[0-9A-Za-z_-]{35}\b/,
412
+ severity: "block",
413
+ keywords: ["aiza"],
414
+ minEntropy: 3
415
+ },
416
+ {
417
+ name: "GCP Service Account",
418
+ regex: /"type"\s*:\s*"service_account"/,
419
+ severity: "block",
420
+ keywords: ["service_account"]
421
+ },
422
+ // ── Azure ─────────────────────────────────────────────────────────────────
423
+ // Pattern: 3 alphanum chars + digit + Q~ + 31-34 alphanum chars
424
+ {
425
+ name: "Azure AD Client Secret",
426
+ regex: /(?:^|[\s>=:(,])([a-zA-Z0-9_~.]{3}\dQ~[a-zA-Z0-9_~.-]{31,34})(?:$|[\s<),])/,
427
+ severity: "block",
428
+ keywords: ["q~"]
429
+ },
430
+ // ── Databricks ────────────────────────────────────────────────────────────
431
+ {
432
+ name: "Databricks API Token",
433
+ regex: /\bdapi[a-f0-9]{32}(?:-\d)?\b/,
434
+ severity: "block",
435
+ keywords: ["dapi"]
436
+ },
437
+ // ── DigitalOcean ──────────────────────────────────────────────────────────
438
+ {
439
+ name: "DigitalOcean PAT",
440
+ regex: /\bdop_v1_[a-f0-9]{64}\b/,
441
+ severity: "block",
442
+ keywords: ["dop_v1_"]
443
+ },
444
+ {
445
+ name: "DigitalOcean Access Token",
446
+ regex: /\bdoo_v1_[a-f0-9]{64}\b/,
447
+ severity: "block",
448
+ keywords: ["doo_v1_"]
449
+ },
450
+ // ── Doppler ───────────────────────────────────────────────────────────────
451
+ {
452
+ name: "Doppler Token",
453
+ regex: /\bdp\.pt\.[a-z0-9]{43}\b/i,
454
+ severity: "block",
455
+ keywords: ["dp.pt."]
456
+ },
457
+ // ── HashiCorp Vault ───────────────────────────────────────────────────────
458
+ {
459
+ name: "HashiCorp Vault Service Token",
460
+ regex: /\bhvs\.[\w-]{90,120}\b/,
461
+ severity: "block",
462
+ keywords: ["hvs."]
463
+ },
464
+ {
465
+ name: "HashiCorp Vault Batch Token",
466
+ regex: /\bhvb\.[\w-]{138,300}\b/,
467
+ severity: "block",
468
+ keywords: ["hvb."]
469
+ },
470
+ // ── Hugging Face ──────────────────────────────────────────────────────────
471
+ {
472
+ name: "HuggingFace Token",
473
+ regex: /\bhf_[A-Za-z]{34}\b/,
474
+ severity: "block",
475
+ keywords: ["hf_"],
476
+ minEntropy: 3
477
+ },
478
+ // ── Postman ───────────────────────────────────────────────────────────────
479
+ {
480
+ name: "Postman API Token",
481
+ regex: /\bPMAK-[a-f0-9]{24}-[a-f0-9]{34}\b/i,
482
+ severity: "block",
483
+ keywords: ["pmak-"]
484
+ },
485
+ // ── Pulumi ────────────────────────────────────────────────────────────────
486
+ {
487
+ name: "Pulumi Access Token",
488
+ regex: /\bpul-[a-f0-9]{40}\b/,
489
+ severity: "block",
490
+ keywords: ["pul-"]
491
+ },
492
+ // ── SendGrid ──────────────────────────────────────────────────────────────
493
+ {
494
+ name: "SendGrid API Key",
495
+ regex: /\bSG\.[a-zA-Z0-9=_.-]{66}\b/,
496
+ severity: "block",
497
+ keywords: ["sg."]
498
+ },
499
+ // ── Private keys (PEM) ────────────────────────────────────────────────────
500
+ {
501
+ name: "Private Key (PEM)",
502
+ regex: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
503
+ severity: "block",
504
+ keywords: ["-----begin"]
505
+ },
506
+ // ── NPM ───────────────────────────────────────────────────────────────────
507
+ {
508
+ name: "NPM Auth Token",
509
+ regex: /_authToken\s*=\s*[A-Za-z0-9_-]{20,}/,
510
+ severity: "block",
511
+ keywords: ["_authtoken"]
512
+ },
513
+ // ── JWT ───────────────────────────────────────────────────────────────────
514
+ // review (not block): JWTs appear legitimately in API calls; flag for human approval
515
+ // contextBoost: promoted to block when assigned (e.g. TOKEN=eyJ...)
516
+ {
517
+ name: "JWT",
518
+ regex: /\bey[a-zA-Z0-9]{17,}\.ey[a-zA-Z0-9\/_-]{17,}\.[a-zA-Z0-9\/_-]{10,}={0,2}\b/,
519
+ severity: "review",
520
+ keywords: ["eyj"],
521
+ contextBoost: true
522
+ },
523
+ // ── Stripe (extended — adds restricted key rk_ prefix) ──────────────────
524
+ {
525
+ name: "Stripe Restricted Key",
526
+ regex: /\brk_(?:live|test|prod)_[0-9a-zA-Z]{10,99}\b/,
527
+ severity: "block",
528
+ keywords: ["rk_live_", "rk_test_", "rk_prod_"]
529
+ },
530
+ // ── Slack (app token) ─────────────────────────────────────────────────────
531
+ {
532
+ name: "Slack App Token",
533
+ regex: /\bxapp-\d-[A-Z0-9]+-\d+-[a-f0-9]+\b/,
534
+ severity: "block",
535
+ keywords: ["xapp-"]
536
+ },
537
+ // ── GitLab ────────────────────────────────────────────────────────────────
538
+ { name: "GitLab PAT", regex: /\bglpat-[\w-]{20}\b/, severity: "block", keywords: ["glpat-"] },
539
+ {
540
+ name: "GitLab Deploy Token",
541
+ regex: /\bgldt-[0-9a-zA-Z_-]{20}\b/,
542
+ severity: "block",
543
+ keywords: ["gldt-"]
544
+ },
545
+ {
546
+ name: "GitLab CI Job Token",
547
+ regex: /\bglcbt-[0-9a-zA-Z]{1,5}_[0-9a-zA-Z_-]{20}\b/,
548
+ severity: "block",
549
+ keywords: ["glcbt-"]
550
+ },
551
+ // ── npm (publish token) ───────────────────────────────────────────────────
552
+ {
553
+ name: "npm Access Token",
554
+ regex: /\bnpm_[a-zA-Z0-9]{36}\b/,
555
+ severity: "block",
556
+ keywords: ["npm_"]
557
+ },
558
+ // ── Shopify ───────────────────────────────────────────────────────────────
559
+ {
560
+ name: "Shopify Access Token",
561
+ regex: /\bshpat_[a-fA-F0-9]{32}\b/,
562
+ severity: "block",
563
+ keywords: ["shpat_"]
564
+ },
565
+ {
566
+ name: "Shopify Custom Access Token",
567
+ regex: /\bshpca_[a-fA-F0-9]{32}\b/,
568
+ severity: "block",
569
+ keywords: ["shpca_"]
570
+ },
571
+ {
572
+ name: "Shopify Private App Token",
573
+ regex: /\bshppa_[a-fA-F0-9]{32}\b/,
574
+ severity: "block",
575
+ keywords: ["shppa_"]
576
+ },
577
+ {
578
+ name: "Shopify Shared Secret",
579
+ regex: /\bshpss_[a-fA-F0-9]{32}\b/,
580
+ severity: "block",
581
+ keywords: ["shpss_"]
582
+ },
583
+ // ── Linear ────────────────────────────────────────────────────────────────
584
+ {
585
+ name: "Linear API Key",
586
+ regex: /\blin_api_[a-zA-Z0-9]{40}\b/,
587
+ severity: "block",
588
+ keywords: ["lin_api_"]
589
+ },
590
+ // ── PlanetScale ───────────────────────────────────────────────────────────
591
+ {
592
+ name: "PlanetScale API Token",
593
+ regex: /\bpscale_tkn_[\w.-]{32,64}\b/,
594
+ severity: "block",
595
+ keywords: ["pscale_tkn_"]
596
+ },
597
+ {
598
+ name: "PlanetScale Password",
599
+ regex: /\bpscale_pw_[\w.-]{32,64}\b/,
600
+ severity: "block",
601
+ keywords: ["pscale_pw_"]
602
+ },
603
+ // ── Sentry ────────────────────────────────────────────────────────────────
604
+ {
605
+ name: "Sentry User Token",
606
+ regex: /\bsntryu_[a-f0-9]{64}\b/,
607
+ severity: "block",
608
+ keywords: ["sntryu_"]
609
+ },
610
+ // ── Grafana ───────────────────────────────────────────────────────────────
611
+ {
612
+ name: "Grafana Service Account Token",
613
+ regex: /\bglsa_[a-zA-Z0-9]{32}_[a-f0-9]{8}\b/,
614
+ severity: "block",
615
+ keywords: ["glsa_"]
616
+ },
617
+ // ── Heroku ────────────────────────────────────────────────────────────────
618
+ {
619
+ name: "Heroku API Key",
620
+ regex: /\bHRKU-AA[0-9a-zA-Z_-]{58}\b/,
621
+ severity: "block",
622
+ keywords: ["hrku-aa"]
623
+ },
624
+ // ── PyPI ──────────────────────────────────────────────────────────────────
625
+ {
626
+ name: "PyPI Upload Token",
627
+ regex: /\bpypi-[A-Za-z0-9_-]{50,}\b/,
628
+ severity: "block",
629
+ keywords: ["pypi-"],
630
+ minEntropy: 3
631
+ },
632
+ // ── Bearer Token ─────────────────────────────────────────────────────────
633
+ // contextBoost: promoted to block when assigned (e.g. AUTH_TOKEN=Bearer eyJ...)
634
+ {
635
+ name: "Bearer Token",
636
+ regex: /Bearer\s+[a-zA-Z0-9\-._~+/]{20,}=*/i,
637
+ severity: "review",
638
+ keywords: ["bearer"],
639
+ contextBoost: true,
640
+ minEntropy: 3
641
+ },
642
+ // ── Resend ────────────────────────────────────────────────────────────────
643
+ {
644
+ name: "Resend API Key",
645
+ regex: /\bre_[a-zA-Z0-9]{24}\b/,
646
+ severity: "block",
647
+ keywords: ["re_"]
648
+ },
649
+ // ── Telegram ──────────────────────────────────────────────────────────────
650
+ {
651
+ name: "Telegram Bot Token",
652
+ regex: /\b[0-9]{7,10}:AA[a-zA-Z0-9_-]{33}\b/,
653
+ severity: "block",
654
+ keywords: [":aa"]
655
+ },
656
+ // ── Mapbox ────────────────────────────────────────────────────────────────
657
+ {
658
+ name: "Mapbox Access Token",
659
+ regex: /\bpk\.eyJ1[a-zA-Z0-9._-]{20,}\b/,
660
+ severity: "block",
661
+ keywords: ["pk.eyj1"],
662
+ minEntropy: 3
663
+ },
664
+ // ── Notion ────────────────────────────────────────────────────────────────
665
+ {
666
+ name: "Notion Integration Token",
667
+ regex: /\bsecret_[a-zA-Z0-9]{43}\b/,
668
+ severity: "block",
669
+ keywords: ["secret_"]
670
+ },
671
+ // ── Square ────────────────────────────────────────────────────────────────
672
+ {
673
+ name: "Square Access Token",
674
+ regex: /\bsq0atp-[0-9A-Za-z_-]{22}\b/,
675
+ severity: "block",
676
+ keywords: ["sq0atp-"]
677
+ },
678
+ {
679
+ name: "Square OAuth Secret",
680
+ regex: /\bsq0csp-[0-9A-Za-z_-]{43}\b/,
681
+ severity: "block",
682
+ keywords: ["sq0csp-"]
683
+ },
684
+ // ── Typeform ──────────────────────────────────────────────────────────────
685
+ {
686
+ name: "Typeform Token",
687
+ regex: /\btfp_[a-zA-Z0-9_]{59}\b/,
688
+ severity: "block",
689
+ keywords: ["tfp_"]
690
+ },
691
+ // ── Cloudinary ────────────────────────────────────────────────────────────
692
+ {
693
+ name: "Cloudinary URL",
694
+ regex: /\bcloudinary:\/\/[0-9]+:[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+/,
695
+ severity: "block",
696
+ keywords: ["cloudinary://"]
697
+ },
698
+ // ── Airtable ──────────────────────────────────────────────────────────────
699
+ // New PAT format: pat + 14 alphanum + . + 64 alphanum
700
+ {
701
+ name: "Airtable PAT",
702
+ regex: /\bpat[a-zA-Z0-9]{14}\.[a-zA-Z0-9]{64}\b/,
703
+ severity: "block",
704
+ keywords: ["pat"]
705
+ },
706
+ // ── RubyGems ──────────────────────────────────────────────────────────────
707
+ {
708
+ name: "RubyGems API Key",
709
+ regex: /\brubygems_[a-f0-9]{48}\b/,
710
+ severity: "block",
711
+ keywords: ["rubygems_"]
712
+ },
713
+ // ── Shippo ────────────────────────────────────────────────────────────────
714
+ {
715
+ name: "Shippo Token",
716
+ regex: /\bshippo_(?:live|test)_[a-f0-9]{40}\b/,
717
+ severity: "block",
718
+ keywords: ["shippo_"]
719
+ },
720
+ // ── Plaid ─────────────────────────────────────────────────────────────────
721
+ {
722
+ name: "Plaid Access Token",
723
+ 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/,
724
+ severity: "block",
725
+ keywords: ["access-sandbox", "access-development", "access-production"]
726
+ },
727
+ // ── Age ───────────────────────────────────────────────────────────────────
728
+ {
729
+ name: "Age Identity Key",
730
+ regex: /\bAGE-SECRET-KEY-1[QPZRY9X8GF2TVDW0S3JNLH]{58}\b/,
731
+ severity: "block",
732
+ keywords: ["age-secret-key-"]
733
+ }
734
+ ];
735
+ var DLP_PATTERNS_GLOBAL = DLP_PATTERNS.map(
736
+ (p) => ({
737
+ pattern: p,
738
+ globalRegex: new RegExp(
739
+ p.regex.source,
740
+ p.regex.flags.includes("g") ? p.regex.flags : p.regex.flags + "g"
741
+ )
742
+ })
743
+ );
744
+ var SENSITIVE_PATH_PATTERNS = [
745
+ /[/\\]\.ssh[/\\]/i,
746
+ /[/\\]\.aws[/\\]/i,
747
+ /[/\\]\.config[/\\]gcloud[/\\]/i,
748
+ /[/\\]\.azure[/\\]/i,
749
+ /[/\\]\.kube[/\\]config$/i,
750
+ /[/\\]\.env($|\.)/i,
751
+ // .env, .env.local, .env.production — not .envoy
752
+ /[/\\]\.git-credentials$/i,
753
+ /[/\\]\.npmrc$/i,
754
+ /[/\\]\.docker[/\\]config\.json$/i,
755
+ /[/\\][^/\\]+\.pem$/i,
756
+ /[/\\][^/\\]+\.key$/i,
757
+ /[/\\][^/\\]+\.p12$/i,
758
+ /[/\\][^/\\]+\.pfx$/i,
759
+ /^(?:[a-zA-Z]:)?\/etc\/passwd$/,
760
+ /^(?:[a-zA-Z]:)?\/etc\/shadow$/,
761
+ /^(?:[a-zA-Z]:)?\/etc\/sudoers$/,
762
+ /[/\\]credentials\.json$/i,
763
+ /[/\\]id_rsa$/i,
764
+ /[/\\]id_ed25519$/i,
765
+ /[/\\]id_ecdsa$/i
766
+ ];
767
+ function assertBuiltinPatternsAreSafe() {
768
+ for (const p of DLP_PATTERNS) {
769
+ if (!safeRegex(p.regex.source)) {
770
+ throw new Error(
771
+ `[node9 engine] Builtin DLP pattern '${p.name}' is vulnerable to ReDoS: ${p.regex.source}`
772
+ );
773
+ }
774
+ }
775
+ for (const re of SENSITIVE_PATH_PATTERNS) {
776
+ if (!safeRegex(re.source)) {
777
+ throw new Error(
778
+ `[node9 engine] Builtin sensitive-path pattern is vulnerable to ReDoS: ${re.source}`
779
+ );
780
+ }
781
+ }
782
+ }
783
+ assertBuiltinPatternsAreSafe();
784
+ var { syntax } = mvdanSh;
785
+ var sharedParser = syntax.NewParser();
786
+ var aws_default = {
787
+ name: "aws",
788
+ description: "Protects AWS infrastructure from destructive AI operations",
789
+ aliases: ["amazon"],
790
+ smartRules: [
791
+ {
792
+ name: "shield:aws:block-delete-s3-bucket",
793
+ tool: "*",
794
+ conditions: [
795
+ {
796
+ field: "command",
797
+ op: "matches",
798
+ value: "aws\\s+s3.*rb\\s|aws\\s+s3api\\s+delete-bucket",
799
+ flags: "i"
800
+ }
801
+ ],
802
+ verdict: "block",
803
+ reason: "S3 bucket deletion is irreversible \u2014 blocked by AWS shield"
804
+ },
805
+ {
806
+ name: "shield:aws:review-iam-changes",
807
+ tool: "*",
808
+ conditions: [
809
+ {
810
+ field: "command",
811
+ op: "matches",
812
+ value: "aws\\s+iam\\s+(create|delete|attach|detach|put|remove)",
813
+ flags: "i"
814
+ }
815
+ ],
816
+ verdict: "review",
817
+ reason: "IAM changes require human approval (AWS shield)"
818
+ },
819
+ {
820
+ name: "shield:aws:block-ec2-terminate",
821
+ tool: "*",
822
+ conditions: [
823
+ {
824
+ field: "command",
825
+ op: "matches",
826
+ value: "aws\\s+ec2\\s+terminate-instances",
827
+ flags: "i"
828
+ }
829
+ ],
830
+ verdict: "block",
831
+ reason: "EC2 instance termination is irreversible \u2014 blocked by AWS shield"
832
+ },
833
+ {
834
+ name: "shield:aws:review-rds-delete",
835
+ tool: "*",
836
+ conditions: [
837
+ { field: "command", op: "matches", value: "aws\\s+rds\\s+delete-", flags: "i" }
838
+ ],
839
+ verdict: "review",
840
+ reason: "RDS deletion requires human approval (AWS shield)"
841
+ }
842
+ ],
843
+ dangerousWords: []
844
+ };
845
+ var bash_safe_default = {
846
+ name: "bash-safe",
847
+ description: "Blocks high-risk bash patterns: pipe-to-shell, rm -rf /, disk overwrites, eval",
848
+ aliases: ["bash", "shell"],
849
+ smartRules: [
850
+ {
851
+ name: "shield:bash-safe:block-pipe-to-shell",
852
+ tool: "bash",
853
+ conditions: [
854
+ {
855
+ field: "command",
856
+ op: "matches",
857
+ value: "(^|&&|\\|\\||;)\\s*(curl|wget)\\s+[^|]*\\|\\s*(?:(bash|sh|zsh|fish)|(python3?|ruby|perl|node)\\b(?!\\s+-[cem]\\b))",
858
+ flags: "i"
859
+ }
860
+ ],
861
+ verdict: "block",
862
+ reason: "Pipe-to-shell is a common supply-chain attack vector \u2014 blocked by bash-safe shield"
863
+ },
864
+ {
865
+ name: "shield:bash-safe:block-obfuscated-exec",
866
+ tool: "bash",
867
+ conditions: [
868
+ {
869
+ field: "command",
870
+ op: "matches",
871
+ value: "\\bbase64\\s+(-d|--decode)[^|;&]*\\|\\s*(bash|sh|zsh)",
872
+ flags: "i"
873
+ }
874
+ ],
875
+ verdict: "block",
876
+ reason: "Obfuscated execution via base64 decode \u2014 blocked by bash-safe shield"
877
+ },
878
+ {
879
+ name: "shield:bash-safe:block-rm-root",
880
+ tool: "bash",
881
+ conditions: [
882
+ {
883
+ field: "command",
884
+ op: "matches",
885
+ 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*$",
886
+ flags: "i"
887
+ }
888
+ ],
889
+ verdict: "block",
890
+ reason: "rm -rf of root or home directory is catastrophic \u2014 blocked by bash-safe shield"
891
+ },
892
+ {
893
+ name: "shield:bash-safe:block-disk-overwrite",
894
+ tool: "bash",
895
+ conditions: [
896
+ {
897
+ field: "command",
898
+ op: "matches",
899
+ value: "(^|&&|\\|\\||;)\\s*dd\\s+.*of=\\/dev\\/(sd|nvme|hd|vd|xvd)",
900
+ flags: "i"
901
+ }
902
+ ],
903
+ verdict: "block",
904
+ reason: "Writing directly to a block device is irreversible \u2014 blocked by bash-safe shield"
905
+ },
906
+ {
907
+ name: "shield:bash-safe:block-eval-remote",
908
+ tool: "bash",
909
+ conditions: [
910
+ {
911
+ field: "command",
912
+ op: "matches",
913
+ value: "(^|&&|\\|\\||;)\\s*eval\\s+.*\\$\\((curl|wget)\\b",
914
+ flags: "i"
915
+ }
916
+ ],
917
+ verdict: "block",
918
+ reason: "eval of remote download is a near-certain supply-chain attack \u2014 blocked by bash-safe shield"
919
+ },
920
+ {
921
+ name: "shield:bash-safe:review-eval-dynamic",
922
+ tool: "bash",
923
+ conditions: [
924
+ {
925
+ field: "command",
926
+ op: "matches",
927
+ value: '(^|&&|\\|\\||[;|\\n{(`])\\s*eval\\s+([\\$`(]|"[^"]*\\$)',
928
+ flags: "i"
929
+ }
930
+ ],
931
+ verdict: "review",
932
+ reason: "eval of dynamic content \u2014 backup regex rule for scan path (real-time uses AST detection)"
933
+ }
934
+ ],
935
+ dangerousWords: []
936
+ };
937
+ var docker_default = {
938
+ name: "docker",
939
+ description: "Protects Docker environments from destructive AI operations",
940
+ aliases: [],
941
+ smartRules: [
942
+ {
943
+ name: "shield:docker:block-system-prune",
944
+ tool: "*",
945
+ conditions: [
946
+ {
947
+ field: "command",
948
+ op: "matches",
949
+ value: "docker\\s+system\\s+prune",
950
+ flags: "i"
951
+ }
952
+ ],
953
+ verdict: "block",
954
+ reason: "docker system prune removes all unused containers, images, and volumes \u2014 blocked by Docker shield"
955
+ },
956
+ {
957
+ name: "shield:docker:block-volume-prune",
958
+ tool: "*",
959
+ conditions: [
960
+ {
961
+ field: "command",
962
+ op: "matches",
963
+ value: "docker\\s+volume\\s+prune",
964
+ flags: "i"
965
+ }
966
+ ],
967
+ verdict: "block",
968
+ reason: "docker volume prune destroys all unused volumes and their data \u2014 blocked by Docker shield"
969
+ },
970
+ {
971
+ name: "shield:docker:block-rm-force",
972
+ tool: "*",
973
+ conditionMode: "all",
974
+ conditions: [
975
+ {
976
+ field: "command",
977
+ op: "matches",
978
+ value: "docker\\s+rm\\b",
979
+ flags: "i"
980
+ },
981
+ {
982
+ field: "command",
983
+ op: "matches",
984
+ value: "(^|\\s)(-f|--force)(\\s|$)",
985
+ flags: "i"
986
+ }
987
+ ],
988
+ verdict: "block",
989
+ reason: "Force-removing running containers is destructive \u2014 blocked by Docker shield"
990
+ },
991
+ {
992
+ name: "shield:docker:review-volume-rm",
993
+ tool: "*",
994
+ conditions: [
995
+ {
996
+ field: "command",
997
+ op: "matches",
998
+ value: "docker\\s+volume\\s+rm\\s+",
999
+ flags: "i"
1000
+ }
1001
+ ],
1002
+ verdict: "review",
1003
+ reason: "Volume removal deletes persistent data and requires human approval (Docker shield)"
1004
+ },
1005
+ {
1006
+ name: "shield:docker:review-stop-kill",
1007
+ tool: "*",
1008
+ conditions: [
1009
+ {
1010
+ field: "command",
1011
+ op: "matches",
1012
+ value: "docker\\s+(stop|kill)\\s+",
1013
+ flags: "i"
1014
+ }
1015
+ ],
1016
+ verdict: "review",
1017
+ reason: "Stopping or killing containers requires human approval (Docker shield)"
1018
+ },
1019
+ {
1020
+ name: "shield:docker:review-image-rm",
1021
+ tool: "*",
1022
+ conditions: [
1023
+ {
1024
+ field: "command",
1025
+ op: "matches",
1026
+ value: "docker\\s+image\\s+rm\\b",
1027
+ flags: "i"
1028
+ }
1029
+ ],
1030
+ verdict: "review",
1031
+ reason: "Image removal requires human approval (Docker shield)"
1032
+ },
1033
+ {
1034
+ name: "shield:docker:review-rmi-force",
1035
+ tool: "*",
1036
+ conditionMode: "all",
1037
+ conditions: [
1038
+ {
1039
+ field: "command",
1040
+ op: "matches",
1041
+ value: "docker\\s+rmi\\b",
1042
+ flags: "i"
1043
+ },
1044
+ {
1045
+ field: "command",
1046
+ op: "matches",
1047
+ value: "(^|\\s)(-f|--force)(\\s|$)",
1048
+ flags: "i"
1049
+ }
1050
+ ],
1051
+ verdict: "review",
1052
+ reason: "Force image removal requires human approval (Docker shield)"
1053
+ }
1054
+ ],
1055
+ dangerousWords: []
1056
+ };
1057
+ var filesystem_default = {
1058
+ name: "filesystem",
1059
+ description: "Protects the local filesystem from dangerous AI operations",
1060
+ aliases: ["fs"],
1061
+ smartRules: [
1062
+ {
1063
+ name: "shield:filesystem:review-chmod-777",
1064
+ tool: "bash",
1065
+ conditions: [
1066
+ { field: "command", op: "matches", value: "chmod\\s+(777|a\\+rwx)", flags: "i" }
1067
+ ],
1068
+ verdict: "review",
1069
+ reason: "chmod 777 requires human approval (filesystem shield)"
1070
+ },
1071
+ {
1072
+ name: "shield:filesystem:review-write-etc",
1073
+ tool: "bash",
1074
+ conditions: [
1075
+ {
1076
+ field: "command",
1077
+ op: "matches",
1078
+ value: "(tee|\\bcp\\b|\\bmv\\b|install|>+)\\s+.*\\/etc\\/"
1079
+ }
1080
+ ],
1081
+ verdict: "review",
1082
+ reason: "Writing to /etc requires human approval (filesystem shield)"
1083
+ }
1084
+ ],
1085
+ dangerousWords: ["wipefs"]
1086
+ };
1087
+ var github_default = {
1088
+ name: "github",
1089
+ description: "Protects GitHub repositories from destructive AI operations",
1090
+ aliases: ["git"],
1091
+ smartRules: [
1092
+ {
1093
+ name: "shield:github:review-delete-branch-remote",
1094
+ tool: "bash",
1095
+ conditions: [
1096
+ { field: "command", op: "matches", value: "git\\s+push\\s+.*--delete", flags: "i" }
1097
+ ],
1098
+ verdict: "review",
1099
+ reason: "Remote branch deletion requires human approval (GitHub shield)"
1100
+ },
1101
+ {
1102
+ name: "shield:github:block-delete-repo",
1103
+ tool: "*",
1104
+ conditions: [
1105
+ { field: "command", op: "matches", value: "gh\\s+repo\\s+delete", flags: "i" }
1106
+ ],
1107
+ verdict: "block",
1108
+ reason: "Repository deletion is irreversible \u2014 blocked by GitHub shield"
1109
+ }
1110
+ ],
1111
+ dangerousWords: []
1112
+ };
1113
+ var k8s_default = {
1114
+ name: "k8s",
1115
+ description: "Protects Kubernetes clusters from destructive AI operations",
1116
+ aliases: ["kubernetes", "kubectl"],
1117
+ smartRules: [
1118
+ {
1119
+ name: "shield:k8s:block-delete-namespace",
1120
+ tool: "*",
1121
+ conditions: [
1122
+ {
1123
+ field: "command",
1124
+ op: "matches",
1125
+ value: "kubectl\\s+delete\\s+(ns|namespace)\\s+",
1126
+ flags: "i"
1127
+ }
1128
+ ],
1129
+ verdict: "block",
1130
+ reason: "Deleting a namespace destroys all resources inside it \u2014 blocked by k8s shield"
1131
+ },
1132
+ {
1133
+ name: "shield:k8s:block-delete-all",
1134
+ tool: "*",
1135
+ conditions: [
1136
+ {
1137
+ field: "command",
1138
+ op: "matches",
1139
+ value: "kubectl\\s+delete\\s+.*--all\\b",
1140
+ flags: "i"
1141
+ }
1142
+ ],
1143
+ verdict: "block",
1144
+ reason: "kubectl delete --all is irreversible \u2014 blocked by k8s shield"
1145
+ },
1146
+ {
1147
+ name: "shield:k8s:block-helm-uninstall",
1148
+ tool: "*",
1149
+ conditions: [
1150
+ {
1151
+ field: "command",
1152
+ op: "matches",
1153
+ value: "helm\\s+(uninstall|delete|del)\\s+",
1154
+ flags: "i"
1155
+ }
1156
+ ],
1157
+ verdict: "block",
1158
+ reason: "helm uninstall removes a release and its resources \u2014 blocked by k8s shield"
1159
+ },
1160
+ {
1161
+ name: "shield:k8s:review-scale-zero",
1162
+ tool: "*",
1163
+ conditions: [
1164
+ {
1165
+ field: "command",
1166
+ op: "matches",
1167
+ value: "kubectl\\s+scale\\s+.*--replicas=0",
1168
+ flags: "i"
1169
+ }
1170
+ ],
1171
+ verdict: "review",
1172
+ reason: "Scaling to zero takes down a workload and requires human approval (k8s shield)"
1173
+ },
1174
+ {
1175
+ name: "shield:k8s:review-delete-deployment",
1176
+ tool: "*",
1177
+ conditions: [
1178
+ {
1179
+ field: "command",
1180
+ op: "matches",
1181
+ value: "kubectl\\s+delete\\s+(deployment|deploy|statefulset|sts|daemonset|ds)\\s+",
1182
+ flags: "i"
1183
+ }
1184
+ ],
1185
+ verdict: "review",
1186
+ reason: "Deleting a workload requires human approval (k8s shield)"
1187
+ },
1188
+ {
1189
+ name: "shield:k8s:review-apply-force",
1190
+ tool: "*",
1191
+ conditions: [
1192
+ {
1193
+ field: "command",
1194
+ op: "matches",
1195
+ value: "kubectl\\s+(apply|replace)\\s+.*--force",
1196
+ flags: "i"
1197
+ }
1198
+ ],
1199
+ verdict: "review",
1200
+ reason: "Force-apply overwrites live resources and requires human approval (k8s shield)"
1201
+ }
1202
+ ],
1203
+ dangerousWords: []
1204
+ };
1205
+ var mongodb_default = {
1206
+ name: "mongodb",
1207
+ description: "Protects MongoDB databases from destructive AI operations",
1208
+ aliases: ["mongo"],
1209
+ smartRules: [
1210
+ {
1211
+ name: "shield:mongodb:block-drop-database",
1212
+ tool: "*",
1213
+ conditions: [
1214
+ {
1215
+ field: "command",
1216
+ op: "matches",
1217
+ value: "\\.dropDatabase\\s*\\(",
1218
+ flags: "i"
1219
+ }
1220
+ ],
1221
+ verdict: "block",
1222
+ reason: "dropDatabase is irreversible \u2014 blocked by MongoDB shield"
1223
+ },
1224
+ {
1225
+ name: "shield:mongodb:block-drop-collection",
1226
+ tool: "*",
1227
+ conditions: [
1228
+ {
1229
+ field: "command",
1230
+ op: "matches",
1231
+ value: "\\.drop\\s*\\(|db\\.getCollection\\([^)]+\\)\\.drop\\s*\\(",
1232
+ flags: "i"
1233
+ }
1234
+ ],
1235
+ verdict: "block",
1236
+ reason: "Collection drop is irreversible \u2014 blocked by MongoDB shield"
1237
+ },
1238
+ {
1239
+ name: "shield:mongodb:block-delete-many-empty-filter",
1240
+ tool: "*",
1241
+ conditions: [
1242
+ {
1243
+ field: "command",
1244
+ op: "matches",
1245
+ value: "\\.deleteMany\\s*\\(\\s*\\{\\s*\\}\\s*\\)",
1246
+ flags: "i"
1247
+ }
1248
+ ],
1249
+ verdict: "block",
1250
+ reason: "deleteMany({}) with empty filter wipes the entire collection \u2014 blocked by MongoDB shield"
1251
+ },
1252
+ {
1253
+ name: "shield:mongodb:review-delete-many",
1254
+ tool: "*",
1255
+ conditions: [
1256
+ {
1257
+ field: "command",
1258
+ op: "matches",
1259
+ value: "\\.deleteMany\\s*\\(",
1260
+ flags: "i"
1261
+ }
1262
+ ],
1263
+ verdict: "review",
1264
+ reason: "deleteMany requires human approval (MongoDB shield)"
1265
+ },
1266
+ {
1267
+ name: "shield:mongodb:review-drop-index",
1268
+ tool: "*",
1269
+ conditions: [
1270
+ {
1271
+ field: "command",
1272
+ op: "matches",
1273
+ value: "\\.dropIndex\\s*\\(|\\.dropIndexes\\s*\\(",
1274
+ flags: "i"
1275
+ }
1276
+ ],
1277
+ verdict: "review",
1278
+ reason: "Index drops affect query performance and require human approval (MongoDB shield)"
1279
+ }
1280
+ ],
1281
+ dangerousWords: ["dropDatabase", "dropCollection", "mongodrop"]
1282
+ };
1283
+ var postgres_default = {
1284
+ name: "postgres",
1285
+ description: "Protects PostgreSQL databases from destructive AI operations",
1286
+ aliases: ["pg", "postgresql"],
1287
+ smartRules: [
1288
+ {
1289
+ name: "shield:postgres:block-drop-table",
1290
+ tool: "*",
1291
+ conditions: [{ field: "sql", op: "matches", value: "DROP\\s+TABLE", flags: "i" }],
1292
+ verdict: "block",
1293
+ reason: "DROP TABLE is irreversible \u2014 blocked by Postgres shield"
1294
+ },
1295
+ {
1296
+ name: "shield:postgres:block-truncate",
1297
+ tool: "*",
1298
+ conditions: [
1299
+ { field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }
1300
+ ],
1301
+ verdict: "block",
1302
+ reason: "TRUNCATE is irreversible \u2014 blocked by Postgres shield"
1303
+ },
1304
+ {
1305
+ name: "shield:postgres:block-drop-column",
1306
+ tool: "*",
1307
+ conditions: [
1308
+ { field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
1309
+ ],
1310
+ verdict: "block",
1311
+ reason: "DROP COLUMN is irreversible \u2014 blocked by Postgres shield"
1312
+ },
1313
+ {
1314
+ name: "shield:postgres:review-grant-revoke",
1315
+ tool: "*",
1316
+ conditions: [
1317
+ { field: "sql", op: "matches", value: "\\b(GRANT|REVOKE)\\b", flags: "i" }
1318
+ ],
1319
+ verdict: "review",
1320
+ reason: "Permission changes require human approval (Postgres shield)"
1321
+ }
1322
+ ],
1323
+ dangerousWords: ["dropdb", "pg_dropcluster"]
1324
+ };
1325
+ var project_jail_default = {
1326
+ name: "project-jail",
1327
+ description: "Restricts AI agents from reading sensitive credential files outside the current project",
1328
+ aliases: ["jail"],
1329
+ smartRules: [
1330
+ {
1331
+ name: "shield:project-jail:block-read-ssh",
1332
+ tool: "bash",
1333
+ conditions: [
1334
+ {
1335
+ field: "command",
1336
+ op: "matches",
1337
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*?\\.ssh[\\/\\\\]",
1338
+ flags: "i"
1339
+ }
1340
+ ],
1341
+ verdict: "block",
1342
+ reason: "Reading SSH private keys is blocked by project-jail shield"
1343
+ },
1344
+ {
1345
+ name: "shield:project-jail:block-read-aws",
1346
+ tool: "bash",
1347
+ conditions: [
1348
+ {
1349
+ field: "command",
1350
+ op: "matches",
1351
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*?\\.aws[\\/\\\\]",
1352
+ flags: "i"
1353
+ }
1354
+ ],
1355
+ verdict: "block",
1356
+ reason: "Reading AWS credentials is blocked by project-jail shield"
1357
+ },
1358
+ {
1359
+ name: "shield:project-jail:block-read-env",
1360
+ tool: "bash",
1361
+ conditions: [
1362
+ {
1363
+ field: "command",
1364
+ op: "matches",
1365
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*\\.env(\\.local|\\.production|\\.staging)?\\b",
1366
+ flags: "i"
1367
+ }
1368
+ ],
1369
+ verdict: "block",
1370
+ reason: "Reading .env files is blocked by project-jail shield"
1371
+ },
1372
+ {
1373
+ name: "shield:project-jail:review-read-credentials",
1374
+ tool: "bash",
1375
+ conditions: [
1376
+ {
1377
+ field: "command",
1378
+ op: "matches",
1379
+ 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)",
1380
+ flags: "i"
1381
+ }
1382
+ ],
1383
+ verdict: "review",
1384
+ reason: "Reading credential files requires approval (project-jail shield)"
1385
+ },
1386
+ {
1387
+ name: "shield:project-jail:block-read-ssh-any-tool",
1388
+ tool: "*",
1389
+ conditions: [
1390
+ {
1391
+ field: "file_path",
1392
+ op: "matches",
1393
+ value: "(^|[\\/\\\\])\\.ssh[\\/\\\\]",
1394
+ flags: "i"
1395
+ }
1396
+ ],
1397
+ verdict: "block",
1398
+ reason: "Reading SSH private keys is blocked by project-jail shield"
1399
+ },
1400
+ {
1401
+ name: "shield:project-jail:block-read-aws-any-tool",
1402
+ tool: "*",
1403
+ conditions: [
1404
+ {
1405
+ field: "file_path",
1406
+ op: "matches",
1407
+ value: "(^|[\\/\\\\])\\.aws[\\/\\\\]",
1408
+ flags: "i"
1409
+ }
1410
+ ],
1411
+ verdict: "block",
1412
+ reason: "Reading AWS credentials is blocked by project-jail shield"
1413
+ },
1414
+ {
1415
+ name: "shield:project-jail:review-read-env-any-tool",
1416
+ tool: "*",
1417
+ conditions: [
1418
+ {
1419
+ field: "file_path",
1420
+ op: "matches",
1421
+ value: "(^|[\\/\\\\])\\.env(\\.(local|production|staging|development|production\\.local|staging\\.local|development\\.local))?$",
1422
+ flags: "i"
1423
+ }
1424
+ ],
1425
+ verdict: "review",
1426
+ reason: "Reading .env files requires approval (project-jail shield)"
1427
+ },
1428
+ {
1429
+ name: "shield:project-jail:review-read-credentials-any-tool",
1430
+ tool: "*",
1431
+ conditions: [
1432
+ {
1433
+ field: "file_path",
1434
+ op: "matches",
1435
+ value: ".*(credentials\\.json|\\.netrc|\\.npmrc|\\.docker[\\/\\\\]config\\.json|gcloud[\\/\\\\]credentials|\\.kube[\\/\\\\]config)",
1436
+ flags: "i"
1437
+ }
1438
+ ],
1439
+ verdict: "review",
1440
+ reason: "Reading credential files requires approval (project-jail shield)"
1441
+ }
1442
+ ],
1443
+ dangerousWords: []
1444
+ };
1445
+ var redis_default = {
1446
+ name: "redis",
1447
+ description: "Protects Redis instances from destructive AI operations",
1448
+ aliases: [],
1449
+ smartRules: [
1450
+ {
1451
+ name: "shield:redis:block-flushall",
1452
+ tool: "*",
1453
+ conditions: [
1454
+ {
1455
+ field: "command",
1456
+ op: "matches",
1457
+ value: "\\bFLUSHALL\\b",
1458
+ flags: "i"
1459
+ }
1460
+ ],
1461
+ verdict: "block",
1462
+ reason: "FLUSHALL deletes every key in every database \u2014 blocked by Redis shield"
1463
+ },
1464
+ {
1465
+ name: "shield:redis:block-flushdb",
1466
+ tool: "*",
1467
+ conditions: [
1468
+ {
1469
+ field: "command",
1470
+ op: "matches",
1471
+ value: "\\bFLUSHDB\\b",
1472
+ flags: "i"
1473
+ }
1474
+ ],
1475
+ verdict: "block",
1476
+ reason: "FLUSHDB deletes all keys in the current database \u2014 blocked by Redis shield"
1477
+ },
1478
+ {
1479
+ name: "shield:redis:block-config-resetstat",
1480
+ tool: "*",
1481
+ conditions: [
1482
+ {
1483
+ field: "command",
1484
+ op: "matches",
1485
+ value: "\\bCONFIG\\s+RESETSTAT\\b",
1486
+ flags: "i"
1487
+ }
1488
+ ],
1489
+ verdict: "block",
1490
+ reason: "CONFIG RESETSTAT resets server statistics irreversibly \u2014 blocked by Redis shield"
1491
+ },
1492
+ {
1493
+ name: "shield:redis:review-config-set",
1494
+ tool: "*",
1495
+ conditions: [
1496
+ {
1497
+ field: "command",
1498
+ op: "matches",
1499
+ value: "\\bCONFIG\\s+SET\\b",
1500
+ flags: "i"
1501
+ }
1502
+ ],
1503
+ verdict: "review",
1504
+ reason: "CONFIG SET changes live server configuration and requires human approval (Redis shield)"
1505
+ },
1506
+ {
1507
+ name: "shield:redis:review-del-wildcard",
1508
+ tool: "*",
1509
+ conditions: [
1510
+ {
1511
+ field: "command",
1512
+ op: "matches",
1513
+ value: "\\bDEL\\b.*[*?\\[]|redis-cli.*--scan.*\\|.*xargs.*del",
1514
+ flags: "i"
1515
+ }
1516
+ ],
1517
+ verdict: "review",
1518
+ reason: "Wildcard key deletion requires human approval (Redis shield)"
1519
+ }
1520
+ ],
1521
+ dangerousWords: ["FLUSHALL", "FLUSHDB"]
1522
+ };
1523
+ var BUILTIN_SHIELDS = {
1524
+ [aws_default.name]: aws_default,
1525
+ [bash_safe_default.name]: bash_safe_default,
1526
+ [docker_default.name]: docker_default,
1527
+ [filesystem_default.name]: filesystem_default,
1528
+ [github_default.name]: github_default,
1529
+ [k8s_default.name]: k8s_default,
1530
+ [mongodb_default.name]: mongodb_default,
1531
+ [postgres_default.name]: postgres_default,
1532
+ [project_jail_default.name]: project_jail_default,
1533
+ [redis_default.name]: redis_default
1534
+ };
1535
+ function assertBuiltinShieldRegexesAreSafe() {
1536
+ for (const shield of Object.values(BUILTIN_SHIELDS)) {
1537
+ for (const rule of shield.smartRules) {
1538
+ const conditions = rule.conditions ?? [];
1539
+ for (const cond of conditions) {
1540
+ if (cond.op !== "matches" && cond.op !== "notMatches") continue;
1541
+ const pattern = cond.value;
1542
+ if (!pattern) continue;
1543
+ if (!safeRegex3(pattern)) {
1544
+ throw new Error(
1545
+ `[node9 engine] Shield '${shield.name}' rule '${rule.name ?? rule.tool}' has unsafe regex: ${pattern}`
1546
+ );
1547
+ }
1548
+ }
1549
+ }
1550
+ }
1551
+ }
1552
+ assertBuiltinShieldRegexesAreSafe();
1553
+ var LONG_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
1554
+
1555
+ // src/cli/render/ink/panels/ShieldsPanel.tsx
1556
+ import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
1557
+ function ShieldsPanel({ summary, blastScore, width }) {
1558
+ const impacts = rollupByShield(summary.sections);
1559
+ const exposed = Math.max(0, 100 - blastScore);
1560
+ const ranked = [...impacts].sort((a, b) => {
1561
+ const aDiscount = PROTECTIVE_SHIELD_DISCOUNTS[a.shieldName] ?? 0;
1562
+ const bDiscount = PROTECTIVE_SHIELD_DISCOUNTS[b.shieldName] ?? 0;
1563
+ if (aDiscount !== bDiscount) return bDiscount - aDiscount;
1564
+ return b.totalCatches - a.totalCatches;
1565
+ });
1566
+ const hitShields = ranked.filter((i) => i.totalCatches > 0);
1567
+ const hitNames = new Set(hitShields.map((i) => i.shieldName));
1568
+ const zeroHitBuiltins = Object.keys(BUILTIN_SHIELDS).filter((name) => !hitNames.has(name)).sort();
1569
+ const topRec = hitShields.find((r) => (PROTECTIVE_SHIELD_DISCOUNTS[r.shieldName] ?? 0) > 0);
1570
+ const topRecBonus = topRec ? Math.round(exposed * (PROTECTIVE_SHIELD_DISCOUNTS[topRec.shieldName] ?? 0)) : 0;
1571
+ return /* @__PURE__ */ jsxs10(Box10, { borderStyle: "round", borderColor: "cyan", paddingX: 1, flexDirection: "column", width, children: [
1572
+ /* @__PURE__ */ jsx10(Text10, { bold: true, color: "cyan", children: "SHIELDS" }),
1573
+ hitShields.map((impact) => {
1574
+ const discount = PROTECTIVE_SHIELD_DISCOUNTS[impact.shieldName] ?? 0;
1575
+ const bonus = Math.round(exposed * discount);
1576
+ const noun = `op${impact.totalCatches !== 1 ? "s" : ""}`;
1577
+ return /* @__PURE__ */ jsxs10(Box10, { children: [
1578
+ /* @__PURE__ */ jsx10(Box10, { width: 16, children: /* @__PURE__ */ jsx10(Text10, { bold: true, color: discount > 0 ? "cyan" : void 0, dimColor: discount === 0, children: impact.shieldName }) }),
1579
+ /* @__PURE__ */ jsx10(Box10, { width: 20, children: /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: `catches ${impact.totalCatches} ${noun}` }) }),
1580
+ bonus > 0 ? /* @__PURE__ */ jsx10(
1581
+ Text10,
1582
+ {
1583
+ bold: true,
1584
+ color: "green",
1585
+ children: `\u2192 +${bonus} pts (${blastScore} \u2192 ${blastScore + bonus})`
1586
+ }
1587
+ ) : null
1588
+ ] }, impact.shieldName);
1589
+ }),
1590
+ zeroHitBuiltins.length > 0 ? /* @__PURE__ */ jsx10(Box10, { flexDirection: "column", children: /* @__PURE__ */ jsx10(Text10, { dimColor: true, wrap: "truncate-end", children: zeroHitBuiltins.join(" \xB7 ") + " (no hits \u2014 install proactively)" }) }) : null,
1591
+ topRec ? /* @__PURE__ */ jsx10(Box10, { children: /* @__PURE__ */ jsx10(Text10, { color: "cyan", bold: true, children: `\u2192 node9 shield enable ${topRec.shieldName} (start here \u2014 +${topRecBonus} pts)` }) }) : null
1592
+ ] });
1593
+ }
1594
+
1595
+ // src/cli/render/ink/StaticScorecard.tsx
1596
+ import { Fragment as Fragment2, jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
1597
+ var MAX_WIDTH = 90;
1598
+ function renderWidth() {
1599
+ const term = process.stdout.columns ?? MAX_WIDTH;
1600
+ return Math.min(term, MAX_WIDTH);
1601
+ }
1602
+ function StaticScorecard({ input, rangeLabel }) {
1603
+ const { summary, blockedCount } = input;
1604
+ const width = renderWidth();
1605
+ const halfWidth = Math.floor((width - 1) / 2);
1606
+ const leakCount = summary.leaks.length;
1607
+ const hasCritical = leakCount > 0 || blockedCount > 0;
1608
+ const criticalLabel = (() => {
1609
+ const parts = [];
1610
+ if (leakCount > 0) {
1611
+ parts.push(`${leakCount} secret${leakCount !== 1 ? "s" : ""} leaked`);
1612
+ }
1613
+ if (blockedCount > 0) {
1614
+ parts.push(`${blockedCount} op${blockedCount !== 1 ? "s" : ""} blocked`);
1615
+ }
1616
+ return `Critical (${parts.join(" + ")})`;
1617
+ })();
1618
+ return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", paddingTop: 1, width, children: [
1619
+ /* @__PURE__ */ jsx11(Header, { rangeLabel }),
1620
+ /* @__PURE__ */ jsx11(SeverityBand, { label: "Spend & activity", width }),
1621
+ /* @__PURE__ */ jsxs11(Box11, { flexDirection: "row", gap: 1, children: [
1622
+ /* @__PURE__ */ jsx11(CostPanel, { summary, width: halfWidth }),
1623
+ /* @__PURE__ */ jsx11(ActivityPanel, { summary, scan: input.scan, width: halfWidth })
1624
+ ] }),
1625
+ hasCritical ? /* @__PURE__ */ jsxs11(Fragment2, { children: [
1626
+ /* @__PURE__ */ jsx11(SeverityBand, { label: criticalLabel, width }),
1627
+ /* @__PURE__ */ jsxs11(Box11, { flexDirection: "row", gap: 1, children: [
1628
+ /* @__PURE__ */ jsx11(LeaksPanel, { summary, width: halfWidth }),
1629
+ /* @__PURE__ */ jsx11(BlockedPanel, { summary, width: halfWidth })
1630
+ ] })
1631
+ ] }) : null,
1632
+ input.blastExposures > 0 ? /* @__PURE__ */ jsxs11(Fragment2, { children: [
1633
+ /* @__PURE__ */ jsx11(
1634
+ SeverityBand,
1635
+ {
1636
+ label: `High (${input.blastExposures} path${input.blastExposures !== 1 ? "s" : ""} reachable on disk)`,
1637
+ width
1638
+ }
1639
+ ),
1640
+ /* @__PURE__ */ jsx11(
1641
+ BlastRadiusPanel,
1642
+ {
1643
+ blast: input.blast,
1644
+ blastExposures: input.blastExposures,
1645
+ width
1646
+ }
1647
+ )
1648
+ ] }) : null,
1649
+ (() => {
1650
+ const reviewCount = input.reviewCount;
1651
+ const loopCount = input.scan.loopFindings.length;
1652
+ if (reviewCount === 0 && loopCount === 0) return null;
1653
+ const { wastePct } = computeLoopWaste(input.scan.loopFindings, input.scan.totalToolCalls);
1654
+ const parts = [];
1655
+ if (reviewCount > 0) parts.push(`${reviewCount} op${reviewCount !== 1 ? "s" : ""} flagged`);
1656
+ if (loopCount > 0)
1657
+ parts.push(
1658
+ `${loopCount} loop${loopCount !== 1 ? "s" : ""}${wastePct > 0 ? ` \xB7 ${wastePct}% wasted` : ""}`
1659
+ );
1660
+ return /* @__PURE__ */ jsxs11(Fragment2, { children: [
1661
+ /* @__PURE__ */ jsx11(SeverityBand, { label: `Medium (${parts.join(" \xB7 ")})`, width }),
1662
+ /* @__PURE__ */ jsxs11(Box11, { flexDirection: "row", gap: 1, children: [
1663
+ /* @__PURE__ */ jsx11(ReviewQueuePanel, { summary: input.summary, width: halfWidth }),
1664
+ /* @__PURE__ */ jsx11(AgentLoopsPanel, { loopFindings: input.scan.loopFindings, width: halfWidth })
1665
+ ] })
1666
+ ] });
1667
+ })(),
1668
+ /* @__PURE__ */ jsx11(SeverityBand, { label: "Recommended action", width }),
1669
+ /* @__PURE__ */ jsx11(ShieldsPanel, { summary: input.summary, blastScore: input.blast.score, width })
1670
+ ] });
1671
+ }
1672
+ function renderScanScorecardInk(input, rangeLabel) {
1673
+ const { unmount } = render(/* @__PURE__ */ jsx11(StaticScorecard, { input, rangeLabel }), {
1674
+ patchConsole: false
1675
+ });
1676
+ unmount();
1677
+ }
1678
+ export {
1679
+ StaticScorecard,
1680
+ renderScanScorecardInk
1681
+ };