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.
- package/README.md +42 -8
- package/analyzer.py +22 -5
- package/cross_file_analyzer.py +216 -0
- package/daemon.py +179 -0
- package/index.js +279 -3
- package/package.json +19 -5
- package/packages/npm-bloom.json +1 -0
- package/pattern_matcher.py +1 -0
- package/regex_fallback.py +199 -1
- package/requirements.txt +1 -0
- package/rules/prompt-injection.security.yaml +273 -41
- package/scripts/postinstall.js +60 -0
- package/skills/openclaw/SKILL.md +102 -0
- package/skills/security-review.md +139 -0
- package/skills/security-scan-batch.md +107 -0
- package/skills/security-scanner.md +76 -0
- package/src/cli/doctor.js +29 -1
- package/src/cli/init.js +93 -0
- package/src/cli/report.js +444 -0
- package/src/config.js +247 -0
- package/src/context.js +289 -0
- package/src/daemon-client.js +233 -0
- package/src/dedup.js +129 -0
- package/src/fix-patterns.js +76 -19
- package/src/history.js +159 -0
- package/src/tools/check-package.js +36 -12
- package/src/tools/fix-security.js +32 -5
- package/src/tools/import-resolver.js +249 -0
- package/src/tools/project-context.js +365 -0
- package/src/tools/scan-action.js +489 -0
- package/src/tools/scan-mcp.js +588 -0
- package/src/tools/scan-project.js +16 -4
- package/src/tools/scan-prompt.js +292 -527
- package/src/tools/scan-security.js +37 -6
- package/src/typosquat.js +210 -0
- package/src/utils.js +215 -8
- package/templates/gitlab-ci-security.yml +225 -0
- package/templates/pre-commit-hook.sh +233 -0
|
@@ -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
|
+
}
|