@node9/policy-engine 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +183 -0
- package/README.md +66 -0
- package/dist/index.d.mts +415 -0
- package/dist/index.d.ts +415 -0
- package/dist/index.js +2297 -0
- package/dist/index.mjs +2228 -0
- package/package.json +57 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2297 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var src_exports = {};
|
|
32
|
+
__export(src_exports, {
|
|
33
|
+
BUILTIN_SHIELDS: () => BUILTIN_SHIELDS,
|
|
34
|
+
DLP_PATTERNS: () => DLP_PATTERNS,
|
|
35
|
+
ENGINE_VERSION: () => ENGINE_VERSION,
|
|
36
|
+
FLAGS_WITH_VALUES: () => FLAGS_WITH_VALUES,
|
|
37
|
+
LOOP_MAX_RECORDS: () => LOOP_MAX_RECORDS,
|
|
38
|
+
SENSITIVE_PATH_REGEXES: () => SENSITIVE_PATH_REGEXES,
|
|
39
|
+
analyzePipeChain: () => analyzePipeChain,
|
|
40
|
+
analyzeShellCommand: () => analyzeShellCommand,
|
|
41
|
+
checkDangerousSql: () => checkDangerousSql,
|
|
42
|
+
computeArgsHash: () => computeArgsHash,
|
|
43
|
+
detectDangerousEval: () => detectDangerousEval,
|
|
44
|
+
detectDangerousShellExec: () => detectDangerousShellExec,
|
|
45
|
+
evaluateLoopWindow: () => evaluateLoopWindow,
|
|
46
|
+
evaluatePolicy: () => evaluatePolicy,
|
|
47
|
+
evaluateSmartConditions: () => evaluateSmartConditions,
|
|
48
|
+
extractAllSshHosts: () => extractAllSshHosts,
|
|
49
|
+
extractNetworkTargets: () => extractNetworkTargets,
|
|
50
|
+
extractPositionalArgs: () => extractPositionalArgs,
|
|
51
|
+
getCompiledRegex: () => getCompiledRegex,
|
|
52
|
+
getNestedValue: () => getNestedValue,
|
|
53
|
+
isIgnoredTool: () => isIgnoredTool,
|
|
54
|
+
isShieldVerdict: () => isShieldVerdict,
|
|
55
|
+
matchSensitivePath: () => matchSensitivePath,
|
|
56
|
+
matchesPattern: () => matchesPattern,
|
|
57
|
+
normalizeCommandForPolicy: () => normalizeCommandForPolicy,
|
|
58
|
+
parseAllSshHostsFromCommand: () => parseAllSshHostsFromCommand,
|
|
59
|
+
redactText: () => redactText,
|
|
60
|
+
scanArgs: () => scanArgs,
|
|
61
|
+
scanText: () => scanText,
|
|
62
|
+
sensitivePathMatch: () => sensitivePathMatch,
|
|
63
|
+
validateOverrides: () => validateOverrides,
|
|
64
|
+
validateRegex: () => validateRegex,
|
|
65
|
+
validateShieldDefinition: () => validateShieldDefinition
|
|
66
|
+
});
|
|
67
|
+
module.exports = __toCommonJS(src_exports);
|
|
68
|
+
|
|
69
|
+
// src/dlp/index.ts
|
|
70
|
+
var ASSIGNMENT_CONTEXT_RE = /\b(?:password|passwd|secret|token|api[_-]?key|auth(?:_key|_token)?|credential|private[_-]?key|access[_-]?key|client[_-]?secret)\s*[=:]\s*/i;
|
|
71
|
+
function isAssignmentContext(text) {
|
|
72
|
+
return ASSIGNMENT_CONTEXT_RE.test(text);
|
|
73
|
+
}
|
|
74
|
+
function shannonEntropy(s) {
|
|
75
|
+
if (s.length === 0) return 0;
|
|
76
|
+
const freq = /* @__PURE__ */ new Map();
|
|
77
|
+
for (const ch of s) freq.set(ch, (freq.get(ch) ?? 0) + 1);
|
|
78
|
+
let h = 0;
|
|
79
|
+
for (const count of freq.values()) {
|
|
80
|
+
const p = count / s.length;
|
|
81
|
+
h -= p * Math.log2(p);
|
|
82
|
+
}
|
|
83
|
+
return h;
|
|
84
|
+
}
|
|
85
|
+
var DLP_STOPWORDS = [
|
|
86
|
+
"example",
|
|
87
|
+
"placeholder",
|
|
88
|
+
"changeme",
|
|
89
|
+
"your_key",
|
|
90
|
+
"your_token",
|
|
91
|
+
"your_secret",
|
|
92
|
+
"replace_me",
|
|
93
|
+
"insert_key",
|
|
94
|
+
"put_your",
|
|
95
|
+
"fake",
|
|
96
|
+
"dummy",
|
|
97
|
+
"sample",
|
|
98
|
+
"xxxxxxxx",
|
|
99
|
+
"aaaaaa",
|
|
100
|
+
"bbbbbb",
|
|
101
|
+
"00000000",
|
|
102
|
+
"${",
|
|
103
|
+
"{{",
|
|
104
|
+
"%{",
|
|
105
|
+
"<your",
|
|
106
|
+
"test_key",
|
|
107
|
+
"test_token",
|
|
108
|
+
"your",
|
|
109
|
+
"here"
|
|
110
|
+
];
|
|
111
|
+
var DLP_PATTERNS = [
|
|
112
|
+
// ── AWS ───────────────────────────────────────────────────────────────────
|
|
113
|
+
{
|
|
114
|
+
name: "AWS Access Key ID",
|
|
115
|
+
regex: /\b(?:A3T[A-Z0-9]|AKIA|ASIA|ABIA|ACCA)[A-Z2-7]{16}\b/,
|
|
116
|
+
severity: "block",
|
|
117
|
+
keywords: ["akia", "asia", "abia", "acca", "a3t"]
|
|
118
|
+
},
|
|
119
|
+
// ── GitHub ────────────────────────────────────────────────────────────────
|
|
120
|
+
{
|
|
121
|
+
name: "GitHub Token",
|
|
122
|
+
regex: /\bgh[pous]_[A-Za-z0-9]{36}\b/,
|
|
123
|
+
severity: "block",
|
|
124
|
+
keywords: ["ghp_", "gho_", "ghu_", "ghs_"],
|
|
125
|
+
minEntropy: 3
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: "GitHub Fine-Grained PAT",
|
|
129
|
+
regex: /\bgithub_pat_\w{82}\b/,
|
|
130
|
+
severity: "block",
|
|
131
|
+
keywords: ["github_pat_"]
|
|
132
|
+
},
|
|
133
|
+
// ── Slack ─────────────────────────────────────────────────────────────────
|
|
134
|
+
{
|
|
135
|
+
name: "Slack Bot Token",
|
|
136
|
+
// Real tokens are ~50–80 chars; lower bound 20 avoids false negatives on partial tokens
|
|
137
|
+
regex: /\bxoxb-[0-9A-Za-z-]{20,100}\b/,
|
|
138
|
+
severity: "block",
|
|
139
|
+
keywords: ["xoxb-"]
|
|
140
|
+
},
|
|
141
|
+
// ── Anthropic ─────────────────────────────────────────────────────────────
|
|
142
|
+
// Listed before OpenAI — Anthropic keys start with sk-ant- which would also
|
|
143
|
+
// match the broader OpenAI sk- pattern; more specific rules must come first.
|
|
144
|
+
{
|
|
145
|
+
name: "Anthropic API Key",
|
|
146
|
+
regex: /\bsk-ant-api03-[a-zA-Z0-9_-]{93}AA\b/,
|
|
147
|
+
severity: "block",
|
|
148
|
+
keywords: ["sk-ant-api03"]
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: "Anthropic Admin Key",
|
|
152
|
+
regex: /\bsk-ant-admin01-[a-zA-Z0-9_-]{93}AA\b/,
|
|
153
|
+
severity: "block",
|
|
154
|
+
keywords: ["sk-ant-admin01"]
|
|
155
|
+
},
|
|
156
|
+
// ── OpenAI ────────────────────────────────────────────────────────────────
|
|
157
|
+
{
|
|
158
|
+
name: "OpenAI API Key",
|
|
159
|
+
regex: /\bsk-[a-zA-Z0-9_-]{20,}\b/,
|
|
160
|
+
severity: "block",
|
|
161
|
+
keywords: ["sk-"],
|
|
162
|
+
minEntropy: 3.5
|
|
163
|
+
},
|
|
164
|
+
// ── Stripe ────────────────────────────────────────────────────────────────
|
|
165
|
+
{
|
|
166
|
+
name: "Stripe Secret Key",
|
|
167
|
+
regex: /\bsk_(?:live|test)_[0-9a-zA-Z]{24}\b/,
|
|
168
|
+
severity: "block",
|
|
169
|
+
keywords: ["sk_live_", "sk_test_"]
|
|
170
|
+
},
|
|
171
|
+
// ── GCP ───────────────────────────────────────────────────────────────────
|
|
172
|
+
{
|
|
173
|
+
name: "GCP API Key",
|
|
174
|
+
regex: /\bAIza[0-9A-Za-z_-]{35}\b/,
|
|
175
|
+
severity: "block",
|
|
176
|
+
keywords: ["aiza"],
|
|
177
|
+
minEntropy: 3
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
name: "GCP Service Account",
|
|
181
|
+
regex: /"type"\s*:\s*"service_account"/,
|
|
182
|
+
severity: "block",
|
|
183
|
+
keywords: ["service_account"]
|
|
184
|
+
},
|
|
185
|
+
// ── Azure ─────────────────────────────────────────────────────────────────
|
|
186
|
+
// Pattern: 3 alphanum chars + digit + Q~ + 31-34 alphanum chars
|
|
187
|
+
{
|
|
188
|
+
name: "Azure AD Client Secret",
|
|
189
|
+
regex: /(?:^|[\s>=:(,])([a-zA-Z0-9_~.]{3}\dQ~[a-zA-Z0-9_~.-]{31,34})(?:$|[\s<),])/,
|
|
190
|
+
severity: "block",
|
|
191
|
+
keywords: ["q~"]
|
|
192
|
+
},
|
|
193
|
+
// ── Databricks ────────────────────────────────────────────────────────────
|
|
194
|
+
{
|
|
195
|
+
name: "Databricks API Token",
|
|
196
|
+
regex: /\bdapi[a-f0-9]{32}(?:-\d)?\b/,
|
|
197
|
+
severity: "block",
|
|
198
|
+
keywords: ["dapi"]
|
|
199
|
+
},
|
|
200
|
+
// ── DigitalOcean ──────────────────────────────────────────────────────────
|
|
201
|
+
{
|
|
202
|
+
name: "DigitalOcean PAT",
|
|
203
|
+
regex: /\bdop_v1_[a-f0-9]{64}\b/,
|
|
204
|
+
severity: "block",
|
|
205
|
+
keywords: ["dop_v1_"]
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
name: "DigitalOcean Access Token",
|
|
209
|
+
regex: /\bdoo_v1_[a-f0-9]{64}\b/,
|
|
210
|
+
severity: "block",
|
|
211
|
+
keywords: ["doo_v1_"]
|
|
212
|
+
},
|
|
213
|
+
// ── Doppler ───────────────────────────────────────────────────────────────
|
|
214
|
+
{
|
|
215
|
+
name: "Doppler Token",
|
|
216
|
+
regex: /\bdp\.pt\.[a-z0-9]{43}\b/i,
|
|
217
|
+
severity: "block",
|
|
218
|
+
keywords: ["dp.pt."]
|
|
219
|
+
},
|
|
220
|
+
// ── HashiCorp Vault ───────────────────────────────────────────────────────
|
|
221
|
+
{
|
|
222
|
+
name: "HashiCorp Vault Service Token",
|
|
223
|
+
regex: /\bhvs\.[\w-]{90,120}\b/,
|
|
224
|
+
severity: "block",
|
|
225
|
+
keywords: ["hvs."]
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
name: "HashiCorp Vault Batch Token",
|
|
229
|
+
regex: /\bhvb\.[\w-]{138,300}\b/,
|
|
230
|
+
severity: "block",
|
|
231
|
+
keywords: ["hvb."]
|
|
232
|
+
},
|
|
233
|
+
// ── Hugging Face ──────────────────────────────────────────────────────────
|
|
234
|
+
{
|
|
235
|
+
name: "HuggingFace Token",
|
|
236
|
+
regex: /\bhf_[A-Za-z]{34}\b/,
|
|
237
|
+
severity: "block",
|
|
238
|
+
keywords: ["hf_"],
|
|
239
|
+
minEntropy: 3
|
|
240
|
+
},
|
|
241
|
+
// ── Postman ───────────────────────────────────────────────────────────────
|
|
242
|
+
{
|
|
243
|
+
name: "Postman API Token",
|
|
244
|
+
regex: /\bPMAK-[a-f0-9]{24}-[a-f0-9]{34}\b/i,
|
|
245
|
+
severity: "block",
|
|
246
|
+
keywords: ["pmak-"]
|
|
247
|
+
},
|
|
248
|
+
// ── Pulumi ────────────────────────────────────────────────────────────────
|
|
249
|
+
{
|
|
250
|
+
name: "Pulumi Access Token",
|
|
251
|
+
regex: /\bpul-[a-f0-9]{40}\b/,
|
|
252
|
+
severity: "block",
|
|
253
|
+
keywords: ["pul-"]
|
|
254
|
+
},
|
|
255
|
+
// ── SendGrid ──────────────────────────────────────────────────────────────
|
|
256
|
+
{
|
|
257
|
+
name: "SendGrid API Key",
|
|
258
|
+
regex: /\bSG\.[a-zA-Z0-9=_.-]{66}\b/,
|
|
259
|
+
severity: "block",
|
|
260
|
+
keywords: ["sg."]
|
|
261
|
+
},
|
|
262
|
+
// ── Private keys (PEM) ────────────────────────────────────────────────────
|
|
263
|
+
{
|
|
264
|
+
name: "Private Key (PEM)",
|
|
265
|
+
regex: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
|
|
266
|
+
severity: "block",
|
|
267
|
+
keywords: ["-----begin"]
|
|
268
|
+
},
|
|
269
|
+
// ── NPM ───────────────────────────────────────────────────────────────────
|
|
270
|
+
{
|
|
271
|
+
name: "NPM Auth Token",
|
|
272
|
+
regex: /_authToken\s*=\s*[A-Za-z0-9_-]{20,}/,
|
|
273
|
+
severity: "block",
|
|
274
|
+
keywords: ["_authtoken"]
|
|
275
|
+
},
|
|
276
|
+
// ── JWT ───────────────────────────────────────────────────────────────────
|
|
277
|
+
// review (not block): JWTs appear legitimately in API calls; flag for human approval
|
|
278
|
+
// contextBoost: promoted to block when assigned (e.g. TOKEN=eyJ...)
|
|
279
|
+
{
|
|
280
|
+
name: "JWT",
|
|
281
|
+
regex: /\bey[a-zA-Z0-9]{17,}\.ey[a-zA-Z0-9\/_-]{17,}\.[a-zA-Z0-9\/_-]{10,}={0,2}\b/,
|
|
282
|
+
severity: "review",
|
|
283
|
+
keywords: ["eyj"],
|
|
284
|
+
contextBoost: true
|
|
285
|
+
},
|
|
286
|
+
// ── Stripe (extended — adds restricted key rk_ prefix) ──────────────────
|
|
287
|
+
{
|
|
288
|
+
name: "Stripe Restricted Key",
|
|
289
|
+
regex: /\brk_(?:live|test|prod)_[0-9a-zA-Z]{10,99}\b/,
|
|
290
|
+
severity: "block",
|
|
291
|
+
keywords: ["rk_live_", "rk_test_", "rk_prod_"]
|
|
292
|
+
},
|
|
293
|
+
// ── Slack (app token) ─────────────────────────────────────────────────────
|
|
294
|
+
{
|
|
295
|
+
name: "Slack App Token",
|
|
296
|
+
regex: /\bxapp-\d-[A-Z0-9]+-\d+-[a-f0-9]+\b/,
|
|
297
|
+
severity: "block",
|
|
298
|
+
keywords: ["xapp-"]
|
|
299
|
+
},
|
|
300
|
+
// ── GitLab ────────────────────────────────────────────────────────────────
|
|
301
|
+
{ name: "GitLab PAT", regex: /\bglpat-[\w-]{20}\b/, severity: "block", keywords: ["glpat-"] },
|
|
302
|
+
{
|
|
303
|
+
name: "GitLab Deploy Token",
|
|
304
|
+
regex: /\bgldt-[0-9a-zA-Z_-]{20}\b/,
|
|
305
|
+
severity: "block",
|
|
306
|
+
keywords: ["gldt-"]
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
name: "GitLab CI Job Token",
|
|
310
|
+
regex: /\bglcbt-[0-9a-zA-Z]{1,5}_[0-9a-zA-Z_-]{20}\b/,
|
|
311
|
+
severity: "block",
|
|
312
|
+
keywords: ["glcbt-"]
|
|
313
|
+
},
|
|
314
|
+
// ── npm (publish token) ───────────────────────────────────────────────────
|
|
315
|
+
{
|
|
316
|
+
name: "npm Access Token",
|
|
317
|
+
regex: /\bnpm_[a-zA-Z0-9]{36}\b/,
|
|
318
|
+
severity: "block",
|
|
319
|
+
keywords: ["npm_"]
|
|
320
|
+
},
|
|
321
|
+
// ── Shopify ───────────────────────────────────────────────────────────────
|
|
322
|
+
{
|
|
323
|
+
name: "Shopify Access Token",
|
|
324
|
+
regex: /\bshpat_[a-fA-F0-9]{32}\b/,
|
|
325
|
+
severity: "block",
|
|
326
|
+
keywords: ["shpat_"]
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
name: "Shopify Custom Access Token",
|
|
330
|
+
regex: /\bshpca_[a-fA-F0-9]{32}\b/,
|
|
331
|
+
severity: "block",
|
|
332
|
+
keywords: ["shpca_"]
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
name: "Shopify Private App Token",
|
|
336
|
+
regex: /\bshppa_[a-fA-F0-9]{32}\b/,
|
|
337
|
+
severity: "block",
|
|
338
|
+
keywords: ["shppa_"]
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
name: "Shopify Shared Secret",
|
|
342
|
+
regex: /\bshpss_[a-fA-F0-9]{32}\b/,
|
|
343
|
+
severity: "block",
|
|
344
|
+
keywords: ["shpss_"]
|
|
345
|
+
},
|
|
346
|
+
// ── Linear ────────────────────────────────────────────────────────────────
|
|
347
|
+
{
|
|
348
|
+
name: "Linear API Key",
|
|
349
|
+
regex: /\blin_api_[a-zA-Z0-9]{40}\b/,
|
|
350
|
+
severity: "block",
|
|
351
|
+
keywords: ["lin_api_"]
|
|
352
|
+
},
|
|
353
|
+
// ── PlanetScale ───────────────────────────────────────────────────────────
|
|
354
|
+
{
|
|
355
|
+
name: "PlanetScale API Token",
|
|
356
|
+
regex: /\bpscale_tkn_[\w.-]{32,64}\b/,
|
|
357
|
+
severity: "block",
|
|
358
|
+
keywords: ["pscale_tkn_"]
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
name: "PlanetScale Password",
|
|
362
|
+
regex: /\bpscale_pw_[\w.-]{32,64}\b/,
|
|
363
|
+
severity: "block",
|
|
364
|
+
keywords: ["pscale_pw_"]
|
|
365
|
+
},
|
|
366
|
+
// ── Sentry ────────────────────────────────────────────────────────────────
|
|
367
|
+
{
|
|
368
|
+
name: "Sentry User Token",
|
|
369
|
+
regex: /\bsntryu_[a-f0-9]{64}\b/,
|
|
370
|
+
severity: "block",
|
|
371
|
+
keywords: ["sntryu_"]
|
|
372
|
+
},
|
|
373
|
+
// ── Grafana ───────────────────────────────────────────────────────────────
|
|
374
|
+
{
|
|
375
|
+
name: "Grafana Service Account Token",
|
|
376
|
+
regex: /\bglsa_[a-zA-Z0-9]{32}_[a-f0-9]{8}\b/,
|
|
377
|
+
severity: "block",
|
|
378
|
+
keywords: ["glsa_"]
|
|
379
|
+
},
|
|
380
|
+
// ── Heroku ────────────────────────────────────────────────────────────────
|
|
381
|
+
{
|
|
382
|
+
name: "Heroku API Key",
|
|
383
|
+
regex: /\bHRKU-AA[0-9a-zA-Z_-]{58}\b/,
|
|
384
|
+
severity: "block",
|
|
385
|
+
keywords: ["hrku-aa"]
|
|
386
|
+
},
|
|
387
|
+
// ── PyPI ──────────────────────────────────────────────────────────────────
|
|
388
|
+
{
|
|
389
|
+
name: "PyPI Upload Token",
|
|
390
|
+
regex: /\bpypi-[A-Za-z0-9_-]{50,}\b/,
|
|
391
|
+
severity: "block",
|
|
392
|
+
keywords: ["pypi-"],
|
|
393
|
+
minEntropy: 3
|
|
394
|
+
},
|
|
395
|
+
// ── Bearer Token ─────────────────────────────────────────────────────────
|
|
396
|
+
// contextBoost: promoted to block when assigned (e.g. AUTH_TOKEN=Bearer eyJ...)
|
|
397
|
+
{
|
|
398
|
+
name: "Bearer Token",
|
|
399
|
+
regex: /Bearer\s+[a-zA-Z0-9\-._~+/]{20,}=*/i,
|
|
400
|
+
severity: "review",
|
|
401
|
+
keywords: ["bearer"],
|
|
402
|
+
contextBoost: true,
|
|
403
|
+
minEntropy: 3
|
|
404
|
+
},
|
|
405
|
+
// ── Resend ────────────────────────────────────────────────────────────────
|
|
406
|
+
{
|
|
407
|
+
name: "Resend API Key",
|
|
408
|
+
regex: /\bre_[a-zA-Z0-9]{24}\b/,
|
|
409
|
+
severity: "block",
|
|
410
|
+
keywords: ["re_"]
|
|
411
|
+
},
|
|
412
|
+
// ── Telegram ──────────────────────────────────────────────────────────────
|
|
413
|
+
{
|
|
414
|
+
name: "Telegram Bot Token",
|
|
415
|
+
regex: /\b[0-9]{7,10}:AA[a-zA-Z0-9_-]{33}\b/,
|
|
416
|
+
severity: "block",
|
|
417
|
+
keywords: [":aa"]
|
|
418
|
+
},
|
|
419
|
+
// ── Mapbox ────────────────────────────────────────────────────────────────
|
|
420
|
+
{
|
|
421
|
+
name: "Mapbox Access Token",
|
|
422
|
+
regex: /\bpk\.eyJ1[a-zA-Z0-9._-]{20,}\b/,
|
|
423
|
+
severity: "block",
|
|
424
|
+
keywords: ["pk.eyj1"],
|
|
425
|
+
minEntropy: 3
|
|
426
|
+
},
|
|
427
|
+
// ── Notion ────────────────────────────────────────────────────────────────
|
|
428
|
+
{
|
|
429
|
+
name: "Notion Integration Token",
|
|
430
|
+
regex: /\bsecret_[a-zA-Z0-9]{43}\b/,
|
|
431
|
+
severity: "block",
|
|
432
|
+
keywords: ["secret_"]
|
|
433
|
+
},
|
|
434
|
+
// ── Square ────────────────────────────────────────────────────────────────
|
|
435
|
+
{
|
|
436
|
+
name: "Square Access Token",
|
|
437
|
+
regex: /\bsq0atp-[0-9A-Za-z_-]{22}\b/,
|
|
438
|
+
severity: "block",
|
|
439
|
+
keywords: ["sq0atp-"]
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
name: "Square OAuth Secret",
|
|
443
|
+
regex: /\bsq0csp-[0-9A-Za-z_-]{43}\b/,
|
|
444
|
+
severity: "block",
|
|
445
|
+
keywords: ["sq0csp-"]
|
|
446
|
+
},
|
|
447
|
+
// ── Typeform ──────────────────────────────────────────────────────────────
|
|
448
|
+
{
|
|
449
|
+
name: "Typeform Token",
|
|
450
|
+
regex: /\btfp_[a-zA-Z0-9_]{59}\b/,
|
|
451
|
+
severity: "block",
|
|
452
|
+
keywords: ["tfp_"]
|
|
453
|
+
},
|
|
454
|
+
// ── Cloudinary ────────────────────────────────────────────────────────────
|
|
455
|
+
{
|
|
456
|
+
name: "Cloudinary URL",
|
|
457
|
+
regex: /\bcloudinary:\/\/[0-9]+:[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+/,
|
|
458
|
+
severity: "block",
|
|
459
|
+
keywords: ["cloudinary://"]
|
|
460
|
+
},
|
|
461
|
+
// ── Airtable ──────────────────────────────────────────────────────────────
|
|
462
|
+
// New PAT format: pat + 14 alphanum + . + 64 alphanum
|
|
463
|
+
{
|
|
464
|
+
name: "Airtable PAT",
|
|
465
|
+
regex: /\bpat[a-zA-Z0-9]{14}\.[a-zA-Z0-9]{64}\b/,
|
|
466
|
+
severity: "block",
|
|
467
|
+
keywords: ["pat"]
|
|
468
|
+
},
|
|
469
|
+
// ── RubyGems ──────────────────────────────────────────────────────────────
|
|
470
|
+
{
|
|
471
|
+
name: "RubyGems API Key",
|
|
472
|
+
regex: /\brubygems_[a-f0-9]{48}\b/,
|
|
473
|
+
severity: "block",
|
|
474
|
+
keywords: ["rubygems_"]
|
|
475
|
+
},
|
|
476
|
+
// ── Shippo ────────────────────────────────────────────────────────────────
|
|
477
|
+
{
|
|
478
|
+
name: "Shippo Token",
|
|
479
|
+
regex: /\bshippo_(?:live|test)_[a-f0-9]{40}\b/,
|
|
480
|
+
severity: "block",
|
|
481
|
+
keywords: ["shippo_"]
|
|
482
|
+
},
|
|
483
|
+
// ── Plaid ─────────────────────────────────────────────────────────────────
|
|
484
|
+
{
|
|
485
|
+
name: "Plaid Access Token",
|
|
486
|
+
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/,
|
|
487
|
+
severity: "block",
|
|
488
|
+
keywords: ["access-sandbox", "access-development", "access-production"]
|
|
489
|
+
},
|
|
490
|
+
// ── Age ───────────────────────────────────────────────────────────────────
|
|
491
|
+
{
|
|
492
|
+
name: "Age Identity Key",
|
|
493
|
+
regex: /\bAGE-SECRET-KEY-1[QPZRY9X8GF2TVDW0S3JNLH]{58}\b/,
|
|
494
|
+
severity: "block",
|
|
495
|
+
keywords: ["age-secret-key-"]
|
|
496
|
+
}
|
|
497
|
+
];
|
|
498
|
+
var DLP_PATTERNS_GLOBAL = DLP_PATTERNS.map(
|
|
499
|
+
(p) => ({
|
|
500
|
+
pattern: p,
|
|
501
|
+
globalRegex: new RegExp(
|
|
502
|
+
p.regex.source,
|
|
503
|
+
p.regex.flags.includes("g") ? p.regex.flags : p.regex.flags + "g"
|
|
504
|
+
)
|
|
505
|
+
})
|
|
506
|
+
);
|
|
507
|
+
var SENSITIVE_PATH_PATTERNS = [
|
|
508
|
+
/[/\\]\.ssh[/\\]/i,
|
|
509
|
+
/[/\\]\.aws[/\\]/i,
|
|
510
|
+
/[/\\]\.config[/\\]gcloud[/\\]/i,
|
|
511
|
+
/[/\\]\.azure[/\\]/i,
|
|
512
|
+
/[/\\]\.kube[/\\]config$/i,
|
|
513
|
+
/[/\\]\.env($|\.)/i,
|
|
514
|
+
// .env, .env.local, .env.production — not .envoy
|
|
515
|
+
/[/\\]\.git-credentials$/i,
|
|
516
|
+
/[/\\]\.npmrc$/i,
|
|
517
|
+
/[/\\]\.docker[/\\]config\.json$/i,
|
|
518
|
+
/[/\\][^/\\]+\.pem$/i,
|
|
519
|
+
/[/\\][^/\\]+\.key$/i,
|
|
520
|
+
/[/\\][^/\\]+\.p12$/i,
|
|
521
|
+
/[/\\][^/\\]+\.pfx$/i,
|
|
522
|
+
/^(?:[a-zA-Z]:)?\/etc\/passwd$/,
|
|
523
|
+
/^(?:[a-zA-Z]:)?\/etc\/shadow$/,
|
|
524
|
+
/^(?:[a-zA-Z]:)?\/etc\/sudoers$/,
|
|
525
|
+
/[/\\]credentials\.json$/i,
|
|
526
|
+
/[/\\]id_rsa$/i,
|
|
527
|
+
/[/\\]id_ed25519$/i,
|
|
528
|
+
/[/\\]id_ecdsa$/i
|
|
529
|
+
];
|
|
530
|
+
function matchSensitivePath(resolvedPath, originalPath) {
|
|
531
|
+
const normalised = resolvedPath.replace(/\\/g, "/");
|
|
532
|
+
for (const pattern of SENSITIVE_PATH_PATTERNS) {
|
|
533
|
+
if (pattern.test(normalised)) {
|
|
534
|
+
return {
|
|
535
|
+
patternName: "Sensitive File Path",
|
|
536
|
+
fieldPath: "file_path",
|
|
537
|
+
redactedSample: originalPath,
|
|
538
|
+
severity: "block"
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
function sensitivePathMatch(originalPath) {
|
|
545
|
+
return {
|
|
546
|
+
patternName: "Sensitive File Path",
|
|
547
|
+
fieldPath: "file_path",
|
|
548
|
+
redactedSample: originalPath,
|
|
549
|
+
severity: "block"
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
var SENSITIVE_PATH_REGEXES = SENSITIVE_PATH_PATTERNS;
|
|
553
|
+
function maskSecret(raw, pattern) {
|
|
554
|
+
const match = raw.match(pattern);
|
|
555
|
+
if (!match) return "****";
|
|
556
|
+
const secret = match[0];
|
|
557
|
+
if (secret.length < 8) return "****";
|
|
558
|
+
const prefix = secret.slice(0, 4);
|
|
559
|
+
const suffix = secret.slice(-4);
|
|
560
|
+
const stars = "*".repeat(Math.min(secret.length - 8, 12));
|
|
561
|
+
return `${prefix}${stars}${suffix}`;
|
|
562
|
+
}
|
|
563
|
+
var MAX_DEPTH = 5;
|
|
564
|
+
var MAX_STRING_BYTES = 1e5;
|
|
565
|
+
var MAX_JSON_PARSE_BYTES = 1e4;
|
|
566
|
+
function scanArgs(args, depth = 0, fieldPath = "args") {
|
|
567
|
+
if (depth > MAX_DEPTH || args === null || args === void 0) return null;
|
|
568
|
+
if (Array.isArray(args)) {
|
|
569
|
+
for (let i = 0; i < args.length; i++) {
|
|
570
|
+
const match = scanArgs(args[i], depth + 1, `${fieldPath}[${i}]`);
|
|
571
|
+
if (match) return match;
|
|
572
|
+
}
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
if (typeof args === "object") {
|
|
576
|
+
for (const [key, value] of Object.entries(args)) {
|
|
577
|
+
const match = scanArgs(value, depth + 1, `${fieldPath}.${key}`);
|
|
578
|
+
if (match) return match;
|
|
579
|
+
}
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
582
|
+
if (typeof args === "string") {
|
|
583
|
+
const text = args.length > MAX_STRING_BYTES ? args.slice(0, MAX_STRING_BYTES) : args;
|
|
584
|
+
const textLower = text.toLowerCase();
|
|
585
|
+
const assignmentCtx = isAssignmentContext(text);
|
|
586
|
+
for (const pattern of DLP_PATTERNS) {
|
|
587
|
+
if (pattern.keywords && !pattern.keywords.some((kw) => textLower.includes(kw.toLowerCase()))) {
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
if (pattern.regex.test(text)) {
|
|
591
|
+
const raw = text.match(pattern.regex)?.[0] ?? "";
|
|
592
|
+
if (DLP_STOPWORDS.some((sw) => raw.toLowerCase().includes(sw))) continue;
|
|
593
|
+
if (pattern.minEntropy !== void 0 && shannonEntropy(raw) < pattern.minEntropy) continue;
|
|
594
|
+
const severity = pattern.contextBoost && assignmentCtx ? "block" : pattern.severity;
|
|
595
|
+
return {
|
|
596
|
+
patternName: pattern.name,
|
|
597
|
+
fieldPath,
|
|
598
|
+
redactedSample: maskSecret(text, pattern.regex),
|
|
599
|
+
severity
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
if (text.length < MAX_JSON_PARSE_BYTES) {
|
|
604
|
+
const trimmed = text.trim();
|
|
605
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
606
|
+
try {
|
|
607
|
+
const parsed = JSON.parse(text);
|
|
608
|
+
const inner = scanArgs(parsed, depth + 1, fieldPath);
|
|
609
|
+
if (inner) return inner;
|
|
610
|
+
} catch {
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return null;
|
|
616
|
+
}
|
|
617
|
+
function scanText(text) {
|
|
618
|
+
const t = text.length > MAX_STRING_BYTES ? text.slice(0, MAX_STRING_BYTES) : text;
|
|
619
|
+
const tLower = t.toLowerCase();
|
|
620
|
+
for (const pattern of DLP_PATTERNS) {
|
|
621
|
+
if (pattern.keywords && !pattern.keywords.some((kw) => tLower.includes(kw.toLowerCase()))) {
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
if (pattern.regex.test(t)) {
|
|
625
|
+
const raw = t.match(pattern.regex)?.[0] ?? "";
|
|
626
|
+
if (DLP_STOPWORDS.some((sw) => raw.toLowerCase().includes(sw))) continue;
|
|
627
|
+
if (pattern.minEntropy !== void 0 && shannonEntropy(raw) < pattern.minEntropy) continue;
|
|
628
|
+
return {
|
|
629
|
+
patternName: pattern.name,
|
|
630
|
+
fieldPath: "response-text",
|
|
631
|
+
redactedSample: maskSecret(t, pattern.regex),
|
|
632
|
+
severity: pattern.severity
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
return null;
|
|
637
|
+
}
|
|
638
|
+
function redactText(text) {
|
|
639
|
+
const t = text.length > MAX_STRING_BYTES ? text.slice(0, MAX_STRING_BYTES) : text;
|
|
640
|
+
let result = t;
|
|
641
|
+
const found = [];
|
|
642
|
+
const lower = t.toLowerCase();
|
|
643
|
+
for (const { pattern, globalRegex } of DLP_PATTERNS_GLOBAL) {
|
|
644
|
+
if (pattern.keywords && !pattern.keywords.some((kw) => lower.includes(kw.toLowerCase()))) {
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
result = result.replace(globalRegex, (match) => {
|
|
648
|
+
if (DLP_STOPWORDS.some((sw) => match.toLowerCase().includes(sw))) return match;
|
|
649
|
+
if (pattern.minEntropy !== void 0 && shannonEntropy(match) < pattern.minEntropy)
|
|
650
|
+
return match;
|
|
651
|
+
if (!found.includes(pattern.name)) found.push(pattern.name);
|
|
652
|
+
return `[node9-redacted:${pattern.name}]`;
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
return { result, found };
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// src/shell/index.ts
|
|
659
|
+
var import_mvdan_sh = __toESM(require("mvdan-sh"));
|
|
660
|
+
var { syntax } = import_mvdan_sh.default;
|
|
661
|
+
var sharedParser = syntax.NewParser();
|
|
662
|
+
var MESSAGE_FLAGS = /* @__PURE__ */ new Set([
|
|
663
|
+
"-m",
|
|
664
|
+
"--message",
|
|
665
|
+
"--body",
|
|
666
|
+
"--title",
|
|
667
|
+
"--description",
|
|
668
|
+
"--comment",
|
|
669
|
+
"--subject",
|
|
670
|
+
"--summary"
|
|
671
|
+
]);
|
|
672
|
+
var SHELL_INTERPRETERS = /* @__PURE__ */ new Set(["bash", "sh", "zsh", "fish", "dash", "ksh"]);
|
|
673
|
+
var DOWNLOAD_CMDS = /* @__PURE__ */ new Set(["curl", "wget"]);
|
|
674
|
+
function normalizeCommandForPolicy(command) {
|
|
675
|
+
try {
|
|
676
|
+
const f = sharedParser.Parse(command, "cmd");
|
|
677
|
+
const strips = [];
|
|
678
|
+
syntax.Walk(f, (node) => {
|
|
679
|
+
if (!node) return false;
|
|
680
|
+
const n = node;
|
|
681
|
+
if (syntax.NodeType(n) !== "CallExpr") return true;
|
|
682
|
+
const args = n.Args || [];
|
|
683
|
+
for (let i = 0; i < args.length - 1; i++) {
|
|
684
|
+
const argParts = args[i].Parts || [];
|
|
685
|
+
if (argParts.length !== 1 || syntax.NodeType(argParts[0]) !== "Lit") continue;
|
|
686
|
+
const flagVal = argParts[0].Value || "";
|
|
687
|
+
if (!MESSAGE_FLAGS.has(flagVal.toLowerCase())) continue;
|
|
688
|
+
const next = args[i + 1];
|
|
689
|
+
const nextParts = next.Parts || [];
|
|
690
|
+
if (nextParts.length !== 1) continue;
|
|
691
|
+
const quotedNode = nextParts[0];
|
|
692
|
+
const nt = syntax.NodeType(quotedNode);
|
|
693
|
+
if (nt === "SglQuoted") {
|
|
694
|
+
strips.push([next.Pos().Offset(), next.End().Offset()]);
|
|
695
|
+
} else if (nt === "DblQuoted") {
|
|
696
|
+
const innerParts = quotedNode.Parts || [];
|
|
697
|
+
const allLit = innerParts.length === 0 || innerParts.every((p) => syntax.NodeType(p) === "Lit");
|
|
698
|
+
if (allLit) strips.push([next.Pos().Offset(), next.End().Offset()]);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
return true;
|
|
702
|
+
});
|
|
703
|
+
if (strips.length === 0) return command;
|
|
704
|
+
strips.sort((a, b) => b[0] - a[0]);
|
|
705
|
+
let result = command;
|
|
706
|
+
for (const [start, end] of strips) {
|
|
707
|
+
result = result.slice(0, start) + '""' + result.slice(end);
|
|
708
|
+
}
|
|
709
|
+
return result;
|
|
710
|
+
} catch {
|
|
711
|
+
return command;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
function scanArgsForDynamicExec(args, startIdx) {
|
|
715
|
+
let hasCmdSubst = false;
|
|
716
|
+
let hasParamExp = false;
|
|
717
|
+
let hasCurl = false;
|
|
718
|
+
for (let i = startIdx; i < args.length; i++) {
|
|
719
|
+
syntax.Walk(args[i], (inner) => {
|
|
720
|
+
if (!inner) return false;
|
|
721
|
+
const inn = inner;
|
|
722
|
+
const it = syntax.NodeType(inn);
|
|
723
|
+
if (it === "CmdSubst") hasCmdSubst = true;
|
|
724
|
+
if (it === "ParamExp") hasParamExp = true;
|
|
725
|
+
if (it === "Lit" && DOWNLOAD_CMDS.has(inn.Value?.toLowerCase())) hasCurl = true;
|
|
726
|
+
return true;
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
if (hasCmdSubst && hasCurl) return "block";
|
|
730
|
+
if (hasCmdSubst || hasParamExp) return "review";
|
|
731
|
+
return null;
|
|
732
|
+
}
|
|
733
|
+
function detectDangerousShellExec(command) {
|
|
734
|
+
try {
|
|
735
|
+
const f = sharedParser.Parse(command, "cmd");
|
|
736
|
+
let result = null;
|
|
737
|
+
syntax.Walk(f, (node) => {
|
|
738
|
+
if (!node || result === "block") return false;
|
|
739
|
+
const n = node;
|
|
740
|
+
if (syntax.NodeType(n) !== "CallExpr") return true;
|
|
741
|
+
const args = n.Args || [];
|
|
742
|
+
if (args.length === 0) return true;
|
|
743
|
+
const firstParts = args[0].Parts || [];
|
|
744
|
+
if (firstParts.length !== 1 || syntax.NodeType(firstParts[0]) !== "Lit") return true;
|
|
745
|
+
const cmdName = firstParts[0].Value?.toLowerCase() ?? "";
|
|
746
|
+
if (cmdName === "eval") {
|
|
747
|
+
const v = scanArgsForDynamicExec(args, 1);
|
|
748
|
+
if (v === "block" || v === "review" && result === null) result = v;
|
|
749
|
+
} else if (SHELL_INTERPRETERS.has(cmdName)) {
|
|
750
|
+
for (let i = 1; i < args.length - 1; i++) {
|
|
751
|
+
const flagParts = args[i].Parts || [];
|
|
752
|
+
if (flagParts.length !== 1 || syntax.NodeType(flagParts[0]) !== "Lit" || flagParts[0].Value !== "-c")
|
|
753
|
+
continue;
|
|
754
|
+
const v = scanArgsForDynamicExec(args, i + 1);
|
|
755
|
+
if (v === "block" || v === "review" && result === null) result = v;
|
|
756
|
+
break;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
return true;
|
|
760
|
+
});
|
|
761
|
+
return result;
|
|
762
|
+
} catch {
|
|
763
|
+
return null;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
var detectDangerousEval = detectDangerousShellExec;
|
|
767
|
+
function analyzeShellCommand(command) {
|
|
768
|
+
const actions = [];
|
|
769
|
+
const paths = [];
|
|
770
|
+
const allTokens = [];
|
|
771
|
+
const addToken = (token) => {
|
|
772
|
+
const lower = token.toLowerCase();
|
|
773
|
+
allTokens.push(lower);
|
|
774
|
+
if (lower.includes("/")) allTokens.push(...lower.split("/").filter(Boolean));
|
|
775
|
+
if (lower.startsWith("-")) allTokens.push(lower.replace(/^-+/, ""));
|
|
776
|
+
};
|
|
777
|
+
try {
|
|
778
|
+
const f = sharedParser.Parse(command, "cmd");
|
|
779
|
+
syntax.Walk(f, (node) => {
|
|
780
|
+
if (!node) return false;
|
|
781
|
+
const n = node;
|
|
782
|
+
if (syntax.NodeType(n) !== "CallExpr") return true;
|
|
783
|
+
const wordValues = (n.Args || []).map((arg) => {
|
|
784
|
+
return (arg.Parts || []).map((p) => (p.Value ?? "").replace(/\\(.)/g, "$1")).join("");
|
|
785
|
+
}).filter((s) => s.length > 0);
|
|
786
|
+
if (wordValues.length > 0) {
|
|
787
|
+
const cmd = wordValues[0].toLowerCase();
|
|
788
|
+
if (!actions.includes(cmd)) actions.push(cmd);
|
|
789
|
+
wordValues.forEach((w) => addToken(w));
|
|
790
|
+
wordValues.slice(1).forEach((w) => {
|
|
791
|
+
if (!w.startsWith("-")) paths.push(w);
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
return true;
|
|
795
|
+
});
|
|
796
|
+
} catch {
|
|
797
|
+
}
|
|
798
|
+
if (allTokens.length === 0) {
|
|
799
|
+
const normalized = command.replace(/\\(.)/g, "$1");
|
|
800
|
+
const sanitized = normalized.replace(/["'<>]/g, " ");
|
|
801
|
+
const segments = sanitized.split(/[|;&]|\$\(|\)|`/);
|
|
802
|
+
segments.forEach((segment) => {
|
|
803
|
+
const tokens = segment.trim().split(/\s+/).filter(Boolean);
|
|
804
|
+
if (tokens.length > 0) {
|
|
805
|
+
const action = tokens[0].toLowerCase();
|
|
806
|
+
if (!actions.includes(action)) actions.push(action);
|
|
807
|
+
tokens.forEach((t) => {
|
|
808
|
+
addToken(t);
|
|
809
|
+
if (t !== tokens[0] && !t.startsWith("-")) {
|
|
810
|
+
if (!paths.includes(t)) paths.push(t);
|
|
811
|
+
}
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
return { actions, paths, allTokens };
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// src/policy/pipe-chain.ts
|
|
820
|
+
var SOURCE_COMMANDS = /* @__PURE__ */ new Set([
|
|
821
|
+
"cat",
|
|
822
|
+
"head",
|
|
823
|
+
"tail",
|
|
824
|
+
"grep",
|
|
825
|
+
"awk",
|
|
826
|
+
"sed",
|
|
827
|
+
"cut",
|
|
828
|
+
"sort",
|
|
829
|
+
"tee",
|
|
830
|
+
"less",
|
|
831
|
+
"more",
|
|
832
|
+
"strings",
|
|
833
|
+
"xxd"
|
|
834
|
+
]);
|
|
835
|
+
var SINK_COMMANDS = /* @__PURE__ */ new Set([
|
|
836
|
+
"curl",
|
|
837
|
+
"wget",
|
|
838
|
+
"nc",
|
|
839
|
+
"ncat",
|
|
840
|
+
"netcat",
|
|
841
|
+
"ssh",
|
|
842
|
+
"scp",
|
|
843
|
+
"rsync",
|
|
844
|
+
"socat",
|
|
845
|
+
"ftp",
|
|
846
|
+
"sftp",
|
|
847
|
+
"telnet"
|
|
848
|
+
]);
|
|
849
|
+
var OBFUSCATORS = /* @__PURE__ */ new Set([
|
|
850
|
+
"base64",
|
|
851
|
+
"gzip",
|
|
852
|
+
"gunzip",
|
|
853
|
+
"bzip2",
|
|
854
|
+
"xz",
|
|
855
|
+
"zstd",
|
|
856
|
+
"openssl",
|
|
857
|
+
"gpg",
|
|
858
|
+
"python",
|
|
859
|
+
"python3",
|
|
860
|
+
"perl",
|
|
861
|
+
"ruby",
|
|
862
|
+
"node"
|
|
863
|
+
]);
|
|
864
|
+
var SENSITIVE_PATTERNS = [
|
|
865
|
+
/(?:^|\/)\.env(?:\.|$)/i,
|
|
866
|
+
// .env, .env.local, .env.production
|
|
867
|
+
/id_rsa|id_ed25519|id_ecdsa|id_dsa/i,
|
|
868
|
+
// SSH private keys
|
|
869
|
+
/\.pem$|\.key$|\.p12$|\.pfx$/i,
|
|
870
|
+
// certificate files
|
|
871
|
+
/(?:^|\/)\.ssh\//i,
|
|
872
|
+
// ~/.ssh/ directory
|
|
873
|
+
/(?:^|\/)\.aws\/credentials/i,
|
|
874
|
+
// AWS credentials
|
|
875
|
+
/(?:^|\/)\.netrc$/i,
|
|
876
|
+
// netrc (stores HTTP credentials)
|
|
877
|
+
/(?:^|\/)(passwd|shadow|sudoers)$/i,
|
|
878
|
+
// /etc/passwd, /etc/shadow
|
|
879
|
+
/(?:^|\/)credentials(?:\.json)?$/i
|
|
880
|
+
// generic credentials files
|
|
881
|
+
];
|
|
882
|
+
function isSensitivePath(p) {
|
|
883
|
+
return SENSITIVE_PATTERNS.some((re) => re.test(p));
|
|
884
|
+
}
|
|
885
|
+
function splitOnPipe(cmd) {
|
|
886
|
+
const segments = [];
|
|
887
|
+
let current = "";
|
|
888
|
+
let inSingle = false;
|
|
889
|
+
let inDouble = false;
|
|
890
|
+
for (let i = 0; i < cmd.length; i++) {
|
|
891
|
+
const ch = cmd[i];
|
|
892
|
+
if (ch === "'" && !inDouble) {
|
|
893
|
+
inSingle = !inSingle;
|
|
894
|
+
current += ch;
|
|
895
|
+
} else if (ch === '"' && !inSingle) {
|
|
896
|
+
inDouble = !inDouble;
|
|
897
|
+
current += ch;
|
|
898
|
+
} else if (ch === "|" && !inSingle && !inDouble && cmd[i + 1] !== "|" && (i === 0 || cmd[i - 1] !== "|")) {
|
|
899
|
+
segments.push(current.trim());
|
|
900
|
+
current = "";
|
|
901
|
+
} else {
|
|
902
|
+
current += ch;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
if (current.trim()) segments.push(current.trim());
|
|
906
|
+
return segments.filter(Boolean);
|
|
907
|
+
}
|
|
908
|
+
function positionalTokens(segment) {
|
|
909
|
+
return segment.split(/\s+/).slice(1).filter((t) => !t.startsWith("-") && !t.startsWith("@") && t.length > 0);
|
|
910
|
+
}
|
|
911
|
+
function analyzePipeChain(command) {
|
|
912
|
+
const segments = splitOnPipe(command);
|
|
913
|
+
if (segments.length < 2) {
|
|
914
|
+
return {
|
|
915
|
+
isPipeline: false,
|
|
916
|
+
hasSensitiveSource: false,
|
|
917
|
+
hasExternalSink: false,
|
|
918
|
+
hasObfuscation: false,
|
|
919
|
+
sourceFiles: [],
|
|
920
|
+
sinkTargets: [],
|
|
921
|
+
risk: "none"
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
const sourceFiles = [];
|
|
925
|
+
const sinkTargets = [];
|
|
926
|
+
let hasSensitiveSource = false;
|
|
927
|
+
let hasExternalSink = false;
|
|
928
|
+
let hasObfuscation = false;
|
|
929
|
+
for (const segment of segments) {
|
|
930
|
+
const tokens = segment.split(/\s+/).filter(Boolean);
|
|
931
|
+
if (tokens.length === 0) continue;
|
|
932
|
+
const binary = tokens[0].toLowerCase();
|
|
933
|
+
const args = positionalTokens(segment);
|
|
934
|
+
if (SOURCE_COMMANDS.has(binary)) {
|
|
935
|
+
sourceFiles.push(...args);
|
|
936
|
+
if (args.some(isSensitivePath)) hasSensitiveSource = true;
|
|
937
|
+
}
|
|
938
|
+
if (OBFUSCATORS.has(binary)) hasObfuscation = true;
|
|
939
|
+
if (SINK_COMMANDS.has(binary)) {
|
|
940
|
+
const targets = args.filter(
|
|
941
|
+
(a) => a.includes(".") || a.includes("://") || /^\d+\.\d+/.test(a)
|
|
942
|
+
);
|
|
943
|
+
sinkTargets.push(...targets);
|
|
944
|
+
if (targets.length > 0) hasExternalSink = true;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
const fullCmd = command.toLowerCase();
|
|
948
|
+
if (!hasSensitiveSource) {
|
|
949
|
+
const redirMatch = fullCmd.match(/<\s*(\S+)/);
|
|
950
|
+
if (redirMatch && isSensitivePath(redirMatch[1])) {
|
|
951
|
+
hasSensitiveSource = true;
|
|
952
|
+
sourceFiles.push(redirMatch[1]);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
const risk = hasSensitiveSource && hasExternalSink && hasObfuscation ? "critical" : hasSensitiveSource && hasExternalSink ? "high" : hasExternalSink ? "medium" : "none";
|
|
956
|
+
return {
|
|
957
|
+
isPipeline: true,
|
|
958
|
+
hasSensitiveSource,
|
|
959
|
+
hasExternalSink,
|
|
960
|
+
hasObfuscation,
|
|
961
|
+
sourceFiles,
|
|
962
|
+
sinkTargets,
|
|
963
|
+
risk
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// src/policy/flag-tables.ts
|
|
968
|
+
function basename(p) {
|
|
969
|
+
const segments = p.split(/[\\/]/);
|
|
970
|
+
return segments[segments.length - 1] || "";
|
|
971
|
+
}
|
|
972
|
+
var FLAGS_WITH_VALUES = {
|
|
973
|
+
curl: /* @__PURE__ */ new Set([
|
|
974
|
+
"-H",
|
|
975
|
+
"--header",
|
|
976
|
+
"-A",
|
|
977
|
+
"--user-agent",
|
|
978
|
+
"-e",
|
|
979
|
+
"--referer",
|
|
980
|
+
"-x",
|
|
981
|
+
"--proxy",
|
|
982
|
+
"-u",
|
|
983
|
+
"--user",
|
|
984
|
+
"-d",
|
|
985
|
+
"--data",
|
|
986
|
+
"--data-raw",
|
|
987
|
+
"--data-binary",
|
|
988
|
+
"-o",
|
|
989
|
+
"--output",
|
|
990
|
+
"-F",
|
|
991
|
+
"--form",
|
|
992
|
+
"--connect-to",
|
|
993
|
+
"--resolve",
|
|
994
|
+
"--cacert",
|
|
995
|
+
"--cert",
|
|
996
|
+
"--key",
|
|
997
|
+
"-m",
|
|
998
|
+
"--max-time"
|
|
999
|
+
]),
|
|
1000
|
+
wget: /* @__PURE__ */ new Set([
|
|
1001
|
+
"-O",
|
|
1002
|
+
"--output-document",
|
|
1003
|
+
"-P",
|
|
1004
|
+
"--directory-prefix",
|
|
1005
|
+
"-U",
|
|
1006
|
+
"--user-agent",
|
|
1007
|
+
"-e",
|
|
1008
|
+
"--execute",
|
|
1009
|
+
"--proxy",
|
|
1010
|
+
"--ca-certificate"
|
|
1011
|
+
]),
|
|
1012
|
+
nc: /* @__PURE__ */ new Set(["-x", "-p", "-s", "-w", "-W", "-I", "-O"]),
|
|
1013
|
+
ncat: /* @__PURE__ */ new Set(["-x", "-p", "-s", "--proxy", "--proxy-auth", "-w", "--wait"]),
|
|
1014
|
+
netcat: /* @__PURE__ */ new Set(["-x", "-p", "-s", "-w"]),
|
|
1015
|
+
ssh: /* @__PURE__ */ new Set([
|
|
1016
|
+
"-i",
|
|
1017
|
+
"-l",
|
|
1018
|
+
"-p",
|
|
1019
|
+
"-o",
|
|
1020
|
+
"-E",
|
|
1021
|
+
"-F",
|
|
1022
|
+
"-J",
|
|
1023
|
+
"-L",
|
|
1024
|
+
"-R",
|
|
1025
|
+
"-W",
|
|
1026
|
+
"-b",
|
|
1027
|
+
"-c",
|
|
1028
|
+
"-D",
|
|
1029
|
+
"-e",
|
|
1030
|
+
"-I",
|
|
1031
|
+
"-S"
|
|
1032
|
+
]),
|
|
1033
|
+
scp: /* @__PURE__ */ new Set(["-i", "-o", "-P", "-S"]),
|
|
1034
|
+
rsync: /* @__PURE__ */ new Set(["-e", "--rsh", "--rsync-path", "--password-file", "--log-file"]),
|
|
1035
|
+
socat: /* @__PURE__ */ new Set([])
|
|
1036
|
+
// socat uses address syntax, not flags — no value-flags
|
|
1037
|
+
};
|
|
1038
|
+
function extractPositionalArgs(tokens, binary) {
|
|
1039
|
+
const binaryName = basename(binary).replace(/\.exe$/i, "");
|
|
1040
|
+
const flagsWithValues = FLAGS_WITH_VALUES[binaryName] ?? /* @__PURE__ */ new Set();
|
|
1041
|
+
const positional = [];
|
|
1042
|
+
let skipNext = false;
|
|
1043
|
+
for (const token of tokens) {
|
|
1044
|
+
if (skipNext) {
|
|
1045
|
+
skipNext = false;
|
|
1046
|
+
continue;
|
|
1047
|
+
}
|
|
1048
|
+
if (token.startsWith("--") && token.includes("=")) continue;
|
|
1049
|
+
if (token.startsWith("-") && token.length === 2 && flagsWithValues.has(token)) {
|
|
1050
|
+
skipNext = true;
|
|
1051
|
+
continue;
|
|
1052
|
+
}
|
|
1053
|
+
if (token.startsWith("--") && flagsWithValues.has(token)) {
|
|
1054
|
+
skipNext = true;
|
|
1055
|
+
continue;
|
|
1056
|
+
}
|
|
1057
|
+
const shortFlag = token.slice(0, 2);
|
|
1058
|
+
if (token.startsWith("-") && token.length > 2 && flagsWithValues.has(shortFlag)) continue;
|
|
1059
|
+
if (token.startsWith("-")) continue;
|
|
1060
|
+
if (token.startsWith("@")) continue;
|
|
1061
|
+
positional.push(token);
|
|
1062
|
+
}
|
|
1063
|
+
return positional;
|
|
1064
|
+
}
|
|
1065
|
+
function extractNetworkTargets(tokens, binary) {
|
|
1066
|
+
return extractPositionalArgs(tokens, binary).map((t) => t.includes("@") ? t.split("@")[1] : t).map((t) => {
|
|
1067
|
+
const colonIdx = t.indexOf(":");
|
|
1068
|
+
if (colonIdx === -1) return t;
|
|
1069
|
+
const afterColon = t.slice(colonIdx + 1);
|
|
1070
|
+
if (/^\d+$/.test(afterColon)) return t.slice(0, colonIdx);
|
|
1071
|
+
return t;
|
|
1072
|
+
}).filter(Boolean);
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// src/policy/ssh-parser.ts
|
|
1076
|
+
function tokenize(cmd) {
|
|
1077
|
+
const tokens = [];
|
|
1078
|
+
let current = "";
|
|
1079
|
+
let inSingle = false;
|
|
1080
|
+
let inDouble = false;
|
|
1081
|
+
for (const ch of cmd) {
|
|
1082
|
+
if (ch === "'" && !inDouble) {
|
|
1083
|
+
inSingle = !inSingle;
|
|
1084
|
+
} else if (ch === '"' && !inSingle) {
|
|
1085
|
+
inDouble = !inDouble;
|
|
1086
|
+
} else if ((ch === " " || ch === " ") && !inSingle && !inDouble) {
|
|
1087
|
+
if (current) {
|
|
1088
|
+
tokens.push(current);
|
|
1089
|
+
current = "";
|
|
1090
|
+
}
|
|
1091
|
+
} else {
|
|
1092
|
+
current += ch;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
if (current) tokens.push(current);
|
|
1096
|
+
return tokens;
|
|
1097
|
+
}
|
|
1098
|
+
function parseHost(raw) {
|
|
1099
|
+
return raw.split("@").pop().split(":")[0];
|
|
1100
|
+
}
|
|
1101
|
+
function extractAllSshHosts(tokens) {
|
|
1102
|
+
const hosts = /* @__PURE__ */ new Set();
|
|
1103
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
1104
|
+
const t = tokens[i];
|
|
1105
|
+
if (t === "-J" && tokens[i + 1]) {
|
|
1106
|
+
for (const hop of tokens[++i].split(",")) {
|
|
1107
|
+
const h = parseHost(hop);
|
|
1108
|
+
if (h) hosts.add(h);
|
|
1109
|
+
}
|
|
1110
|
+
continue;
|
|
1111
|
+
}
|
|
1112
|
+
if (t === "-o" && tokens[i + 1]?.toLowerCase().startsWith("proxyjump=")) {
|
|
1113
|
+
const val = tokens[++i].split("=").slice(1).join("=");
|
|
1114
|
+
for (const hop of val.split(",")) {
|
|
1115
|
+
const h = parseHost(hop);
|
|
1116
|
+
if (h) hosts.add(h);
|
|
1117
|
+
}
|
|
1118
|
+
continue;
|
|
1119
|
+
}
|
|
1120
|
+
if (t === "-o" && tokens[i + 1]?.toLowerCase().startsWith("proxycommand=")) {
|
|
1121
|
+
const raw = tokens[++i].split("=").slice(1).join("=").replace(/^['"]|['"]$/g, "");
|
|
1122
|
+
const subTokens = tokenize(raw);
|
|
1123
|
+
const binary = subTokens[0] ?? "";
|
|
1124
|
+
extractNetworkTargets(subTokens.slice(1), binary).forEach((h) => hosts.add(h));
|
|
1125
|
+
extractAllSshHosts(subTokens.slice(1)).forEach((h) => hosts.add(h));
|
|
1126
|
+
continue;
|
|
1127
|
+
}
|
|
1128
|
+
if (!t.startsWith("-")) {
|
|
1129
|
+
const h = parseHost(t);
|
|
1130
|
+
if (h) hosts.add(h);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
return [...hosts].filter(Boolean);
|
|
1134
|
+
}
|
|
1135
|
+
function parseAllSshHostsFromCommand(command) {
|
|
1136
|
+
const tokens = tokenize(command);
|
|
1137
|
+
return extractAllSshHosts(tokens.slice(1));
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
// src/rules/index.ts
|
|
1141
|
+
var import_picomatch = __toESM(require("picomatch"));
|
|
1142
|
+
|
|
1143
|
+
// src/utils/regex.ts
|
|
1144
|
+
var import_safe_regex2 = __toESM(require("safe-regex2"));
|
|
1145
|
+
var MAX_REGEX_LENGTH = 100;
|
|
1146
|
+
var REGEX_CACHE_MAX = 500;
|
|
1147
|
+
var regexCache = /* @__PURE__ */ new Map();
|
|
1148
|
+
function validateRegex(pattern) {
|
|
1149
|
+
if (!pattern) return "Pattern is required";
|
|
1150
|
+
if (pattern.length > MAX_REGEX_LENGTH) return `Pattern exceeds max length of ${MAX_REGEX_LENGTH}`;
|
|
1151
|
+
try {
|
|
1152
|
+
new RegExp(pattern);
|
|
1153
|
+
} catch (e) {
|
|
1154
|
+
return `Invalid regex syntax: ${e.message}`;
|
|
1155
|
+
}
|
|
1156
|
+
if (/\\\d+[*+{]/.test(pattern)) return "Quantified backreferences are forbidden (ReDoS risk)";
|
|
1157
|
+
if (!(0, import_safe_regex2.default)(pattern)) return "Pattern rejected: potential ReDoS vulnerability detected";
|
|
1158
|
+
return null;
|
|
1159
|
+
}
|
|
1160
|
+
function getCompiledRegex(pattern, flags = "") {
|
|
1161
|
+
if (flags && !/^[gimsuy]+$/.test(flags)) return null;
|
|
1162
|
+
const key = `${pattern}\0${flags}`;
|
|
1163
|
+
if (regexCache.has(key)) {
|
|
1164
|
+
const cached = regexCache.get(key);
|
|
1165
|
+
regexCache.delete(key);
|
|
1166
|
+
regexCache.set(key, cached);
|
|
1167
|
+
return cached;
|
|
1168
|
+
}
|
|
1169
|
+
if (validateRegex(pattern) !== null) return null;
|
|
1170
|
+
try {
|
|
1171
|
+
const re = new RegExp(pattern, flags);
|
|
1172
|
+
if (regexCache.size >= REGEX_CACHE_MAX) {
|
|
1173
|
+
const oldest = regexCache.keys().next().value;
|
|
1174
|
+
if (oldest) regexCache.delete(oldest);
|
|
1175
|
+
}
|
|
1176
|
+
regexCache.set(key, re);
|
|
1177
|
+
return re;
|
|
1178
|
+
} catch {
|
|
1179
|
+
return null;
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// src/rules/index.ts
|
|
1184
|
+
function matchesPattern(text, patterns) {
|
|
1185
|
+
const p = Array.isArray(patterns) ? patterns : [patterns];
|
|
1186
|
+
if (p.length === 0) return false;
|
|
1187
|
+
const isMatch = (0, import_picomatch.default)(p, { nocase: true, dot: true });
|
|
1188
|
+
const target = text.toLowerCase();
|
|
1189
|
+
const directMatch = isMatch(target);
|
|
1190
|
+
if (directMatch) return true;
|
|
1191
|
+
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
1192
|
+
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
1193
|
+
}
|
|
1194
|
+
function getNestedValue(obj, path) {
|
|
1195
|
+
if (!obj || typeof obj !== "object") return null;
|
|
1196
|
+
return path.split(".").reduce((prev, curr) => prev?.[curr], obj);
|
|
1197
|
+
}
|
|
1198
|
+
function evaluateSmartConditions(args, rule) {
|
|
1199
|
+
if (!rule.conditions || rule.conditions.length === 0) return true;
|
|
1200
|
+
const mode = rule.conditionMode ?? "all";
|
|
1201
|
+
const results = rule.conditions.map((cond) => {
|
|
1202
|
+
const rawVal = getNestedValue(args, cond.field);
|
|
1203
|
+
const normalized = rawVal !== null && rawVal !== void 0 ? String(rawVal).replace(/\s+/g, " ").trim() : null;
|
|
1204
|
+
const val = cond.field === "command" && normalized !== null ? normalizeCommandForPolicy(normalized) : normalized;
|
|
1205
|
+
switch (cond.op) {
|
|
1206
|
+
case "exists":
|
|
1207
|
+
return val !== null && val !== "";
|
|
1208
|
+
case "notExists":
|
|
1209
|
+
return val === null || val === "";
|
|
1210
|
+
case "contains":
|
|
1211
|
+
return val !== null && cond.value ? val.includes(cond.value) : false;
|
|
1212
|
+
case "notContains":
|
|
1213
|
+
return val !== null && cond.value ? !val.includes(cond.value) : true;
|
|
1214
|
+
case "matches": {
|
|
1215
|
+
if (val === null || !cond.value) return false;
|
|
1216
|
+
const reM = getCompiledRegex(cond.value, cond.flags ?? "");
|
|
1217
|
+
if (!reM) return false;
|
|
1218
|
+
return reM.test(val);
|
|
1219
|
+
}
|
|
1220
|
+
case "notMatches": {
|
|
1221
|
+
if (!cond.value) return false;
|
|
1222
|
+
if (val === null) return true;
|
|
1223
|
+
const reN = getCompiledRegex(cond.value, cond.flags ?? "");
|
|
1224
|
+
if (!reN) return false;
|
|
1225
|
+
return !reN.test(val);
|
|
1226
|
+
}
|
|
1227
|
+
case "matchesGlob":
|
|
1228
|
+
return val !== null && cond.value ? import_picomatch.default.isMatch(val, cond.value) : false;
|
|
1229
|
+
case "notMatchesGlob":
|
|
1230
|
+
return val !== null && cond.value ? !import_picomatch.default.isMatch(val, cond.value) : false;
|
|
1231
|
+
default:
|
|
1232
|
+
return false;
|
|
1233
|
+
}
|
|
1234
|
+
});
|
|
1235
|
+
return mode === "any" ? results.some((r) => r) : results.every((r) => r);
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
// src/policy/index.ts
|
|
1239
|
+
function tokenize2(toolName) {
|
|
1240
|
+
return toolName.toLowerCase().split(/[_.\-\s]+/).filter(Boolean);
|
|
1241
|
+
}
|
|
1242
|
+
function extractShellCommand(toolName, args, toolInspection) {
|
|
1243
|
+
const patterns = Object.keys(toolInspection);
|
|
1244
|
+
const matchingPattern = patterns.find((p) => matchesPattern(toolName, p));
|
|
1245
|
+
if (!matchingPattern) return null;
|
|
1246
|
+
const fieldPath = toolInspection[matchingPattern];
|
|
1247
|
+
const value = getNestedValue(args, fieldPath);
|
|
1248
|
+
return typeof value === "string" ? value : null;
|
|
1249
|
+
}
|
|
1250
|
+
function isSqlTool(toolName, toolInspection) {
|
|
1251
|
+
const patterns = Object.keys(toolInspection);
|
|
1252
|
+
const matchingPattern = patterns.find((p) => matchesPattern(toolName, p));
|
|
1253
|
+
if (!matchingPattern) return false;
|
|
1254
|
+
const fieldName = toolInspection[matchingPattern];
|
|
1255
|
+
return fieldName === "sql" || fieldName === "query";
|
|
1256
|
+
}
|
|
1257
|
+
var SQL_DML_KEYWORDS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "merge", "upsert"]);
|
|
1258
|
+
function checkDangerousSql(sql) {
|
|
1259
|
+
const norm = sql.replace(/\s+/g, " ").trim().toLowerCase();
|
|
1260
|
+
const hasWhere = /\bwhere\b/.test(norm);
|
|
1261
|
+
if (/^delete\s+from\s+\S+/.test(norm) && !hasWhere)
|
|
1262
|
+
return "DELETE without WHERE \u2014 full table wipe";
|
|
1263
|
+
if (/^update\s+\S+\s+set\s+/.test(norm) && !hasWhere)
|
|
1264
|
+
return "UPDATE without WHERE \u2014 updates every row";
|
|
1265
|
+
return null;
|
|
1266
|
+
}
|
|
1267
|
+
async function evaluatePolicy(config, toolName, args, context = {}, hooks = {}) {
|
|
1268
|
+
const { agent, cwd, activeEnvironment } = context;
|
|
1269
|
+
const { checkProvenance, isTrustedHost } = hooks;
|
|
1270
|
+
const wouldBeIgnored = matchesPattern(toolName, config.policy.ignoredTools);
|
|
1271
|
+
if (config.policy.dlp.enabled && (!wouldBeIgnored || config.policy.dlp.scanIgnoredTools)) {
|
|
1272
|
+
const dlpMatch = args !== void 0 ? scanArgs(args) : null;
|
|
1273
|
+
if (dlpMatch) {
|
|
1274
|
+
return {
|
|
1275
|
+
decision: dlpMatch.severity,
|
|
1276
|
+
blockedByLabel: `DLP: ${dlpMatch.patternName}`,
|
|
1277
|
+
reason: `${dlpMatch.patternName} detected in ${dlpMatch.fieldPath}`
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
if (wouldBeIgnored) return { decision: "allow" };
|
|
1282
|
+
if (config.policy.smartRules.length > 0) {
|
|
1283
|
+
const matchedRule = config.policy.smartRules.find(
|
|
1284
|
+
(rule) => matchesPattern(toolName, rule.tool) && evaluateSmartConditions(args, rule)
|
|
1285
|
+
);
|
|
1286
|
+
if (matchedRule) {
|
|
1287
|
+
if (matchedRule.verdict === "allow")
|
|
1288
|
+
return { decision: "allow", ruleName: matchedRule.name ?? matchedRule.tool };
|
|
1289
|
+
return {
|
|
1290
|
+
decision: matchedRule.verdict,
|
|
1291
|
+
blockedByLabel: `Smart Rule: ${matchedRule.name ?? matchedRule.tool}`,
|
|
1292
|
+
reason: matchedRule.reason,
|
|
1293
|
+
tier: 2,
|
|
1294
|
+
ruleName: matchedRule.name ?? matchedRule.tool,
|
|
1295
|
+
...(matchedRule.description ?? matchedRule.reason) && {
|
|
1296
|
+
ruleDescription: matchedRule.description ?? matchedRule.reason
|
|
1297
|
+
},
|
|
1298
|
+
...matchedRule.verdict === "block" && matchedRule.dependsOnState?.length && {
|
|
1299
|
+
dependsOnStatePredicates: matchedRule.dependsOnState
|
|
1300
|
+
},
|
|
1301
|
+
...matchedRule.verdict === "block" && matchedRule.recoveryCommand && {
|
|
1302
|
+
recoveryCommand: matchedRule.recoveryCommand
|
|
1303
|
+
}
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
let allTokens = [];
|
|
1308
|
+
let pathTokens = [];
|
|
1309
|
+
const shellCommand = extractShellCommand(toolName, args, config.policy.toolInspection);
|
|
1310
|
+
if (shellCommand) {
|
|
1311
|
+
const analyzed = analyzeShellCommand(shellCommand);
|
|
1312
|
+
allTokens = analyzed.allTokens;
|
|
1313
|
+
pathTokens = analyzed.paths;
|
|
1314
|
+
const INLINE_EXEC_PATTERN = /^(python3?|bash|sh|zsh|perl|ruby|node|php|lua)\s+(-c|-e|-eval)\s/i;
|
|
1315
|
+
if (INLINE_EXEC_PATTERN.test(shellCommand.trim())) {
|
|
1316
|
+
return {
|
|
1317
|
+
decision: "review",
|
|
1318
|
+
blockedByLabel: "Node9 Standard (Inline Execution)",
|
|
1319
|
+
ruleDescription: "The AI is running code directly from the command line. Review the full script below before allowing it to execute.",
|
|
1320
|
+
tier: 3
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1323
|
+
const evalVerdict = detectDangerousShellExec(shellCommand);
|
|
1324
|
+
if (evalVerdict === "block") {
|
|
1325
|
+
return {
|
|
1326
|
+
decision: "block",
|
|
1327
|
+
blockedByLabel: "Node9: Eval Remote Execution",
|
|
1328
|
+
reason: "eval of remote download (curl/wget) is a near-certain supply-chain attack",
|
|
1329
|
+
ruleDescription: "The AI is downloading a script from the internet and running it immediately without inspection. This is a common way malware gets installed.",
|
|
1330
|
+
tier: 3
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1333
|
+
if (evalVerdict === "review") {
|
|
1334
|
+
return {
|
|
1335
|
+
decision: "review",
|
|
1336
|
+
blockedByLabel: "Node9: Eval Dynamic Content",
|
|
1337
|
+
reason: "eval of dynamic content (variable or subshell expansion) requires approval",
|
|
1338
|
+
ruleDescription: "The AI is running a command that includes a variable or subshell expansion. The actual command executed at runtime may differ from what is shown here.",
|
|
1339
|
+
tier: 3
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1342
|
+
const pipeAnalysis = analyzePipeChain(shellCommand);
|
|
1343
|
+
if (pipeAnalysis.isPipeline && (pipeAnalysis.risk === "critical" || pipeAnalysis.risk === "high")) {
|
|
1344
|
+
const sinks = pipeAnalysis.sinkTargets;
|
|
1345
|
+
const allTrusted = sinks.length > 0 && sinks.every((host) => isTrustedHost ? isTrustedHost(host) : false);
|
|
1346
|
+
if (pipeAnalysis.risk === "critical") {
|
|
1347
|
+
if (allTrusted) {
|
|
1348
|
+
return {
|
|
1349
|
+
decision: "review",
|
|
1350
|
+
blockedByLabel: "Node9: Pipe-Chain to Trusted Host (obfuscated)",
|
|
1351
|
+
reason: `Obfuscated pipe to trusted host(s): ${sinks.join(", ")} \u2014 requires approval`,
|
|
1352
|
+
tier: 3
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
return {
|
|
1356
|
+
decision: "block",
|
|
1357
|
+
blockedByLabel: "Node9: Pipe-Chain Exfiltration (critical)",
|
|
1358
|
+
reason: `Sensitive file piped through obfuscator to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
|
|
1359
|
+
tier: 3
|
|
1360
|
+
};
|
|
1361
|
+
}
|
|
1362
|
+
if (allTrusted) {
|
|
1363
|
+
return {
|
|
1364
|
+
decision: "allow",
|
|
1365
|
+
blockedByLabel: "Node9: Pipe-Chain to Trusted Host",
|
|
1366
|
+
reason: `Sensitive file piped to trusted host(s): ${sinks.join(", ")}`,
|
|
1367
|
+
tier: 3
|
|
1368
|
+
};
|
|
1369
|
+
}
|
|
1370
|
+
return {
|
|
1371
|
+
decision: "review",
|
|
1372
|
+
blockedByLabel: "Node9: Pipe-Chain Exfiltration (high)",
|
|
1373
|
+
reason: `Sensitive file piped to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
|
|
1374
|
+
tier: 3
|
|
1375
|
+
};
|
|
1376
|
+
}
|
|
1377
|
+
const firstToken = analyzed.actions[0] ?? "";
|
|
1378
|
+
if (["ssh", "scp", "rsync"].includes(firstToken)) {
|
|
1379
|
+
const rawTokens = shellCommand.trim().split(/\s+/);
|
|
1380
|
+
const sshHosts = extractAllSshHosts(rawTokens.slice(1));
|
|
1381
|
+
allTokens.push(...sshHosts);
|
|
1382
|
+
}
|
|
1383
|
+
if (firstToken && firstToken.startsWith("/") && checkProvenance) {
|
|
1384
|
+
const prov = checkProvenance(firstToken, cwd);
|
|
1385
|
+
if (prov.trustLevel === "suspect") {
|
|
1386
|
+
return {
|
|
1387
|
+
decision: config.settings.mode === "strict" ? "block" : "review",
|
|
1388
|
+
blockedByLabel: "Node9: Suspect Binary",
|
|
1389
|
+
reason: `Binary "${firstToken}" resolved to ${prov.resolvedPath} \u2014 ${prov.reason}`,
|
|
1390
|
+
tier: 3
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
if (prov.trustLevel === "unknown" && config.settings.mode === "strict") {
|
|
1394
|
+
return {
|
|
1395
|
+
decision: "review",
|
|
1396
|
+
blockedByLabel: "Node9: Unknown Binary (strict mode)",
|
|
1397
|
+
reason: `Binary "${firstToken}" \u2014 ${prov.reason}`,
|
|
1398
|
+
tier: 3
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
if (isSqlTool(toolName, config.policy.toolInspection)) {
|
|
1403
|
+
allTokens = allTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
|
|
1404
|
+
}
|
|
1405
|
+
} else {
|
|
1406
|
+
allTokens = tokenize2(toolName);
|
|
1407
|
+
if (args && typeof args === "object") {
|
|
1408
|
+
const flattenedArgs = JSON.stringify(args).toLowerCase();
|
|
1409
|
+
const extraTokens = flattenedArgs.split(/[^a-zA-Z0-9]+/).filter((t) => t.length > 1);
|
|
1410
|
+
allTokens.push(...extraTokens);
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
const isManual = agent === "Terminal";
|
|
1414
|
+
if (isManual) {
|
|
1415
|
+
const SYSTEM_DISASTER_COMMANDS = ["mkfs", "shred", "dd", "drop", "truncate", "purge"];
|
|
1416
|
+
const hasSystemDisaster = allTokens.some(
|
|
1417
|
+
(t) => SYSTEM_DISASTER_COMMANDS.includes(t.toLowerCase())
|
|
1418
|
+
);
|
|
1419
|
+
const isRootWipe = allTokens.includes("rm") && (allTokens.includes("/") || allTokens.includes("/*"));
|
|
1420
|
+
if (hasSystemDisaster || isRootWipe) {
|
|
1421
|
+
return { decision: "review", blockedByLabel: "Manual Nuclear Protection", tier: 3 };
|
|
1422
|
+
}
|
|
1423
|
+
return { decision: "allow" };
|
|
1424
|
+
}
|
|
1425
|
+
if (pathTokens.length > 0 && config.policy.sandboxPaths.length > 0) {
|
|
1426
|
+
const allInSandbox = pathTokens.every((p) => matchesPattern(p, config.policy.sandboxPaths));
|
|
1427
|
+
if (allInSandbox) return { decision: "allow" };
|
|
1428
|
+
}
|
|
1429
|
+
let matchedDangerousWord;
|
|
1430
|
+
const isDangerous = allTokens.some(
|
|
1431
|
+
(token) => config.policy.dangerousWords.some((word) => {
|
|
1432
|
+
const w = word.toLowerCase();
|
|
1433
|
+
const hit = token === w || (() => {
|
|
1434
|
+
try {
|
|
1435
|
+
return new RegExp(`\\b${w}\\b`, "i").test(token);
|
|
1436
|
+
} catch {
|
|
1437
|
+
return false;
|
|
1438
|
+
}
|
|
1439
|
+
})();
|
|
1440
|
+
if (hit && !matchedDangerousWord) matchedDangerousWord = word;
|
|
1441
|
+
return hit;
|
|
1442
|
+
})
|
|
1443
|
+
);
|
|
1444
|
+
if (isDangerous) {
|
|
1445
|
+
let matchedField;
|
|
1446
|
+
if (matchedDangerousWord && args && typeof args === "object" && !Array.isArray(args)) {
|
|
1447
|
+
const obj = args;
|
|
1448
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1449
|
+
if (typeof value === "string") {
|
|
1450
|
+
try {
|
|
1451
|
+
if (new RegExp(
|
|
1452
|
+
`\\b${matchedDangerousWord.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`,
|
|
1453
|
+
"i"
|
|
1454
|
+
).test(value)) {
|
|
1455
|
+
matchedField = key;
|
|
1456
|
+
break;
|
|
1457
|
+
}
|
|
1458
|
+
} catch {
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
return {
|
|
1464
|
+
decision: "review",
|
|
1465
|
+
blockedByLabel: `Project/Global Config \u2014 dangerous word: "${matchedDangerousWord}"`,
|
|
1466
|
+
matchedWord: matchedDangerousWord,
|
|
1467
|
+
matchedField,
|
|
1468
|
+
ruleDescription: `This command contains a flagged keyword ("${matchedDangerousWord}") from your node9 config. Review it before allowing.`,
|
|
1469
|
+
tier: 6
|
|
1470
|
+
};
|
|
1471
|
+
}
|
|
1472
|
+
if (config.settings.mode === "strict") {
|
|
1473
|
+
if (activeEnvironment?.requireApproval === false) return { decision: "allow" };
|
|
1474
|
+
return { decision: "review", blockedByLabel: "Global Config (Strict Mode Active)", tier: 7 };
|
|
1475
|
+
}
|
|
1476
|
+
return { decision: "allow" };
|
|
1477
|
+
}
|
|
1478
|
+
function isIgnoredTool(toolName, config) {
|
|
1479
|
+
return matchesPattern(toolName, config.policy.ignoredTools);
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
// src/shields/builtin/aws.json
|
|
1483
|
+
var aws_default = {
|
|
1484
|
+
name: "aws",
|
|
1485
|
+
description: "Protects AWS infrastructure from destructive AI operations",
|
|
1486
|
+
aliases: ["amazon"],
|
|
1487
|
+
smartRules: [
|
|
1488
|
+
{
|
|
1489
|
+
name: "shield:aws:block-delete-s3-bucket",
|
|
1490
|
+
tool: "*",
|
|
1491
|
+
conditions: [
|
|
1492
|
+
{
|
|
1493
|
+
field: "command",
|
|
1494
|
+
op: "matches",
|
|
1495
|
+
value: "aws\\s+s3.*rb\\s|aws\\s+s3api\\s+delete-bucket",
|
|
1496
|
+
flags: "i"
|
|
1497
|
+
}
|
|
1498
|
+
],
|
|
1499
|
+
verdict: "block",
|
|
1500
|
+
reason: "S3 bucket deletion is irreversible \u2014 blocked by AWS shield"
|
|
1501
|
+
},
|
|
1502
|
+
{
|
|
1503
|
+
name: "shield:aws:review-iam-changes",
|
|
1504
|
+
tool: "*",
|
|
1505
|
+
conditions: [
|
|
1506
|
+
{
|
|
1507
|
+
field: "command",
|
|
1508
|
+
op: "matches",
|
|
1509
|
+
value: "aws\\s+iam\\s+(create|delete|attach|detach|put|remove)",
|
|
1510
|
+
flags: "i"
|
|
1511
|
+
}
|
|
1512
|
+
],
|
|
1513
|
+
verdict: "review",
|
|
1514
|
+
reason: "IAM changes require human approval (AWS shield)"
|
|
1515
|
+
},
|
|
1516
|
+
{
|
|
1517
|
+
name: "shield:aws:block-ec2-terminate",
|
|
1518
|
+
tool: "*",
|
|
1519
|
+
conditions: [
|
|
1520
|
+
{
|
|
1521
|
+
field: "command",
|
|
1522
|
+
op: "matches",
|
|
1523
|
+
value: "aws\\s+ec2\\s+terminate-instances",
|
|
1524
|
+
flags: "i"
|
|
1525
|
+
}
|
|
1526
|
+
],
|
|
1527
|
+
verdict: "block",
|
|
1528
|
+
reason: "EC2 instance termination is irreversible \u2014 blocked by AWS shield"
|
|
1529
|
+
},
|
|
1530
|
+
{
|
|
1531
|
+
name: "shield:aws:review-rds-delete",
|
|
1532
|
+
tool: "*",
|
|
1533
|
+
conditions: [
|
|
1534
|
+
{ field: "command", op: "matches", value: "aws\\s+rds\\s+delete-", flags: "i" }
|
|
1535
|
+
],
|
|
1536
|
+
verdict: "review",
|
|
1537
|
+
reason: "RDS deletion requires human approval (AWS shield)"
|
|
1538
|
+
}
|
|
1539
|
+
],
|
|
1540
|
+
dangerousWords: []
|
|
1541
|
+
};
|
|
1542
|
+
|
|
1543
|
+
// src/shields/builtin/bash-safe.json
|
|
1544
|
+
var bash_safe_default = {
|
|
1545
|
+
name: "bash-safe",
|
|
1546
|
+
description: "Blocks high-risk bash patterns: pipe-to-shell, rm -rf /, disk overwrites, eval",
|
|
1547
|
+
aliases: ["bash", "shell"],
|
|
1548
|
+
smartRules: [
|
|
1549
|
+
{
|
|
1550
|
+
name: "shield:bash-safe:block-pipe-to-shell",
|
|
1551
|
+
tool: "bash",
|
|
1552
|
+
conditions: [
|
|
1553
|
+
{
|
|
1554
|
+
field: "command",
|
|
1555
|
+
op: "matches",
|
|
1556
|
+
value: "(^|&&|\\|\\||;)\\s*(curl|wget)\\s+[^|]*\\|\\s*(?:(bash|sh|zsh|fish)|(python3?|ruby|perl|node)\\b(?!\\s+-[cem]\\b))",
|
|
1557
|
+
flags: "i"
|
|
1558
|
+
}
|
|
1559
|
+
],
|
|
1560
|
+
verdict: "block",
|
|
1561
|
+
reason: "Pipe-to-shell is a common supply-chain attack vector \u2014 blocked by bash-safe shield"
|
|
1562
|
+
},
|
|
1563
|
+
{
|
|
1564
|
+
name: "shield:bash-safe:block-obfuscated-exec",
|
|
1565
|
+
tool: "bash",
|
|
1566
|
+
conditions: [
|
|
1567
|
+
{
|
|
1568
|
+
field: "command",
|
|
1569
|
+
op: "matches",
|
|
1570
|
+
value: "\\bbase64\\s+(-d|--decode)[^|;&]*\\|\\s*(bash|sh|zsh)",
|
|
1571
|
+
flags: "i"
|
|
1572
|
+
}
|
|
1573
|
+
],
|
|
1574
|
+
verdict: "block",
|
|
1575
|
+
reason: "Obfuscated execution via base64 decode \u2014 blocked by bash-safe shield"
|
|
1576
|
+
},
|
|
1577
|
+
{
|
|
1578
|
+
name: "shield:bash-safe:block-rm-root",
|
|
1579
|
+
tool: "bash",
|
|
1580
|
+
conditions: [
|
|
1581
|
+
{
|
|
1582
|
+
field: "command",
|
|
1583
|
+
op: "matches",
|
|
1584
|
+
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*$",
|
|
1585
|
+
flags: "i"
|
|
1586
|
+
}
|
|
1587
|
+
],
|
|
1588
|
+
verdict: "block",
|
|
1589
|
+
reason: "rm -rf of root or home directory is catastrophic \u2014 blocked by bash-safe shield"
|
|
1590
|
+
},
|
|
1591
|
+
{
|
|
1592
|
+
name: "shield:bash-safe:block-disk-overwrite",
|
|
1593
|
+
tool: "bash",
|
|
1594
|
+
conditions: [
|
|
1595
|
+
{
|
|
1596
|
+
field: "command",
|
|
1597
|
+
op: "matches",
|
|
1598
|
+
value: "(^|&&|\\|\\||;)\\s*dd\\s+.*of=\\/dev\\/(sd|nvme|hd|vd|xvd)",
|
|
1599
|
+
flags: "i"
|
|
1600
|
+
}
|
|
1601
|
+
],
|
|
1602
|
+
verdict: "block",
|
|
1603
|
+
reason: "Writing directly to a block device is irreversible \u2014 blocked by bash-safe shield"
|
|
1604
|
+
},
|
|
1605
|
+
{
|
|
1606
|
+
name: "shield:bash-safe:block-eval-remote",
|
|
1607
|
+
tool: "bash",
|
|
1608
|
+
conditions: [
|
|
1609
|
+
{
|
|
1610
|
+
field: "command",
|
|
1611
|
+
op: "matches",
|
|
1612
|
+
value: "(^|&&|\\|\\||;)\\s*eval\\s+.*\\$\\((curl|wget)\\b",
|
|
1613
|
+
flags: "i"
|
|
1614
|
+
}
|
|
1615
|
+
],
|
|
1616
|
+
verdict: "block",
|
|
1617
|
+
reason: "eval of remote download is a near-certain supply-chain attack \u2014 blocked by bash-safe shield"
|
|
1618
|
+
},
|
|
1619
|
+
{
|
|
1620
|
+
name: "shield:bash-safe:review-eval-dynamic",
|
|
1621
|
+
tool: "bash",
|
|
1622
|
+
conditions: [
|
|
1623
|
+
{
|
|
1624
|
+
field: "command",
|
|
1625
|
+
op: "matches",
|
|
1626
|
+
value: '(^|&&|\\|\\||[;|\\n{(`])\\s*eval\\s+([\\$`(]|"[^"]*\\$)',
|
|
1627
|
+
flags: "i"
|
|
1628
|
+
}
|
|
1629
|
+
],
|
|
1630
|
+
verdict: "review",
|
|
1631
|
+
reason: "eval of dynamic content \u2014 backup regex rule for scan path (real-time uses AST detection)"
|
|
1632
|
+
}
|
|
1633
|
+
],
|
|
1634
|
+
dangerousWords: []
|
|
1635
|
+
};
|
|
1636
|
+
|
|
1637
|
+
// src/shields/builtin/docker.json
|
|
1638
|
+
var docker_default = {
|
|
1639
|
+
name: "docker",
|
|
1640
|
+
description: "Protects Docker environments from destructive AI operations",
|
|
1641
|
+
aliases: [],
|
|
1642
|
+
smartRules: [
|
|
1643
|
+
{
|
|
1644
|
+
name: "shield:docker:block-system-prune",
|
|
1645
|
+
tool: "*",
|
|
1646
|
+
conditions: [
|
|
1647
|
+
{
|
|
1648
|
+
field: "command",
|
|
1649
|
+
op: "matches",
|
|
1650
|
+
value: "docker\\s+system\\s+prune",
|
|
1651
|
+
flags: "i"
|
|
1652
|
+
}
|
|
1653
|
+
],
|
|
1654
|
+
verdict: "block",
|
|
1655
|
+
reason: "docker system prune removes all unused containers, images, and volumes \u2014 blocked by Docker shield"
|
|
1656
|
+
},
|
|
1657
|
+
{
|
|
1658
|
+
name: "shield:docker:block-volume-prune",
|
|
1659
|
+
tool: "*",
|
|
1660
|
+
conditions: [
|
|
1661
|
+
{
|
|
1662
|
+
field: "command",
|
|
1663
|
+
op: "matches",
|
|
1664
|
+
value: "docker\\s+volume\\s+prune",
|
|
1665
|
+
flags: "i"
|
|
1666
|
+
}
|
|
1667
|
+
],
|
|
1668
|
+
verdict: "block",
|
|
1669
|
+
reason: "docker volume prune destroys all unused volumes and their data \u2014 blocked by Docker shield"
|
|
1670
|
+
},
|
|
1671
|
+
{
|
|
1672
|
+
name: "shield:docker:block-rm-force",
|
|
1673
|
+
tool: "*",
|
|
1674
|
+
conditionMode: "all",
|
|
1675
|
+
conditions: [
|
|
1676
|
+
{
|
|
1677
|
+
field: "command",
|
|
1678
|
+
op: "matches",
|
|
1679
|
+
value: "docker\\s+rm\\b",
|
|
1680
|
+
flags: "i"
|
|
1681
|
+
},
|
|
1682
|
+
{
|
|
1683
|
+
field: "command",
|
|
1684
|
+
op: "matches",
|
|
1685
|
+
value: "(^|\\s)(-f|--force)(\\s|$)",
|
|
1686
|
+
flags: "i"
|
|
1687
|
+
}
|
|
1688
|
+
],
|
|
1689
|
+
verdict: "block",
|
|
1690
|
+
reason: "Force-removing running containers is destructive \u2014 blocked by Docker shield"
|
|
1691
|
+
},
|
|
1692
|
+
{
|
|
1693
|
+
name: "shield:docker:review-volume-rm",
|
|
1694
|
+
tool: "*",
|
|
1695
|
+
conditions: [
|
|
1696
|
+
{
|
|
1697
|
+
field: "command",
|
|
1698
|
+
op: "matches",
|
|
1699
|
+
value: "docker\\s+volume\\s+rm\\s+",
|
|
1700
|
+
flags: "i"
|
|
1701
|
+
}
|
|
1702
|
+
],
|
|
1703
|
+
verdict: "review",
|
|
1704
|
+
reason: "Volume removal deletes persistent data and requires human approval (Docker shield)"
|
|
1705
|
+
},
|
|
1706
|
+
{
|
|
1707
|
+
name: "shield:docker:review-stop-kill",
|
|
1708
|
+
tool: "*",
|
|
1709
|
+
conditions: [
|
|
1710
|
+
{
|
|
1711
|
+
field: "command",
|
|
1712
|
+
op: "matches",
|
|
1713
|
+
value: "docker\\s+(stop|kill)\\s+",
|
|
1714
|
+
flags: "i"
|
|
1715
|
+
}
|
|
1716
|
+
],
|
|
1717
|
+
verdict: "review",
|
|
1718
|
+
reason: "Stopping or killing containers requires human approval (Docker shield)"
|
|
1719
|
+
},
|
|
1720
|
+
{
|
|
1721
|
+
name: "shield:docker:review-image-rm",
|
|
1722
|
+
tool: "*",
|
|
1723
|
+
conditions: [
|
|
1724
|
+
{
|
|
1725
|
+
field: "command",
|
|
1726
|
+
op: "matches",
|
|
1727
|
+
value: "docker\\s+image\\s+rm\\b",
|
|
1728
|
+
flags: "i"
|
|
1729
|
+
}
|
|
1730
|
+
],
|
|
1731
|
+
verdict: "review",
|
|
1732
|
+
reason: "Image removal requires human approval (Docker shield)"
|
|
1733
|
+
},
|
|
1734
|
+
{
|
|
1735
|
+
name: "shield:docker:review-rmi-force",
|
|
1736
|
+
tool: "*",
|
|
1737
|
+
conditionMode: "all",
|
|
1738
|
+
conditions: [
|
|
1739
|
+
{
|
|
1740
|
+
field: "command",
|
|
1741
|
+
op: "matches",
|
|
1742
|
+
value: "docker\\s+rmi\\b",
|
|
1743
|
+
flags: "i"
|
|
1744
|
+
},
|
|
1745
|
+
{
|
|
1746
|
+
field: "command",
|
|
1747
|
+
op: "matches",
|
|
1748
|
+
value: "(^|\\s)(-f|--force)(\\s|$)",
|
|
1749
|
+
flags: "i"
|
|
1750
|
+
}
|
|
1751
|
+
],
|
|
1752
|
+
verdict: "review",
|
|
1753
|
+
reason: "Force image removal requires human approval (Docker shield)"
|
|
1754
|
+
}
|
|
1755
|
+
],
|
|
1756
|
+
dangerousWords: []
|
|
1757
|
+
};
|
|
1758
|
+
|
|
1759
|
+
// src/shields/builtin/filesystem.json
|
|
1760
|
+
var filesystem_default = {
|
|
1761
|
+
name: "filesystem",
|
|
1762
|
+
description: "Protects the local filesystem from dangerous AI operations",
|
|
1763
|
+
aliases: ["fs"],
|
|
1764
|
+
smartRules: [
|
|
1765
|
+
{
|
|
1766
|
+
name: "shield:filesystem:review-chmod-777",
|
|
1767
|
+
tool: "bash",
|
|
1768
|
+
conditions: [
|
|
1769
|
+
{ field: "command", op: "matches", value: "chmod\\s+(777|a\\+rwx)", flags: "i" }
|
|
1770
|
+
],
|
|
1771
|
+
verdict: "review",
|
|
1772
|
+
reason: "chmod 777 requires human approval (filesystem shield)"
|
|
1773
|
+
},
|
|
1774
|
+
{
|
|
1775
|
+
name: "shield:filesystem:review-write-etc",
|
|
1776
|
+
tool: "bash",
|
|
1777
|
+
conditions: [
|
|
1778
|
+
{
|
|
1779
|
+
field: "command",
|
|
1780
|
+
op: "matches",
|
|
1781
|
+
value: "(tee|\\bcp\\b|\\bmv\\b|install|>+)\\s+.*\\/etc\\/"
|
|
1782
|
+
}
|
|
1783
|
+
],
|
|
1784
|
+
verdict: "review",
|
|
1785
|
+
reason: "Writing to /etc requires human approval (filesystem shield)"
|
|
1786
|
+
}
|
|
1787
|
+
],
|
|
1788
|
+
dangerousWords: ["wipefs"]
|
|
1789
|
+
};
|
|
1790
|
+
|
|
1791
|
+
// src/shields/builtin/github.json
|
|
1792
|
+
var github_default = {
|
|
1793
|
+
name: "github",
|
|
1794
|
+
description: "Protects GitHub repositories from destructive AI operations",
|
|
1795
|
+
aliases: ["git"],
|
|
1796
|
+
smartRules: [
|
|
1797
|
+
{
|
|
1798
|
+
name: "shield:github:review-delete-branch-remote",
|
|
1799
|
+
tool: "bash",
|
|
1800
|
+
conditions: [
|
|
1801
|
+
{ field: "command", op: "matches", value: "git\\s+push\\s+.*--delete", flags: "i" }
|
|
1802
|
+
],
|
|
1803
|
+
verdict: "review",
|
|
1804
|
+
reason: "Remote branch deletion requires human approval (GitHub shield)"
|
|
1805
|
+
},
|
|
1806
|
+
{
|
|
1807
|
+
name: "shield:github:block-delete-repo",
|
|
1808
|
+
tool: "*",
|
|
1809
|
+
conditions: [
|
|
1810
|
+
{ field: "command", op: "matches", value: "gh\\s+repo\\s+delete", flags: "i" }
|
|
1811
|
+
],
|
|
1812
|
+
verdict: "block",
|
|
1813
|
+
reason: "Repository deletion is irreversible \u2014 blocked by GitHub shield"
|
|
1814
|
+
}
|
|
1815
|
+
],
|
|
1816
|
+
dangerousWords: []
|
|
1817
|
+
};
|
|
1818
|
+
|
|
1819
|
+
// src/shields/builtin/k8s.json
|
|
1820
|
+
var k8s_default = {
|
|
1821
|
+
name: "k8s",
|
|
1822
|
+
description: "Protects Kubernetes clusters from destructive AI operations",
|
|
1823
|
+
aliases: ["kubernetes", "kubectl"],
|
|
1824
|
+
smartRules: [
|
|
1825
|
+
{
|
|
1826
|
+
name: "shield:k8s:block-delete-namespace",
|
|
1827
|
+
tool: "*",
|
|
1828
|
+
conditions: [
|
|
1829
|
+
{
|
|
1830
|
+
field: "command",
|
|
1831
|
+
op: "matches",
|
|
1832
|
+
value: "kubectl\\s+delete\\s+(ns|namespace)\\s+",
|
|
1833
|
+
flags: "i"
|
|
1834
|
+
}
|
|
1835
|
+
],
|
|
1836
|
+
verdict: "block",
|
|
1837
|
+
reason: "Deleting a namespace destroys all resources inside it \u2014 blocked by k8s shield"
|
|
1838
|
+
},
|
|
1839
|
+
{
|
|
1840
|
+
name: "shield:k8s:block-delete-all",
|
|
1841
|
+
tool: "*",
|
|
1842
|
+
conditions: [
|
|
1843
|
+
{
|
|
1844
|
+
field: "command",
|
|
1845
|
+
op: "matches",
|
|
1846
|
+
value: "kubectl\\s+delete\\s+.*--all\\b",
|
|
1847
|
+
flags: "i"
|
|
1848
|
+
}
|
|
1849
|
+
],
|
|
1850
|
+
verdict: "block",
|
|
1851
|
+
reason: "kubectl delete --all is irreversible \u2014 blocked by k8s shield"
|
|
1852
|
+
},
|
|
1853
|
+
{
|
|
1854
|
+
name: "shield:k8s:block-helm-uninstall",
|
|
1855
|
+
tool: "*",
|
|
1856
|
+
conditions: [
|
|
1857
|
+
{
|
|
1858
|
+
field: "command",
|
|
1859
|
+
op: "matches",
|
|
1860
|
+
value: "helm\\s+(uninstall|delete|del)\\s+",
|
|
1861
|
+
flags: "i"
|
|
1862
|
+
}
|
|
1863
|
+
],
|
|
1864
|
+
verdict: "block",
|
|
1865
|
+
reason: "helm uninstall removes a release and its resources \u2014 blocked by k8s shield"
|
|
1866
|
+
},
|
|
1867
|
+
{
|
|
1868
|
+
name: "shield:k8s:review-scale-zero",
|
|
1869
|
+
tool: "*",
|
|
1870
|
+
conditions: [
|
|
1871
|
+
{
|
|
1872
|
+
field: "command",
|
|
1873
|
+
op: "matches",
|
|
1874
|
+
value: "kubectl\\s+scale\\s+.*--replicas=0",
|
|
1875
|
+
flags: "i"
|
|
1876
|
+
}
|
|
1877
|
+
],
|
|
1878
|
+
verdict: "review",
|
|
1879
|
+
reason: "Scaling to zero takes down a workload and requires human approval (k8s shield)"
|
|
1880
|
+
},
|
|
1881
|
+
{
|
|
1882
|
+
name: "shield:k8s:review-delete-deployment",
|
|
1883
|
+
tool: "*",
|
|
1884
|
+
conditions: [
|
|
1885
|
+
{
|
|
1886
|
+
field: "command",
|
|
1887
|
+
op: "matches",
|
|
1888
|
+
value: "kubectl\\s+delete\\s+(deployment|deploy|statefulset|sts|daemonset|ds)\\s+",
|
|
1889
|
+
flags: "i"
|
|
1890
|
+
}
|
|
1891
|
+
],
|
|
1892
|
+
verdict: "review",
|
|
1893
|
+
reason: "Deleting a workload requires human approval (k8s shield)"
|
|
1894
|
+
},
|
|
1895
|
+
{
|
|
1896
|
+
name: "shield:k8s:review-apply-force",
|
|
1897
|
+
tool: "*",
|
|
1898
|
+
conditions: [
|
|
1899
|
+
{
|
|
1900
|
+
field: "command",
|
|
1901
|
+
op: "matches",
|
|
1902
|
+
value: "kubectl\\s+(apply|replace)\\s+.*--force",
|
|
1903
|
+
flags: "i"
|
|
1904
|
+
}
|
|
1905
|
+
],
|
|
1906
|
+
verdict: "review",
|
|
1907
|
+
reason: "Force-apply overwrites live resources and requires human approval (k8s shield)"
|
|
1908
|
+
}
|
|
1909
|
+
],
|
|
1910
|
+
dangerousWords: []
|
|
1911
|
+
};
|
|
1912
|
+
|
|
1913
|
+
// src/shields/builtin/mcp-tool-gating.json
|
|
1914
|
+
var mcp_tool_gating_default = {
|
|
1915
|
+
name: "mcp-tool-gating",
|
|
1916
|
+
description: "Intercept MCP tool lists and require user approval before the agent can use any tools from a new server",
|
|
1917
|
+
aliases: ["mcp-gating", "mcp-tools"],
|
|
1918
|
+
smartRules: [],
|
|
1919
|
+
dangerousWords: []
|
|
1920
|
+
};
|
|
1921
|
+
|
|
1922
|
+
// src/shields/builtin/mongodb.json
|
|
1923
|
+
var mongodb_default = {
|
|
1924
|
+
name: "mongodb",
|
|
1925
|
+
description: "Protects MongoDB databases from destructive AI operations",
|
|
1926
|
+
aliases: ["mongo"],
|
|
1927
|
+
smartRules: [
|
|
1928
|
+
{
|
|
1929
|
+
name: "shield:mongodb:block-drop-database",
|
|
1930
|
+
tool: "*",
|
|
1931
|
+
conditions: [
|
|
1932
|
+
{
|
|
1933
|
+
field: "command",
|
|
1934
|
+
op: "matches",
|
|
1935
|
+
value: "\\.dropDatabase\\s*\\(",
|
|
1936
|
+
flags: "i"
|
|
1937
|
+
}
|
|
1938
|
+
],
|
|
1939
|
+
verdict: "block",
|
|
1940
|
+
reason: "dropDatabase is irreversible \u2014 blocked by MongoDB shield"
|
|
1941
|
+
},
|
|
1942
|
+
{
|
|
1943
|
+
name: "shield:mongodb:block-drop-collection",
|
|
1944
|
+
tool: "*",
|
|
1945
|
+
conditions: [
|
|
1946
|
+
{
|
|
1947
|
+
field: "command",
|
|
1948
|
+
op: "matches",
|
|
1949
|
+
value: "\\.drop\\s*\\(|db\\.getCollection\\([^)]+\\)\\.drop\\s*\\(",
|
|
1950
|
+
flags: "i"
|
|
1951
|
+
}
|
|
1952
|
+
],
|
|
1953
|
+
verdict: "block",
|
|
1954
|
+
reason: "Collection drop is irreversible \u2014 blocked by MongoDB shield"
|
|
1955
|
+
},
|
|
1956
|
+
{
|
|
1957
|
+
name: "shield:mongodb:block-delete-many-empty-filter",
|
|
1958
|
+
tool: "*",
|
|
1959
|
+
conditions: [
|
|
1960
|
+
{
|
|
1961
|
+
field: "command",
|
|
1962
|
+
op: "matches",
|
|
1963
|
+
value: "\\.deleteMany\\s*\\(\\s*\\{\\s*\\}\\s*\\)",
|
|
1964
|
+
flags: "i"
|
|
1965
|
+
}
|
|
1966
|
+
],
|
|
1967
|
+
verdict: "block",
|
|
1968
|
+
reason: "deleteMany({}) with empty filter wipes the entire collection \u2014 blocked by MongoDB shield"
|
|
1969
|
+
},
|
|
1970
|
+
{
|
|
1971
|
+
name: "shield:mongodb:review-delete-many",
|
|
1972
|
+
tool: "*",
|
|
1973
|
+
conditions: [
|
|
1974
|
+
{
|
|
1975
|
+
field: "command",
|
|
1976
|
+
op: "matches",
|
|
1977
|
+
value: "\\.deleteMany\\s*\\(",
|
|
1978
|
+
flags: "i"
|
|
1979
|
+
}
|
|
1980
|
+
],
|
|
1981
|
+
verdict: "review",
|
|
1982
|
+
reason: "deleteMany requires human approval (MongoDB shield)"
|
|
1983
|
+
},
|
|
1984
|
+
{
|
|
1985
|
+
name: "shield:mongodb:review-drop-index",
|
|
1986
|
+
tool: "*",
|
|
1987
|
+
conditions: [
|
|
1988
|
+
{
|
|
1989
|
+
field: "command",
|
|
1990
|
+
op: "matches",
|
|
1991
|
+
value: "\\.dropIndex\\s*\\(|\\.dropIndexes\\s*\\(",
|
|
1992
|
+
flags: "i"
|
|
1993
|
+
}
|
|
1994
|
+
],
|
|
1995
|
+
verdict: "review",
|
|
1996
|
+
reason: "Index drops affect query performance and require human approval (MongoDB shield)"
|
|
1997
|
+
}
|
|
1998
|
+
],
|
|
1999
|
+
dangerousWords: ["dropDatabase", "dropCollection", "mongodrop"]
|
|
2000
|
+
};
|
|
2001
|
+
|
|
2002
|
+
// src/shields/builtin/postgres.json
|
|
2003
|
+
var postgres_default = {
|
|
2004
|
+
name: "postgres",
|
|
2005
|
+
description: "Protects PostgreSQL databases from destructive AI operations",
|
|
2006
|
+
aliases: ["pg", "postgresql"],
|
|
2007
|
+
smartRules: [
|
|
2008
|
+
{
|
|
2009
|
+
name: "shield:postgres:block-drop-table",
|
|
2010
|
+
tool: "*",
|
|
2011
|
+
conditions: [{ field: "sql", op: "matches", value: "DROP\\s+TABLE", flags: "i" }],
|
|
2012
|
+
verdict: "block",
|
|
2013
|
+
reason: "DROP TABLE is irreversible \u2014 blocked by Postgres shield"
|
|
2014
|
+
},
|
|
2015
|
+
{
|
|
2016
|
+
name: "shield:postgres:block-truncate",
|
|
2017
|
+
tool: "*",
|
|
2018
|
+
conditions: [
|
|
2019
|
+
{ field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }
|
|
2020
|
+
],
|
|
2021
|
+
verdict: "block",
|
|
2022
|
+
reason: "TRUNCATE is irreversible \u2014 blocked by Postgres shield"
|
|
2023
|
+
},
|
|
2024
|
+
{
|
|
2025
|
+
name: "shield:postgres:block-drop-column",
|
|
2026
|
+
tool: "*",
|
|
2027
|
+
conditions: [
|
|
2028
|
+
{ field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
|
|
2029
|
+
],
|
|
2030
|
+
verdict: "block",
|
|
2031
|
+
reason: "DROP COLUMN is irreversible \u2014 blocked by Postgres shield"
|
|
2032
|
+
},
|
|
2033
|
+
{
|
|
2034
|
+
name: "shield:postgres:review-grant-revoke",
|
|
2035
|
+
tool: "*",
|
|
2036
|
+
conditions: [
|
|
2037
|
+
{ field: "sql", op: "matches", value: "\\b(GRANT|REVOKE)\\b", flags: "i" }
|
|
2038
|
+
],
|
|
2039
|
+
verdict: "review",
|
|
2040
|
+
reason: "Permission changes require human approval (Postgres shield)"
|
|
2041
|
+
}
|
|
2042
|
+
],
|
|
2043
|
+
dangerousWords: ["dropdb", "pg_dropcluster"]
|
|
2044
|
+
};
|
|
2045
|
+
|
|
2046
|
+
// src/shields/builtin/project-jail.json
|
|
2047
|
+
var project_jail_default = {
|
|
2048
|
+
name: "project-jail",
|
|
2049
|
+
description: "Restricts AI agents from reading sensitive credential files outside the current project",
|
|
2050
|
+
aliases: ["jail"],
|
|
2051
|
+
smartRules: [
|
|
2052
|
+
{
|
|
2053
|
+
name: "shield:project-jail:block-read-ssh",
|
|
2054
|
+
tool: "bash",
|
|
2055
|
+
conditions: [
|
|
2056
|
+
{
|
|
2057
|
+
field: "command",
|
|
2058
|
+
op: "matches",
|
|
2059
|
+
value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*[\\/\\\\]\\.ssh[\\/\\\\]",
|
|
2060
|
+
flags: "i"
|
|
2061
|
+
}
|
|
2062
|
+
],
|
|
2063
|
+
verdict: "block",
|
|
2064
|
+
reason: "Reading SSH private keys is blocked by project-jail shield"
|
|
2065
|
+
},
|
|
2066
|
+
{
|
|
2067
|
+
name: "shield:project-jail:block-read-aws",
|
|
2068
|
+
tool: "bash",
|
|
2069
|
+
conditions: [
|
|
2070
|
+
{
|
|
2071
|
+
field: "command",
|
|
2072
|
+
op: "matches",
|
|
2073
|
+
value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*[\\/\\\\]\\.aws[\\/\\\\]",
|
|
2074
|
+
flags: "i"
|
|
2075
|
+
}
|
|
2076
|
+
],
|
|
2077
|
+
verdict: "block",
|
|
2078
|
+
reason: "Reading AWS credentials is blocked by project-jail shield"
|
|
2079
|
+
},
|
|
2080
|
+
{
|
|
2081
|
+
name: "shield:project-jail:block-read-env",
|
|
2082
|
+
tool: "bash",
|
|
2083
|
+
conditions: [
|
|
2084
|
+
{
|
|
2085
|
+
field: "command",
|
|
2086
|
+
op: "matches",
|
|
2087
|
+
value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*\\.env(\\.local|\\.production|\\.staging)?\\b",
|
|
2088
|
+
flags: "i"
|
|
2089
|
+
}
|
|
2090
|
+
],
|
|
2091
|
+
verdict: "block",
|
|
2092
|
+
reason: "Reading .env files is blocked by project-jail shield"
|
|
2093
|
+
},
|
|
2094
|
+
{
|
|
2095
|
+
name: "shield:project-jail:block-read-credentials",
|
|
2096
|
+
tool: "bash",
|
|
2097
|
+
conditions: [
|
|
2098
|
+
{
|
|
2099
|
+
field: "command",
|
|
2100
|
+
op: "matches",
|
|
2101
|
+
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)",
|
|
2102
|
+
flags: "i"
|
|
2103
|
+
}
|
|
2104
|
+
],
|
|
2105
|
+
verdict: "block",
|
|
2106
|
+
reason: "Reading credential files is blocked by project-jail shield"
|
|
2107
|
+
}
|
|
2108
|
+
],
|
|
2109
|
+
dangerousWords: []
|
|
2110
|
+
};
|
|
2111
|
+
|
|
2112
|
+
// src/shields/builtin/redis.json
|
|
2113
|
+
var redis_default = {
|
|
2114
|
+
name: "redis",
|
|
2115
|
+
description: "Protects Redis instances from destructive AI operations",
|
|
2116
|
+
aliases: [],
|
|
2117
|
+
smartRules: [
|
|
2118
|
+
{
|
|
2119
|
+
name: "shield:redis:block-flushall",
|
|
2120
|
+
tool: "*",
|
|
2121
|
+
conditions: [
|
|
2122
|
+
{
|
|
2123
|
+
field: "command",
|
|
2124
|
+
op: "matches",
|
|
2125
|
+
value: "\\bFLUSHALL\\b",
|
|
2126
|
+
flags: "i"
|
|
2127
|
+
}
|
|
2128
|
+
],
|
|
2129
|
+
verdict: "block",
|
|
2130
|
+
reason: "FLUSHALL deletes every key in every database \u2014 blocked by Redis shield"
|
|
2131
|
+
},
|
|
2132
|
+
{
|
|
2133
|
+
name: "shield:redis:block-flushdb",
|
|
2134
|
+
tool: "*",
|
|
2135
|
+
conditions: [
|
|
2136
|
+
{
|
|
2137
|
+
field: "command",
|
|
2138
|
+
op: "matches",
|
|
2139
|
+
value: "\\bFLUSHDB\\b",
|
|
2140
|
+
flags: "i"
|
|
2141
|
+
}
|
|
2142
|
+
],
|
|
2143
|
+
verdict: "block",
|
|
2144
|
+
reason: "FLUSHDB deletes all keys in the current database \u2014 blocked by Redis shield"
|
|
2145
|
+
},
|
|
2146
|
+
{
|
|
2147
|
+
name: "shield:redis:block-config-resetstat",
|
|
2148
|
+
tool: "*",
|
|
2149
|
+
conditions: [
|
|
2150
|
+
{
|
|
2151
|
+
field: "command",
|
|
2152
|
+
op: "matches",
|
|
2153
|
+
value: "\\bCONFIG\\s+RESETSTAT\\b",
|
|
2154
|
+
flags: "i"
|
|
2155
|
+
}
|
|
2156
|
+
],
|
|
2157
|
+
verdict: "block",
|
|
2158
|
+
reason: "CONFIG RESETSTAT resets server statistics irreversibly \u2014 blocked by Redis shield"
|
|
2159
|
+
},
|
|
2160
|
+
{
|
|
2161
|
+
name: "shield:redis:review-config-set",
|
|
2162
|
+
tool: "*",
|
|
2163
|
+
conditions: [
|
|
2164
|
+
{
|
|
2165
|
+
field: "command",
|
|
2166
|
+
op: "matches",
|
|
2167
|
+
value: "\\bCONFIG\\s+SET\\b",
|
|
2168
|
+
flags: "i"
|
|
2169
|
+
}
|
|
2170
|
+
],
|
|
2171
|
+
verdict: "review",
|
|
2172
|
+
reason: "CONFIG SET changes live server configuration and requires human approval (Redis shield)"
|
|
2173
|
+
},
|
|
2174
|
+
{
|
|
2175
|
+
name: "shield:redis:review-del-wildcard",
|
|
2176
|
+
tool: "*",
|
|
2177
|
+
conditions: [
|
|
2178
|
+
{
|
|
2179
|
+
field: "command",
|
|
2180
|
+
op: "matches",
|
|
2181
|
+
value: "\\bDEL\\b.*[*?\\[]|redis-cli.*--scan.*\\|.*xargs.*del",
|
|
2182
|
+
flags: "i"
|
|
2183
|
+
}
|
|
2184
|
+
],
|
|
2185
|
+
verdict: "review",
|
|
2186
|
+
reason: "Wildcard key deletion requires human approval (Redis shield)"
|
|
2187
|
+
}
|
|
2188
|
+
],
|
|
2189
|
+
dangerousWords: ["FLUSHALL", "FLUSHDB"]
|
|
2190
|
+
};
|
|
2191
|
+
|
|
2192
|
+
// src/shields/index.ts
|
|
2193
|
+
function isShieldVerdict(v) {
|
|
2194
|
+
return v === "allow" || v === "review" || v === "block";
|
|
2195
|
+
}
|
|
2196
|
+
function validateShieldDefinition(raw) {
|
|
2197
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
2198
|
+
return { error: "Shield file is not an object" };
|
|
2199
|
+
}
|
|
2200
|
+
const r = raw;
|
|
2201
|
+
if (typeof r.name !== "string" || !r.name) return { error: "Shield file missing 'name'" };
|
|
2202
|
+
if (typeof r.description !== "string") return { error: "Shield file missing 'description'" };
|
|
2203
|
+
if (!Array.isArray(r.aliases)) return { error: "Shield file missing 'aliases' array" };
|
|
2204
|
+
if (!Array.isArray(r.smartRules)) return { error: "Shield file missing 'smartRules' array" };
|
|
2205
|
+
if (!Array.isArray(r.dangerousWords))
|
|
2206
|
+
return { error: "Shield file missing 'dangerousWords' array" };
|
|
2207
|
+
return { ok: r };
|
|
2208
|
+
}
|
|
2209
|
+
function validateOverrides(raw) {
|
|
2210
|
+
const warnings = [];
|
|
2211
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return { overrides: {}, warnings };
|
|
2212
|
+
const result = {};
|
|
2213
|
+
for (const [shieldName, rules] of Object.entries(raw)) {
|
|
2214
|
+
if (!rules || typeof rules !== "object" || Array.isArray(rules)) continue;
|
|
2215
|
+
const validRules = {};
|
|
2216
|
+
for (const [ruleName, verdict] of Object.entries(rules)) {
|
|
2217
|
+
if (isShieldVerdict(verdict)) {
|
|
2218
|
+
validRules[ruleName] = verdict;
|
|
2219
|
+
} else {
|
|
2220
|
+
warnings.push(
|
|
2221
|
+
`shields.json contains invalid verdict "${String(verdict)}" for ${shieldName}/${ruleName} \u2014 entry ignored. File may be corrupted or tampered with.`
|
|
2222
|
+
);
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
if (Object.keys(validRules).length > 0) result[shieldName] = validRules;
|
|
2226
|
+
}
|
|
2227
|
+
return { overrides: result, warnings };
|
|
2228
|
+
}
|
|
2229
|
+
var BUILTIN_SHIELDS = {
|
|
2230
|
+
[aws_default.name]: aws_default,
|
|
2231
|
+
[bash_safe_default.name]: bash_safe_default,
|
|
2232
|
+
[docker_default.name]: docker_default,
|
|
2233
|
+
[filesystem_default.name]: filesystem_default,
|
|
2234
|
+
[github_default.name]: github_default,
|
|
2235
|
+
[k8s_default.name]: k8s_default,
|
|
2236
|
+
[mcp_tool_gating_default.name]: mcp_tool_gating_default,
|
|
2237
|
+
[mongodb_default.name]: mongodb_default,
|
|
2238
|
+
[postgres_default.name]: postgres_default,
|
|
2239
|
+
[project_jail_default.name]: project_jail_default,
|
|
2240
|
+
[redis_default.name]: redis_default
|
|
2241
|
+
};
|
|
2242
|
+
|
|
2243
|
+
// src/loop/index.ts
|
|
2244
|
+
var import_crypto = __toESM(require("crypto"));
|
|
2245
|
+
var LOOP_MAX_RECORDS = 500;
|
|
2246
|
+
function computeArgsHash(args) {
|
|
2247
|
+
const str = JSON.stringify(args ?? "");
|
|
2248
|
+
return import_crypto.default.createHash("sha256").update(str).digest("hex").slice(0, 16);
|
|
2249
|
+
}
|
|
2250
|
+
function evaluateLoopWindow(records, tool, args, threshold, windowMs, now) {
|
|
2251
|
+
const hash = computeArgsHash(args);
|
|
2252
|
+
const cutoff = now - windowMs;
|
|
2253
|
+
const fresh = records.filter((r) => r.ts >= cutoff);
|
|
2254
|
+
fresh.push({ t: tool, h: hash, ts: now });
|
|
2255
|
+
const count = fresh.filter((r) => r.t === tool && r.h === hash).length;
|
|
2256
|
+
const nextRecords = fresh.slice(-LOOP_MAX_RECORDS);
|
|
2257
|
+
return { nextRecords, count, looping: count >= threshold };
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
// src/index.ts
|
|
2261
|
+
var ENGINE_VERSION = "1.0.0";
|
|
2262
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2263
|
+
0 && (module.exports = {
|
|
2264
|
+
BUILTIN_SHIELDS,
|
|
2265
|
+
DLP_PATTERNS,
|
|
2266
|
+
ENGINE_VERSION,
|
|
2267
|
+
FLAGS_WITH_VALUES,
|
|
2268
|
+
LOOP_MAX_RECORDS,
|
|
2269
|
+
SENSITIVE_PATH_REGEXES,
|
|
2270
|
+
analyzePipeChain,
|
|
2271
|
+
analyzeShellCommand,
|
|
2272
|
+
checkDangerousSql,
|
|
2273
|
+
computeArgsHash,
|
|
2274
|
+
detectDangerousEval,
|
|
2275
|
+
detectDangerousShellExec,
|
|
2276
|
+
evaluateLoopWindow,
|
|
2277
|
+
evaluatePolicy,
|
|
2278
|
+
evaluateSmartConditions,
|
|
2279
|
+
extractAllSshHosts,
|
|
2280
|
+
extractNetworkTargets,
|
|
2281
|
+
extractPositionalArgs,
|
|
2282
|
+
getCompiledRegex,
|
|
2283
|
+
getNestedValue,
|
|
2284
|
+
isIgnoredTool,
|
|
2285
|
+
isShieldVerdict,
|
|
2286
|
+
matchSensitivePath,
|
|
2287
|
+
matchesPattern,
|
|
2288
|
+
normalizeCommandForPolicy,
|
|
2289
|
+
parseAllSshHostsFromCommand,
|
|
2290
|
+
redactText,
|
|
2291
|
+
scanArgs,
|
|
2292
|
+
scanText,
|
|
2293
|
+
sensitivePathMatch,
|
|
2294
|
+
validateOverrides,
|
|
2295
|
+
validateRegex,
|
|
2296
|
+
validateShieldDefinition
|
|
2297
|
+
});
|