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.
- package/README.md +2 -2
- package/index.js +32 -14
- package/openclaw.plugin.json +3 -3
- package/package.json +2 -1
- package/src/cli/audit.js +10 -3
- package/src/cli/demo.js +3 -13
- package/src/cli/doctor.js +15 -13
- package/src/cli/harden.js +10 -3
- package/src/cli/init.js +11 -5
- package/src/config.js +4 -1
- package/src/daemon-client.js +3 -1
- package/src/python.js +54 -0
- package/src/tools/scan-action.js +222 -2
- package/src/tools/scan-prompt.js +34 -0
- package/src/tools/scan-skill.js +438 -80
- package/src/utils.js +71 -12
package/src/tools/scan-action.js
CHANGED
|
@@ -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
|
|
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);
|
package/src/tools/scan-prompt.js
CHANGED
|
@@ -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
|