agent-security-scanner-mcp 3.11.0 → 3.12.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.
@@ -2,10 +2,10 @@
2
2
  import { z } from "zod";
3
3
 
4
4
  export const scanAgentActionSchema = {
5
- action_type: z.enum(["bash", "file_write", "file_read", "http_request", "file_delete"])
5
+ action_type: z.enum(["bash", "file_write", "file_read", "http_request", "file_delete", "cron", "process_spawn", "git", "docker"])
6
6
  .describe("Type of agent action to evaluate"),
7
7
  action_value: z.string()
8
- .describe("The command, file path, or URL to check"),
8
+ .describe("The command, file path, URL, or structured input to check"),
9
9
  verbosity: z.enum(["minimal", "compact", "full"]).optional()
10
10
  .describe("Response detail level: 'minimal' (action only), 'compact' (default), 'full' (all details)")
11
11
  };
@@ -197,6 +197,159 @@ const EXFILTRATION_PATTERNS = [
197
197
  { pattern: /burpcollaborator/i, label: "Burp Collaborator" }
198
198
  ];
199
199
 
200
+ // --- Cron rules ---
201
+
202
+ const CRON_RULES = [
203
+ {
204
+ rule: "cron.persistence.at-boot",
205
+ pattern: /@reboot/,
206
+ severity: "HIGH",
207
+ action: "WARN",
208
+ message: "Cron entry runs at reboot — potential persistence mechanism"
209
+ },
210
+ {
211
+ rule: "cron.frequency.every-minute",
212
+ pattern: /^\s*\*\s+\*\s+\*\s+\*\s+\*/,
213
+ severity: "MEDIUM",
214
+ action: "WARN",
215
+ message: "Cron entry runs every minute — high frequency may indicate abuse"
216
+ },
217
+ {
218
+ rule: "cron.rce.curl-pipe",
219
+ pattern: /\b(curl|wget)\b.*\|\s*(sh|bash|python|perl|ruby)\b/,
220
+ severity: "CRITICAL",
221
+ action: "BLOCK",
222
+ message: "Cron entry downloads and executes remote code"
223
+ },
224
+ {
225
+ rule: "cron.exfiltration.redirect",
226
+ pattern: /\b(curl|wget)\b.*(-d|--data|--upload-file)\b/,
227
+ severity: "HIGH",
228
+ action: "WARN",
229
+ message: "Cron entry sends data to a remote server — potential exfiltration"
230
+ }
231
+ ];
232
+
233
+ // --- Process spawn rules ---
234
+
235
+ const PROCESS_SPAWN_RULES = [
236
+ {
237
+ rule: "process_spawn.reverse-shell",
238
+ pattern: /\b(nc|ncat|netcat)\s+.*-e\s+\/bin\/(sh|bash)\b/,
239
+ severity: "CRITICAL",
240
+ action: "BLOCK",
241
+ message: "Reverse shell via netcat"
242
+ },
243
+ {
244
+ rule: "process_spawn.python-shell",
245
+ pattern: /python.*\bsocket\b.*\bconnect\b/,
246
+ severity: "CRITICAL",
247
+ action: "BLOCK",
248
+ message: "Python-based reverse shell or socket connection"
249
+ },
250
+ {
251
+ rule: "process_spawn.background-daemon",
252
+ pattern: /\bnohup\b.*&\s*$/,
253
+ severity: "MEDIUM",
254
+ action: "WARN",
255
+ message: "Background daemon spawned with nohup — may persist after session"
256
+ },
257
+ {
258
+ rule: "process_spawn.privilege-escalation",
259
+ pattern: /\bsudo\b/,
260
+ severity: "MEDIUM",
261
+ action: "WARN",
262
+ message: "Process spawned with elevated privileges via sudo"
263
+ }
264
+ ];
265
+
266
+ // --- Git rules ---
267
+
268
+ const GIT_RULES = [
269
+ {
270
+ rule: "git.destructive.force-push",
271
+ pattern: /\bgit\s+push\s+.*--force\b/,
272
+ severity: "HIGH",
273
+ action: "WARN",
274
+ message: "Git force push — can overwrite remote history and cause data loss"
275
+ },
276
+ {
277
+ rule: "git.destructive.reset-hard",
278
+ pattern: /\bgit\s+reset\s+--hard\b/,
279
+ severity: "HIGH",
280
+ action: "WARN",
281
+ message: "Git hard reset — discards all uncommitted changes"
282
+ },
283
+ {
284
+ rule: "git.destructive.clean-fd",
285
+ pattern: /\bgit\s+clean\s+-[a-zA-Z]*f[a-zA-Z]*d/,
286
+ severity: "HIGH",
287
+ action: "WARN",
288
+ message: "Git clean with force+directory flags — permanently deletes untracked files and directories"
289
+ },
290
+ {
291
+ rule: "git.credential.config-password",
292
+ pattern: /\bgit\s+(config|credential)\b.*\b(password|token)\b/i,
293
+ severity: "HIGH",
294
+ action: "WARN",
295
+ message: "Git command may expose or set credentials in config"
296
+ },
297
+ {
298
+ rule: "git.remote.add-untrusted",
299
+ pattern: /\bgit\s+remote\s+add\b/,
300
+ severity: "MEDIUM",
301
+ action: "WARN",
302
+ message: "Adding a new git remote — verify the URL is trusted"
303
+ }
304
+ ];
305
+
306
+ // --- Docker rules ---
307
+
308
+ const DOCKER_RULES = [
309
+ {
310
+ rule: "docker.privileged",
311
+ pattern: /--privileged/,
312
+ severity: "CRITICAL",
313
+ action: "BLOCK",
314
+ message: "Docker container with --privileged flag — full host access"
315
+ },
316
+ {
317
+ rule: "docker.host-mount.root",
318
+ pattern: /-v\s+\/:/,
319
+ severity: "CRITICAL",
320
+ action: "BLOCK",
321
+ message: "Docker container mounts host root filesystem"
322
+ },
323
+ {
324
+ rule: "docker.host-mount.docker-sock",
325
+ pattern: /-v\s+\/var\/run\/docker\.sock/,
326
+ severity: "CRITICAL",
327
+ action: "BLOCK",
328
+ message: "Docker container mounts Docker socket — can control host Docker daemon"
329
+ },
330
+ {
331
+ rule: "docker.host-network",
332
+ pattern: /--net(work)?=host\b/,
333
+ severity: "HIGH",
334
+ action: "WARN",
335
+ message: "Docker container uses host network — no network isolation"
336
+ },
337
+ {
338
+ rule: "docker.host-pid",
339
+ pattern: /--pid=host\b/,
340
+ severity: "HIGH",
341
+ action: "WARN",
342
+ message: "Docker container shares host PID namespace — can see/signal host processes"
343
+ },
344
+ {
345
+ rule: "docker.cap-add",
346
+ pattern: /--cap-add\s+(SYS_ADMIN|ALL)\b/,
347
+ severity: "CRITICAL",
348
+ action: "BLOCK",
349
+ message: "Docker container adds dangerous Linux capability"
350
+ }
351
+ ];
352
+
200
353
  // --- Detection logic per action type ---
201
354
 
202
355
  function checkBash(value) {
@@ -306,6 +459,61 @@ function checkHttpRequest(value) {
306
459
  return findings;
307
460
  }
308
461
 
462
+ function checkCron(value) {
463
+ const findings = [];
464
+ for (const rule of CRON_RULES) {
465
+ if (rule.pattern.test(value)) {
466
+ findings.push({ rule: rule.rule, severity: rule.severity, action: rule.action, message: rule.message });
467
+ }
468
+ }
469
+ // Also run bash rules on the command portion (after the schedule)
470
+ const cmdPortion = value.replace(/^[@*0-9,\-\/\s]+/, '').trim();
471
+ if (cmdPortion) {
472
+ for (const rule of BASH_RULES) {
473
+ if (rule.pattern.test(cmdPortion)) {
474
+ findings.push({ rule: rule.rule, severity: rule.severity, action: rule.action, message: rule.message });
475
+ }
476
+ }
477
+ }
478
+ return findings;
479
+ }
480
+
481
+ function checkProcessSpawn(value) {
482
+ const findings = [];
483
+ for (const rule of PROCESS_SPAWN_RULES) {
484
+ if (rule.pattern.test(value)) {
485
+ findings.push({ rule: rule.rule, severity: rule.severity, action: rule.action, message: rule.message });
486
+ }
487
+ }
488
+ // Also apply bash rules for general command safety
489
+ for (const rule of BASH_RULES) {
490
+ if (rule.pattern.test(value)) {
491
+ findings.push({ rule: rule.rule, severity: rule.severity, action: rule.action, message: rule.message });
492
+ }
493
+ }
494
+ return findings;
495
+ }
496
+
497
+ function checkGit(value) {
498
+ const findings = [];
499
+ for (const rule of GIT_RULES) {
500
+ if (rule.pattern.test(value)) {
501
+ findings.push({ rule: rule.rule, severity: rule.severity, action: rule.action, message: rule.message });
502
+ }
503
+ }
504
+ return findings;
505
+ }
506
+
507
+ function checkDocker(value) {
508
+ const findings = [];
509
+ for (const rule of DOCKER_RULES) {
510
+ if (rule.pattern.test(value)) {
511
+ findings.push({ rule: rule.rule, severity: rule.severity, action: rule.action, message: rule.message });
512
+ }
513
+ }
514
+ return findings;
515
+ }
516
+
309
517
  function checkFileDelete(value) {
310
518
  const findings = [];
311
519
 
@@ -459,6 +667,18 @@ export async function scanAgentAction({ action_type, action_value, verbosity })
459
667
  case "file_delete":
460
668
  findings = checkFileDelete(action_value);
461
669
  break;
670
+ case "cron":
671
+ findings = checkCron(action_value);
672
+ break;
673
+ case "process_spawn":
674
+ findings = checkProcessSpawn(action_value);
675
+ break;
676
+ case "git":
677
+ findings = checkGit(action_value);
678
+ break;
679
+ case "docker":
680
+ findings = checkDocker(action_value);
681
+ break;
462
682
  }
463
683
 
464
684
  const action = deriveAction(findings);
@@ -55,12 +55,22 @@ const CONFIDENCE_MULTIPLIERS = {
55
55
  "LOW": 0.4
56
56
  };
57
57
 
58
+ // Maximum prompt size to prevent DoS via large inputs (100KB)
59
+ const MAX_PROMPT_SIZE = 100 * 1024;
60
+
61
+ // Rule caches — loaded once per process, not on every call
62
+ let _agentAttackRulesCache = null;
63
+ let _promptInjectionRulesCache = null;
64
+ let _openClawRulesCache = null;
65
+
58
66
  // Load agent attack rules from YAML
59
67
  function loadAgentAttackRules() {
68
+ if (_agentAttackRulesCache !== null) return _agentAttackRulesCache;
60
69
  try {
61
70
  const rulesPath = join(__dirname, '..', '..', 'rules', 'agent-attacks.security.yaml');
62
71
  if (!existsSync(rulesPath)) {
63
72
  console.error("Agent attack rules file not found");
73
+ _agentAttackRulesCache = [];
64
74
  return [];
65
75
  }
66
76
 
@@ -120,18 +130,22 @@ function loadAgentAttackRules() {
120
130
  }
121
131
  }
122
132
 
133
+ _agentAttackRulesCache = rules;
123
134
  return rules;
124
135
  } catch (error) {
125
136
  console.error("Error loading agent attack rules:", error.message);
137
+ _agentAttackRulesCache = [];
126
138
  return [];
127
139
  }
128
140
  }
129
141
 
130
142
  // Also load prompt injection rules
131
143
  function loadPromptInjectionRules() {
144
+ if (_promptInjectionRulesCache !== null) return _promptInjectionRulesCache;
132
145
  try {
133
146
  const rulesPath = join(__dirname, '..', '..', 'rules', 'prompt-injection.security.yaml');
134
147
  if (!existsSync(rulesPath)) {
148
+ _promptInjectionRulesCache = [];
135
149
  return [];
136
150
  }
137
151
 
@@ -188,18 +202,22 @@ function loadPromptInjectionRules() {
188
202
  }
189
203
  }
190
204
 
205
+ _promptInjectionRulesCache = rules;
191
206
  return rules;
192
207
  } catch (error) {
193
208
  console.error("Error loading prompt injection rules:", error.message);
209
+ _promptInjectionRulesCache = [];
194
210
  return [];
195
211
  }
196
212
  }
197
213
 
198
214
  // Load OpenClaw-specific rules
199
215
  function loadOpenClawRules() {
216
+ if (_openClawRulesCache !== null) return _openClawRulesCache;
200
217
  try {
201
218
  const rulesPath = join(__dirname, '..', '..', 'rules', 'openclaw.security.yaml');
202
219
  if (!existsSync(rulesPath)) {
220
+ _openClawRulesCache = [];
203
221
  return [];
204
222
  }
205
223
 
@@ -251,9 +269,11 @@ function loadOpenClawRules() {
251
269
  }
252
270
  }
253
271
 
272
+ _openClawRulesCache = rules;
254
273
  return rules;
255
274
  } catch (error) {
256
275
  console.error("Error loading OpenClaw rules:", error.message);
276
+ _openClawRulesCache = [];
257
277
  return [];
258
278
  }
259
279
  }
@@ -441,6 +461,20 @@ export const scanAgentPromptSchema = {
441
461
 
442
462
  // Export handler function
443
463
  export async function scanAgentPrompt({ prompt_text, context, verbosity }) {
464
+ // Guard against oversized inputs that could cause DoS via regex scanning
465
+ if (prompt_text.length > MAX_PROMPT_SIZE) {
466
+ return {
467
+ content: [{
468
+ type: "text",
469
+ text: JSON.stringify({
470
+ action: "BLOCK",
471
+ risk_level: "HIGH",
472
+ error: `Prompt too large (${prompt_text.length} bytes, max ${MAX_PROMPT_SIZE}). Reduce size or split into smaller chunks.`
473
+ }, null, 2)
474
+ }]
475
+ };
476
+ }
477
+
444
478
  const findings = [];
445
479
 
446
480
  // Load rules