agent-security-scanner-mcp 3.7.0 → 3.8.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.
@@ -0,0 +1,489 @@
1
+ // src/tools/scan-action.js
2
+ import { z } from "zod";
3
+
4
+ export const scanAgentActionSchema = {
5
+ action_type: z.enum(["bash", "file_write", "file_read", "http_request", "file_delete"])
6
+ .describe("Type of agent action to evaluate"),
7
+ action_value: z.string()
8
+ .describe("The command, file path, or URL to check"),
9
+ verbosity: z.enum(["minimal", "compact", "full"]).optional()
10
+ .describe("Response detail level: 'minimal' (action only), 'compact' (default), 'full' (all details)")
11
+ };
12
+
13
+ // --- Detection rule definitions ---
14
+
15
+ const BASH_RULES = [
16
+ // BLOCK: Destructive filesystem operations
17
+ {
18
+ rule: "bash.destructive.rm-rf",
19
+ pattern: /\brm\s+(-[a-zA-Z]*f[a-zA-Z]*\s+-[a-zA-Z]*r[a-zA-Z]*|-[a-zA-Z]*r[a-zA-Z]*\s+-[a-zA-Z]*f[a-zA-Z]*|-[a-zA-Z]*rf[a-zA-Z]*|-[a-zA-Z]*fr[a-zA-Z]*)\s+[/~*]/,
20
+ severity: "CRITICAL",
21
+ action: "BLOCK",
22
+ message: "Destructive recursive force-delete targeting root, home, or wildcard path"
23
+ },
24
+ // BLOCK: Remote code execution via pipe
25
+ {
26
+ rule: "bash.rce.curl-pipe-sh",
27
+ pattern: /\b(curl|wget)\b.*\|\s*(sh|bash|zsh|ksh|dash|python|perl|ruby)\b/,
28
+ severity: "CRITICAL",
29
+ action: "BLOCK",
30
+ message: "Remote code execution: piping downloaded content directly into a shell interpreter"
31
+ },
32
+ // BLOCK: SQL destructive operations
33
+ {
34
+ rule: "bash.sql.drop-table",
35
+ pattern: /\bDROP\s+TABLE\b/i,
36
+ severity: "CRITICAL",
37
+ action: "BLOCK",
38
+ message: "SQL DROP TABLE detected - destructive database operation"
39
+ },
40
+ {
41
+ rule: "bash.sql.delete-no-where",
42
+ pattern: /\bDELETE\s+FROM\s+\w+\s*(?:;|$)/i,
43
+ severity: "CRITICAL",
44
+ action: "BLOCK",
45
+ message: "SQL DELETE FROM without WHERE clause - will delete all rows"
46
+ },
47
+ // BLOCK: Disk operations
48
+ {
49
+ rule: "bash.disk.dd",
50
+ pattern: /\bdd\s+if=/,
51
+ severity: "CRITICAL",
52
+ action: "BLOCK",
53
+ message: "Low-level disk write via dd - can destroy disk contents"
54
+ },
55
+ {
56
+ rule: "bash.disk.mkfs",
57
+ pattern: /\bmkfs\b/,
58
+ severity: "CRITICAL",
59
+ action: "BLOCK",
60
+ message: "Filesystem formatting via mkfs - will erase all data on target device"
61
+ },
62
+ // BLOCK: Credential file access
63
+ {
64
+ rule: "bash.credential.ssh-key-read",
65
+ pattern: /\bcat\s+~?\/?\.ssh\/id_(rsa|ed25519|ecdsa|dsa)\b/,
66
+ severity: "CRITICAL",
67
+ action: "BLOCK",
68
+ message: "Attempting to read SSH private key"
69
+ },
70
+ {
71
+ rule: "bash.credential.etc-shadow",
72
+ pattern: /\bcat\s+\/etc\/shadow\b/,
73
+ severity: "CRITICAL",
74
+ action: "BLOCK",
75
+ message: "Attempting to read /etc/shadow (password hashes)"
76
+ },
77
+ {
78
+ rule: "bash.credential.aws-creds",
79
+ pattern: /\bcat\s+~?\/?\.aws\/credentials\b/,
80
+ severity: "CRITICAL",
81
+ action: "BLOCK",
82
+ message: "Attempting to read AWS credentials file"
83
+ },
84
+ // WARN: Overly permissive chmod
85
+ {
86
+ rule: "bash.permissions.chmod-777",
87
+ pattern: /\bchmod\s+(777|666)\b/,
88
+ severity: "HIGH",
89
+ action: "WARN",
90
+ message: "Overly permissive file permissions (world-readable/writable)"
91
+ },
92
+ // WARN: sudo usage
93
+ {
94
+ rule: "bash.escalation.sudo",
95
+ pattern: /\bsudo\b/,
96
+ severity: "MEDIUM",
97
+ action: "WARN",
98
+ message: "Privilege escalation via sudo"
99
+ },
100
+ // WARN: SSH key manipulation
101
+ {
102
+ rule: "bash.ssh.keygen",
103
+ pattern: /\bssh-keygen\b/,
104
+ severity: "MEDIUM",
105
+ action: "WARN",
106
+ message: "SSH key generation - may overwrite existing keys"
107
+ },
108
+ {
109
+ rule: "bash.ssh.add",
110
+ pattern: /\bssh-add\b/,
111
+ severity: "MEDIUM",
112
+ action: "WARN",
113
+ message: "SSH agent key addition"
114
+ },
115
+ // WARN: Process killing
116
+ {
117
+ rule: "bash.process.kill-9",
118
+ pattern: /\bkill\s+-9\b/,
119
+ severity: "MEDIUM",
120
+ action: "WARN",
121
+ message: "Forceful process termination (SIGKILL)"
122
+ },
123
+ {
124
+ rule: "bash.process.killall",
125
+ pattern: /\bkillall\b/,
126
+ severity: "MEDIUM",
127
+ action: "WARN",
128
+ message: "Bulk process termination via killall"
129
+ },
130
+ // WARN: Force push
131
+ {
132
+ rule: "bash.git.force-push",
133
+ pattern: /\bgit\s+push\s+--force\b/,
134
+ severity: "HIGH",
135
+ action: "WARN",
136
+ message: "Git force push - can overwrite remote history and cause data loss"
137
+ },
138
+ // WARN: Environment variable dumping with pipe
139
+ {
140
+ rule: "bash.env.dump-pipe",
141
+ pattern: /\b(env|printenv)\b.*\|/,
142
+ severity: "MEDIUM",
143
+ action: "WARN",
144
+ message: "Environment variable dump piped to another command - potential secret exfiltration"
145
+ }
146
+ ];
147
+
148
+ const SENSITIVE_FILE_PATTERNS = [
149
+ { pattern: /(^|\/)\.env($|\.)/, label: ".env file", severity: "HIGH" },
150
+ { pattern: /(^|\/)\.ssh\//, label: "SSH directory", severity: "CRITICAL" },
151
+ { pattern: /credentials/i, label: "credentials file", severity: "HIGH" },
152
+ { pattern: /secrets/i, label: "secrets file", severity: "HIGH" },
153
+ { pattern: /(^|\/)\.github\/workflows\//, label: "GitHub Actions workflow", severity: "HIGH" },
154
+ { pattern: /(^|\/)Dockerfile$/, label: "Dockerfile", severity: "MEDIUM" },
155
+ { pattern: /(^|\/)docker-compose/, label: "Docker Compose file", severity: "MEDIUM" }
156
+ ];
157
+
158
+ const SYSTEM_FILE_PATTERNS = [
159
+ { pattern: /^\/etc\//, label: "/etc system config", severity: "CRITICAL" },
160
+ { pattern: /^\/usr\//, label: "/usr system directory", severity: "CRITICAL" },
161
+ { pattern: /^\/bin\//, label: "/bin system binaries", severity: "CRITICAL" }
162
+ ];
163
+
164
+ const PACKAGE_FILE_PATTERNS = [
165
+ { pattern: /(^|\/)package\.json$/, label: "npm package manifest", severity: "MEDIUM" },
166
+ { pattern: /(^|\/)requirements\.txt$/, label: "Python requirements", severity: "MEDIUM" },
167
+ { pattern: /(^|\/)Gemfile$/, label: "Ruby Gemfile", severity: "MEDIUM" },
168
+ { pattern: /(^|\/)Cargo\.toml$/, label: "Rust Cargo manifest", severity: "MEDIUM" },
169
+ { pattern: /(^|\/)go\.mod$/, label: "Go module file", severity: "MEDIUM" }
170
+ ];
171
+
172
+ const CREDENTIAL_READ_PATTERNS = [
173
+ { pattern: /(^|\/)\.env($|\.)/, label: ".env file", severity: "MEDIUM" },
174
+ { pattern: /\.pem$/, label: "PEM certificate/key", severity: "HIGH" },
175
+ { pattern: /\.key$/, label: "private key file", severity: "HIGH" },
176
+ { pattern: /(^|\/)\.ssh\//, label: "SSH directory", severity: "HIGH" },
177
+ { pattern: /credentials/i, label: "credentials file", severity: "HIGH" },
178
+ { pattern: /secret/i, label: "secret file", severity: "HIGH" }
179
+ ];
180
+
181
+ const PRIVATE_IP_PATTERNS = [
182
+ { pattern: /\b127\.0\.0\.1\b/, label: "loopback address (127.0.0.1)" },
183
+ { pattern: /\blocalhost\b/, label: "localhost" },
184
+ { pattern: /\b10\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/, label: "private IP (10.x.x.x)" },
185
+ { pattern: /\b172\.(1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}\b/, label: "private IP (172.16-31.x.x)" },
186
+ { pattern: /\b192\.168\.\d{1,3}\.\d{1,3}\b/, label: "private IP (192.168.x.x)" },
187
+ { pattern: /\b169\.254\.\d{1,3}\.\d{1,3}\b/, label: "link-local address (169.254.x.x)" }
188
+ ];
189
+
190
+ const EXFILTRATION_PATTERNS = [
191
+ { pattern: /webhook\.site/i, label: "webhook.site" },
192
+ { pattern: /requestbin/i, label: "RequestBin" },
193
+ { pattern: /pastebin\.com/i, label: "Pastebin" },
194
+ { pattern: /hookbin/i, label: "HookBin" },
195
+ { pattern: /pipedream/i, label: "Pipedream" },
196
+ { pattern: /ngrok\.io/i, label: "ngrok tunnel" },
197
+ { pattern: /burpcollaborator/i, label: "Burp Collaborator" }
198
+ ];
199
+
200
+ // --- Detection logic per action type ---
201
+
202
+ function checkBash(value) {
203
+ const findings = [];
204
+ const normalized = value.toLowerCase();
205
+
206
+ for (const rule of BASH_RULES) {
207
+ if (rule.pattern.test(value) || rule.pattern.test(normalized)) {
208
+ findings.push({
209
+ rule: rule.rule,
210
+ severity: rule.severity,
211
+ action: rule.action,
212
+ message: rule.message
213
+ });
214
+ }
215
+ }
216
+
217
+ return findings;
218
+ }
219
+
220
+ function checkFileWrite(value) {
221
+ const findings = [];
222
+
223
+ // System files -> BLOCK
224
+ for (const p of SYSTEM_FILE_PATTERNS) {
225
+ if (p.pattern.test(value)) {
226
+ findings.push({
227
+ rule: "file_write.system." + p.label.replace(/[^a-z0-9]/gi, '-').toLowerCase(),
228
+ severity: "CRITICAL",
229
+ action: "BLOCK",
230
+ message: `Writing to system path (${p.label}) is blocked`
231
+ });
232
+ }
233
+ }
234
+
235
+ // Sensitive files -> WARN
236
+ for (const p of SENSITIVE_FILE_PATTERNS) {
237
+ if (p.pattern.test(value)) {
238
+ findings.push({
239
+ rule: "file_write.sensitive." + p.label.replace(/[^a-z0-9]/gi, '-').toLowerCase(),
240
+ severity: p.severity,
241
+ action: "WARN",
242
+ message: `Writing to sensitive file (${p.label}) - review carefully`
243
+ });
244
+ }
245
+ }
246
+
247
+ // Package files -> WARN
248
+ for (const p of PACKAGE_FILE_PATTERNS) {
249
+ if (p.pattern.test(value)) {
250
+ findings.push({
251
+ rule: "file_write.package." + p.label.replace(/[^a-z0-9]/gi, '-').toLowerCase(),
252
+ severity: p.severity,
253
+ action: "WARN",
254
+ message: `Modifying dependency file (${p.label}) - may introduce supply chain risk`
255
+ });
256
+ }
257
+ }
258
+
259
+ return findings;
260
+ }
261
+
262
+ function checkFileRead(value) {
263
+ const findings = [];
264
+
265
+ for (const p of CREDENTIAL_READ_PATTERNS) {
266
+ if (p.pattern.test(value)) {
267
+ findings.push({
268
+ rule: "file_read.credential." + p.label.replace(/[^a-z0-9]/gi, '-').toLowerCase(),
269
+ severity: p.severity,
270
+ action: "WARN",
271
+ message: `Reading credential/sensitive file (${p.label}) - potential secret exposure`
272
+ });
273
+ }
274
+ }
275
+
276
+ return findings;
277
+ }
278
+
279
+ function checkHttpRequest(value) {
280
+ const findings = [];
281
+
282
+ // SSRF: private/internal IPs -> BLOCK
283
+ for (const p of PRIVATE_IP_PATTERNS) {
284
+ if (p.pattern.test(value)) {
285
+ findings.push({
286
+ rule: "http.ssrf." + p.label.replace(/[^a-z0-9]/gi, '-').toLowerCase(),
287
+ severity: "CRITICAL",
288
+ action: "BLOCK",
289
+ message: `SSRF risk: request targets internal/private address (${p.label})`
290
+ });
291
+ }
292
+ }
293
+
294
+ // Exfiltration patterns -> WARN
295
+ for (const p of EXFILTRATION_PATTERNS) {
296
+ if (p.pattern.test(value)) {
297
+ findings.push({
298
+ rule: "http.exfiltration." + p.label.replace(/[^a-z0-9]/gi, '-').toLowerCase(),
299
+ severity: "HIGH",
300
+ action: "WARN",
301
+ message: `Potential data exfiltration: request targets known exfiltration service (${p.label})`
302
+ });
303
+ }
304
+ }
305
+
306
+ return findings;
307
+ }
308
+
309
+ function checkFileDelete(value) {
310
+ const findings = [];
311
+
312
+ // System files -> BLOCK
313
+ for (const p of SYSTEM_FILE_PATTERNS) {
314
+ if (p.pattern.test(value)) {
315
+ findings.push({
316
+ rule: "file_delete.system." + p.label.replace(/[^a-z0-9]/gi, '-').toLowerCase(),
317
+ severity: "CRITICAL",
318
+ action: "BLOCK",
319
+ message: `Deleting system file (${p.label}) is blocked`
320
+ });
321
+ }
322
+ }
323
+
324
+ // Sensitive files -> BLOCK (upgraded from WARN)
325
+ for (const p of SENSITIVE_FILE_PATTERNS) {
326
+ if (p.pattern.test(value)) {
327
+ findings.push({
328
+ rule: "file_delete.sensitive." + p.label.replace(/[^a-z0-9]/gi, '-').toLowerCase(),
329
+ severity: "CRITICAL",
330
+ action: "BLOCK",
331
+ message: `Deleting sensitive file (${p.label}) is blocked`
332
+ });
333
+ }
334
+ }
335
+
336
+ // Package files -> WARN (upgraded severity)
337
+ for (const p of PACKAGE_FILE_PATTERNS) {
338
+ if (p.pattern.test(value)) {
339
+ findings.push({
340
+ rule: "file_delete.package." + p.label.replace(/[^a-z0-9]/gi, '-').toLowerCase(),
341
+ severity: "HIGH",
342
+ action: "WARN",
343
+ message: `Deleting dependency file (${p.label}) - may break project builds`
344
+ });
345
+ }
346
+ }
347
+
348
+ return findings;
349
+ }
350
+
351
+ // --- Risk level derivation ---
352
+
353
+ function deriveRiskLevel(findings) {
354
+ if (findings.length === 0) return "NONE";
355
+
356
+ const hasBlock = findings.some(f => f.action === "BLOCK");
357
+ const hasCritical = findings.some(f => f.severity === "CRITICAL");
358
+ const hasHigh = findings.some(f => f.severity === "HIGH");
359
+ const hasMedium = findings.some(f => f.severity === "MEDIUM");
360
+
361
+ if (hasBlock || hasCritical) return "CRITICAL";
362
+ if (hasHigh) return "HIGH";
363
+ if (hasMedium) return "MEDIUM";
364
+ return "LOW";
365
+ }
366
+
367
+ // --- Overall action derivation ---
368
+
369
+ function deriveAction(findings) {
370
+ if (findings.length === 0) return "ALLOW";
371
+ if (findings.some(f => f.action === "BLOCK")) return "BLOCK";
372
+ if (findings.some(f => f.action === "WARN")) return "WARN";
373
+ return "ALLOW";
374
+ }
375
+
376
+ // --- Recommendation generation ---
377
+
378
+ function generateRecommendation(action, actionType, findings) {
379
+ if (action === "ALLOW") {
380
+ return "Action appears safe to proceed.";
381
+ }
382
+
383
+ if (action === "BLOCK") {
384
+ const rules = findings.filter(f => f.action === "BLOCK").map(f => f.rule);
385
+ return `Action BLOCKED due to: ${rules.join(', ')}. Do not execute this action. Consider a safer alternative.`;
386
+ }
387
+
388
+ // WARN
389
+ const messages = findings.map(f => f.message);
390
+ const uniqueMessages = [...new Set(messages)];
391
+ return `Proceed with caution. ${uniqueMessages.length} concern(s): ${uniqueMessages.join('; ')}.`;
392
+ }
393
+
394
+ // --- Verbosity formatters ---
395
+
396
+ function formatMinimal(action, actionType, actionValue, riskLevel, findings) {
397
+ return {
398
+ action,
399
+ action_type: actionType,
400
+ risk_level: riskLevel,
401
+ findings_count: findings.length,
402
+ message: findings.length > 0
403
+ ? `${action}: ${findings.length} concern(s) detected. Use verbosity='compact' for details.`
404
+ : "ALLOW: No security concerns detected."
405
+ };
406
+ }
407
+
408
+ function formatCompact(action, actionType, actionValue, riskLevel, findings, recommendation) {
409
+ return {
410
+ action,
411
+ action_type: actionType,
412
+ action_value: actionValue,
413
+ risk_level: riskLevel,
414
+ findings: findings.map(f => ({
415
+ rule: f.rule,
416
+ severity: f.severity,
417
+ message: f.message
418
+ })),
419
+ recommendation
420
+ };
421
+ }
422
+
423
+ function formatFull(action, actionType, actionValue, riskLevel, findings, recommendation) {
424
+ return {
425
+ action,
426
+ action_type: actionType,
427
+ action_value: actionValue,
428
+ risk_level: riskLevel,
429
+ findings_count: findings.length,
430
+ findings: findings.map(f => ({
431
+ rule: f.rule,
432
+ severity: f.severity,
433
+ action: f.action,
434
+ message: f.message
435
+ })),
436
+ recommendation,
437
+ timestamp: new Date().toISOString()
438
+ };
439
+ }
440
+
441
+ // --- Exported handler ---
442
+
443
+ export async function scanAgentAction({ action_type, action_value, verbosity }) {
444
+ let findings = [];
445
+
446
+ switch (action_type) {
447
+ case "bash":
448
+ findings = checkBash(action_value);
449
+ break;
450
+ case "file_write":
451
+ findings = checkFileWrite(action_value);
452
+ break;
453
+ case "file_read":
454
+ findings = checkFileRead(action_value);
455
+ break;
456
+ case "http_request":
457
+ findings = checkHttpRequest(action_value);
458
+ break;
459
+ case "file_delete":
460
+ findings = checkFileDelete(action_value);
461
+ break;
462
+ }
463
+
464
+ const action = deriveAction(findings);
465
+ const riskLevel = deriveRiskLevel(findings);
466
+ const recommendation = generateRecommendation(action, action_type, findings);
467
+
468
+ const level = verbosity || "compact";
469
+
470
+ let result;
471
+ switch (level) {
472
+ case "minimal":
473
+ result = formatMinimal(action, action_type, action_value, riskLevel, findings);
474
+ break;
475
+ case "full":
476
+ result = formatFull(action, action_type, action_value, riskLevel, findings, recommendation);
477
+ break;
478
+ case "compact":
479
+ default:
480
+ result = formatCompact(action, action_type, action_value, riskLevel, findings, recommendation);
481
+ }
482
+
483
+ return {
484
+ content: [{
485
+ type: "text",
486
+ text: JSON.stringify(result, null, 2)
487
+ }]
488
+ };
489
+ }