agent-security-scanner-mcp 3.4.0 → 3.5.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 +3 -283
- package/analyzer.py +5 -22
- package/index.js +2 -191
- package/package.json +5 -15
- package/pattern_matcher.py +0 -1
- package/regex_fallback.py +1 -199
- package/src/cli/init.js +0 -93
- package/src/fix-patterns.js +17 -66
- package/src/tools/fix-security.js +4 -31
- package/src/tools/scan-prompt.js +1 -71
- package/src/tools/scan-security.js +5 -33
- package/src/utils.js +7 -76
- package/cross_file_analyzer.py +0 -216
- package/scripts/postinstall.js +0 -25
- package/skills/openclaw/SKILL.md +0 -102
- package/skills/security-scan-batch.md +0 -107
- package/skills/security-scanner.md +0 -76
- package/src/config.js +0 -181
- package/src/context.js +0 -228
- package/src/dedup.js +0 -129
package/pattern_matcher.py
CHANGED
package/regex_fallback.py
CHANGED
|
@@ -9,207 +9,10 @@ from typing import List, Dict, Optional
|
|
|
9
9
|
import re
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
# Severity classification by vulnerability class
|
|
13
|
-
SEVERITY_MAP = {
|
|
14
|
-
# ERROR - exploitable vulnerabilities (injection, RCE, deserialization)
|
|
15
|
-
'sql-injection': 'error',
|
|
16
|
-
'sql-injection-query': 'error',
|
|
17
|
-
'sql-injection-sprintf': 'error',
|
|
18
|
-
'sql-injection-where': 'error',
|
|
19
|
-
'sql-injection-order': 'error',
|
|
20
|
-
'sql-injection-raw': 'error',
|
|
21
|
-
'sql-injection-db-cursor': 'error',
|
|
22
|
-
'sql-injection-using-sqlalchemy': 'error',
|
|
23
|
-
'sql-injection-sqlcommand': 'error',
|
|
24
|
-
'sql-injection-sqlquery': 'error',
|
|
25
|
-
'sql-injection-concat': 'error',
|
|
26
|
-
'command-injection': 'error',
|
|
27
|
-
'command-injection-exec': 'error',
|
|
28
|
-
'command-injection-system': 'error',
|
|
29
|
-
'command-injection-open': 'error',
|
|
30
|
-
'command-injection-process-start': 'error',
|
|
31
|
-
'child-process-exec': 'error',
|
|
32
|
-
'spawn-shell': 'error',
|
|
33
|
-
'dangerous-subprocess-use': 'error',
|
|
34
|
-
'dangerous-system-call': 'error',
|
|
35
|
-
'eval-detected': 'error',
|
|
36
|
-
'eval-usage': 'error',
|
|
37
|
-
'exec-detected': 'error',
|
|
38
|
-
'pickle-load': 'error',
|
|
39
|
-
'unsafe-unserialize': 'error',
|
|
40
|
-
'unsafe-yaml-load': 'error',
|
|
41
|
-
'unsafe-marshal': 'error',
|
|
42
|
-
'yaml-load': 'error',
|
|
43
|
-
'file-inclusion': 'error',
|
|
44
|
-
'path-traversal': 'error',
|
|
45
|
-
'xss-echo': 'error',
|
|
46
|
-
'xss-raw': 'error',
|
|
47
|
-
'ssrf': 'error',
|
|
48
|
-
'open-redirect': 'error',
|
|
49
|
-
'backticks-exec': 'error',
|
|
50
|
-
'preg-code-exec': 'error',
|
|
51
|
-
'assert-usage': 'error',
|
|
52
|
-
'insecure-deserialization-binaryformatter': 'error',
|
|
53
|
-
'insecure-deserialization-xmlserializer': 'error',
|
|
54
|
-
'libc-system-call': 'error',
|
|
55
|
-
'format-string-printf': 'error',
|
|
56
|
-
'format-string-syslog': 'error',
|
|
57
|
-
'xss-innerhtml': 'error',
|
|
58
|
-
'xss-response-write': 'error',
|
|
59
|
-
'path-traversal-directory-delete': 'error',
|
|
60
|
-
'path-traversal-file-delete': 'error',
|
|
61
|
-
'path-traversal-file-read': 'error',
|
|
62
|
-
|
|
63
|
-
# WARNING - risky patterns requiring attention
|
|
64
|
-
'innerHTML': 'warning',
|
|
65
|
-
'outerHTML': 'warning',
|
|
66
|
-
'document-write': 'warning',
|
|
67
|
-
'insertAdjacentHTML': 'warning',
|
|
68
|
-
'dangerouslySetInnerHTML': 'warning',
|
|
69
|
-
'function-constructor': 'warning',
|
|
70
|
-
'setTimeout-string': 'warning',
|
|
71
|
-
'strcpy-usage': 'warning',
|
|
72
|
-
'strcat-usage': 'warning',
|
|
73
|
-
'sprintf-usage': 'warning',
|
|
74
|
-
'vsprintf-usage': 'warning',
|
|
75
|
-
'gets-usage': 'warning',
|
|
76
|
-
'system-usage': 'warning',
|
|
77
|
-
'popen-usage': 'warning',
|
|
78
|
-
'hardcoded-password': 'warning',
|
|
79
|
-
'hardcoded-secret': 'warning',
|
|
80
|
-
'hardcoded-api-key': 'warning',
|
|
81
|
-
'hardcoded-connection-string': 'warning',
|
|
82
|
-
'session-secret-hardcoded': 'warning',
|
|
83
|
-
'ssl-verify-disabled': 'warning',
|
|
84
|
-
'curl-ssl-disabled': 'warning',
|
|
85
|
-
'csrf-disabled': 'warning',
|
|
86
|
-
'mass-assignment-permit-all': 'warning',
|
|
87
|
-
'constantize': 'warning',
|
|
88
|
-
'render-inline': 'warning',
|
|
89
|
-
'privileged-container': 'warning',
|
|
90
|
-
'run-as-root': 'warning',
|
|
91
|
-
'allow-privilege-escalation': 'warning',
|
|
92
|
-
'host-network': 'warning',
|
|
93
|
-
'host-pid': 'warning',
|
|
94
|
-
'host-path': 'warning',
|
|
95
|
-
'secrets-in-env': 'warning',
|
|
96
|
-
'cluster-admin-binding': 'warning',
|
|
97
|
-
'capabilities-add': 'warning',
|
|
98
|
-
'no-readonly-root': 'warning',
|
|
99
|
-
'wildcard-rbac': 'warning',
|
|
100
|
-
's3-public-read': 'warning',
|
|
101
|
-
'security-group-open-ingress': 'warning',
|
|
102
|
-
'rds-public-access': 'warning',
|
|
103
|
-
'rds-encryption-disabled': 'warning',
|
|
104
|
-
'rds-deletion-protection': 'warning',
|
|
105
|
-
'cloudtrail-disabled': 'warning',
|
|
106
|
-
'kms-key-rotation': 'warning',
|
|
107
|
-
'ebs-encryption-disabled': 'warning',
|
|
108
|
-
'ec2-imdsv1': 'warning',
|
|
109
|
-
'phpinfo-exposure': 'warning',
|
|
110
|
-
'error-display': 'warning',
|
|
111
|
-
'permissive-cors': 'warning',
|
|
112
|
-
'mcrypt-deprecated': 'warning',
|
|
113
|
-
'aws-access-key-id': 'warning',
|
|
114
|
-
'aws-secret-access-key': 'warning',
|
|
115
|
-
'github-pat': 'warning',
|
|
116
|
-
'stripe-api-key': 'warning',
|
|
117
|
-
'private-key-rsa': 'warning',
|
|
118
|
-
'database-url': 'warning',
|
|
119
|
-
'jwt-token': 'warning',
|
|
120
|
-
'openai-api-key': 'warning',
|
|
121
|
-
'python.lang.security.audit.hardcoded-password': 'warning',
|
|
122
|
-
'python.lang.security.audit.hardcoded-api-key': 'warning',
|
|
123
|
-
'generic.secrets.security.hardcoded-password': 'warning',
|
|
124
|
-
'generic.secrets.security.hardcoded-api-key': 'warning',
|
|
125
|
-
|
|
126
|
-
# INFO - informational / hygiene / low-risk patterns
|
|
127
|
-
'weak-hash-md5': 'info',
|
|
128
|
-
'weak-hash-sha1': 'info',
|
|
129
|
-
'weak-hash': 'info',
|
|
130
|
-
'weak-cipher': 'info',
|
|
131
|
-
'weak-cipher-des': 'info',
|
|
132
|
-
'ecb-mode': 'info',
|
|
133
|
-
'weak-random': 'info',
|
|
134
|
-
'insecure-random': 'info',
|
|
135
|
-
'insecure-hash-md5': 'info',
|
|
136
|
-
'insecure-hash-sha1': 'info',
|
|
137
|
-
'insecure-memset': 'info',
|
|
138
|
-
'strtok-usage': 'info',
|
|
139
|
-
'insecure-tempfile': 'info',
|
|
140
|
-
'unchecked-return': 'info',
|
|
141
|
-
'unsafe-block': 'info',
|
|
142
|
-
'unwrap-usage': 'info',
|
|
143
|
-
'raw-pointer-deref': 'info',
|
|
144
|
-
'panic-usage': 'info',
|
|
145
|
-
'compile-detected': 'info',
|
|
146
|
-
'scanf-usage': 'info',
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
# Confidence classification by rule ID
|
|
150
|
-
CONFIDENCE_MAP = {
|
|
151
|
-
# HIGH - very specific patterns, low false-positive rate
|
|
152
|
-
'sql-injection-db-cursor': 'HIGH',
|
|
153
|
-
'sql-injection-using-sqlalchemy': 'HIGH',
|
|
154
|
-
'sql-injection-sqlcommand': 'HIGH',
|
|
155
|
-
'sql-injection-sqlquery': 'HIGH',
|
|
156
|
-
'sql-injection-concat': 'HIGH',
|
|
157
|
-
'format-string-printf': 'HIGH',
|
|
158
|
-
'format-string-syslog': 'HIGH',
|
|
159
|
-
'pickle-load': 'HIGH',
|
|
160
|
-
'eval-detected': 'HIGH',
|
|
161
|
-
'eval-usage': 'HIGH',
|
|
162
|
-
'exec-detected': 'HIGH',
|
|
163
|
-
'child-process-exec': 'HIGH',
|
|
164
|
-
'dangerous-subprocess-use': 'HIGH',
|
|
165
|
-
'dangerous-system-call': 'HIGH',
|
|
166
|
-
'hardcoded-password': 'HIGH',
|
|
167
|
-
'hardcoded-connection-string': 'HIGH',
|
|
168
|
-
'aws-access-key-id': 'HIGH',
|
|
169
|
-
'github-pat': 'HIGH',
|
|
170
|
-
'stripe-api-key': 'HIGH',
|
|
171
|
-
'private-key-rsa': 'HIGH',
|
|
172
|
-
'openai-api-key': 'HIGH',
|
|
173
|
-
'unsafe-unserialize': 'HIGH',
|
|
174
|
-
'backticks-exec': 'HIGH',
|
|
175
|
-
'preg-code-exec': 'HIGH',
|
|
176
|
-
'file-inclusion': 'HIGH',
|
|
177
|
-
'gets-usage': 'HIGH',
|
|
178
|
-
'insecure-deserialization-binaryformatter': 'HIGH',
|
|
179
|
-
'insecure-deserialization-xmlserializer': 'HIGH',
|
|
180
|
-
|
|
181
|
-
# LOW - broad patterns with high false-positive rate
|
|
182
|
-
'compile-detected': 'LOW',
|
|
183
|
-
'unsafe-block': 'LOW',
|
|
184
|
-
'unwrap-usage': 'LOW',
|
|
185
|
-
'insecure-memset': 'LOW',
|
|
186
|
-
'strtok-usage': 'LOW',
|
|
187
|
-
'unchecked-return': 'LOW',
|
|
188
|
-
'scanf-usage': 'LOW',
|
|
189
|
-
'panic-usage': 'LOW',
|
|
190
|
-
'raw-pointer-deref': 'LOW',
|
|
191
|
-
'insecure-random': 'LOW',
|
|
192
|
-
'weak-random': 'LOW',
|
|
193
|
-
'insecure-hash-md5': 'LOW',
|
|
194
|
-
'insecure-hash-sha1': 'LOW',
|
|
195
|
-
'weak-hash-md5': 'LOW',
|
|
196
|
-
'weak-hash-sha1': 'LOW',
|
|
197
|
-
'weak-hash': 'LOW',
|
|
198
|
-
'database-url': 'LOW',
|
|
199
|
-
|
|
200
|
-
# Everything else defaults to MEDIUM
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
|
|
204
12
|
def _make_finding(rule_id: str, line_idx: int, line: str, col_start: int = 0, col_end: Optional[int] = None,
|
|
205
|
-
message: Optional[str] = None, severity:
|
|
206
|
-
confidence: Optional[str] = None) -> Dict:
|
|
13
|
+
message: Optional[str] = None, severity: str = "warning") -> Dict:
|
|
207
14
|
if col_end is None:
|
|
208
15
|
col_end = max(col_start + 1, len(line.rstrip("\n")))
|
|
209
|
-
if severity is None:
|
|
210
|
-
severity = SEVERITY_MAP.get(rule_id, "warning")
|
|
211
|
-
if confidence is None:
|
|
212
|
-
confidence = CONFIDENCE_MAP.get(rule_id, "MEDIUM")
|
|
213
16
|
return {
|
|
214
17
|
"ruleId": rule_id,
|
|
215
18
|
"message": message or f"[Regex] {rule_id}",
|
|
@@ -219,7 +22,6 @@ def _make_finding(rule_id: str, line_idx: int, line: str, col_start: int = 0, co
|
|
|
219
22
|
"endColumn": col_end,
|
|
220
23
|
"length": max(0, col_end - col_start),
|
|
221
24
|
"severity": severity,
|
|
222
|
-
"confidence": confidence,
|
|
223
25
|
"metadata": {"source": "regex-fallback"},
|
|
224
26
|
"metavariables": {},
|
|
225
27
|
}
|
package/src/cli/init.js
CHANGED
|
@@ -73,12 +73,6 @@ const CLIENT_CONFIGS = {
|
|
|
73
73
|
configKey: 'mcpServers',
|
|
74
74
|
configPath: () => join(vscodeBase(), 'Code', 'User', 'globalStorage', 'sourcegraph.cody-ai', 'mcp_settings.json'),
|
|
75
75
|
buildEntry: () => ({ ...MCP_SERVER_ENTRY })
|
|
76
|
-
},
|
|
77
|
-
'openclaw': {
|
|
78
|
-
name: 'OpenClaw',
|
|
79
|
-
isSkillBased: true, // OpenClaw uses skills, not MCP config
|
|
80
|
-
skillPath: () => join(homedir(), '.openclaw', 'workspace', 'skills', 'security-scanner'),
|
|
81
|
-
configPath: () => join(homedir(), '.openclaw', 'workspace', 'skills', 'security-scanner', 'SKILL.md')
|
|
82
76
|
}
|
|
83
77
|
};
|
|
84
78
|
|
|
@@ -156,87 +150,6 @@ function printInitUsage() {
|
|
|
156
150
|
console.log(' npx agent-security-scanner-mcp init cline --force --name my-scanner\n');
|
|
157
151
|
}
|
|
158
152
|
|
|
159
|
-
// Special installer for OpenClaw (skill-based)
|
|
160
|
-
async function installOpenClawSkill(client, flags) {
|
|
161
|
-
const skillDir = client.skillPath();
|
|
162
|
-
const skillFile = client.configPath();
|
|
163
|
-
|
|
164
|
-
// Find the source skill file (bundled with the package)
|
|
165
|
-
const __dirname = dirname(new URL(import.meta.url).pathname);
|
|
166
|
-
const sourceSkill = join(__dirname, '..', '..', 'skills', 'openclaw', 'SKILL.md');
|
|
167
|
-
|
|
168
|
-
console.log(`\n Client: ${client.name}`);
|
|
169
|
-
console.log(` Skill: ${skillDir}`);
|
|
170
|
-
console.log(` OS: ${platform()} (${process.arch})\n`);
|
|
171
|
-
|
|
172
|
-
// Check if OpenClaw workspace exists
|
|
173
|
-
const openclawDir = join(homedir(), '.openclaw');
|
|
174
|
-
if (!existsSync(openclawDir)) {
|
|
175
|
-
console.log(` OpenClaw not found at ${openclawDir}`);
|
|
176
|
-
console.log(` Please install OpenClaw first: https://openclaw.ai\n`);
|
|
177
|
-
process.exit(1);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Check if source skill exists
|
|
181
|
-
if (!existsSync(sourceSkill)) {
|
|
182
|
-
console.error(` ERROR: Skill source not found at ${sourceSkill}`);
|
|
183
|
-
console.error(` This may be a packaging issue. Please reinstall the package.\n`);
|
|
184
|
-
process.exit(1);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Check if skill already exists
|
|
188
|
-
if (existsSync(skillFile)) {
|
|
189
|
-
const existing = readFileSync(skillFile, 'utf-8');
|
|
190
|
-
const source = readFileSync(sourceSkill, 'utf-8');
|
|
191
|
-
if (existing === source) {
|
|
192
|
-
console.log(` Security scanner skill is already installed (identical).`);
|
|
193
|
-
console.log(` Nothing to do.\n`);
|
|
194
|
-
process.exit(0);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
console.log(` Security scanner skill exists but differs.`);
|
|
198
|
-
if (!flags.force) {
|
|
199
|
-
if (flags.yes) {
|
|
200
|
-
console.log(` Skipping (use --force to overwrite).\n`);
|
|
201
|
-
process.exit(0);
|
|
202
|
-
}
|
|
203
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
204
|
-
const answer = await new Promise((resolve) => {
|
|
205
|
-
rl.question(' Overwrite? (y/N): ', (a) => { rl.close(); resolve(a); });
|
|
206
|
-
});
|
|
207
|
-
if (answer.toLowerCase() !== 'y') {
|
|
208
|
-
console.log(' Aborted.\n');
|
|
209
|
-
process.exit(0);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Dry-run mode
|
|
215
|
-
if (flags.dryRun) {
|
|
216
|
-
console.log(` [dry-run] Would create directory: ${skillDir}`);
|
|
217
|
-
console.log(` [dry-run] Would copy skill from: ${sourceSkill}`);
|
|
218
|
-
console.log(` [dry-run] Would write to: ${skillFile}`);
|
|
219
|
-
console.log(` No changes made.\n`);
|
|
220
|
-
process.exit(0);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// Create skill directory
|
|
224
|
-
if (!existsSync(skillDir)) {
|
|
225
|
-
mkdirSync(skillDir, { recursive: true });
|
|
226
|
-
console.log(` Created directory: ${skillDir}`);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// Copy skill file
|
|
230
|
-
copyFileSync(sourceSkill, skillFile);
|
|
231
|
-
console.log(` Installed skill: ${skillFile}`);
|
|
232
|
-
|
|
233
|
-
console.log(`\n OpenClaw security scanner skill installed successfully!`);
|
|
234
|
-
console.log(`\n Usage in OpenClaw:`);
|
|
235
|
-
console.log(` - The skill will be auto-discovered by OpenClaw`);
|
|
236
|
-
console.log(` - Use /security-scanner to invoke it`);
|
|
237
|
-
console.log(` - Or ask: "scan this prompt for security issues"\n`);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
153
|
export async function runInit(args) {
|
|
241
154
|
const flags = parseInitFlags(args);
|
|
242
155
|
let clientName = flags.client;
|
|
@@ -258,12 +171,6 @@ export async function runInit(args) {
|
|
|
258
171
|
process.exit(1);
|
|
259
172
|
}
|
|
260
173
|
|
|
261
|
-
// Special handling for OpenClaw (skill-based, not MCP config)
|
|
262
|
-
if (client.isSkillBased) {
|
|
263
|
-
await installOpenClawSkill(client, flags);
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
174
|
const configPath = flags.path || client.configPath();
|
|
268
175
|
const serverName = flags.name;
|
|
269
176
|
const entry = client.buildEntry();
|
package/src/fix-patterns.js
CHANGED
|
@@ -63,56 +63,20 @@ export const FIX_TEMPLATES = {
|
|
|
63
63
|
// COMMAND INJECTION
|
|
64
64
|
// ===========================================
|
|
65
65
|
"child-process-exec": {
|
|
66
|
-
description: "Use execFile()
|
|
67
|
-
fix: (line) =>
|
|
68
|
-
// Match: exec("cmd " + arg) -> execFile("cmd", [arg])
|
|
69
|
-
const concatMatch = line.match(/\bexec\s*\(\s*["'](\S+)\s+["']\s*\+\s*(\w+)/);
|
|
70
|
-
if (concatMatch) {
|
|
71
|
-
return line.replace(/\bexec\s*\(\s*["'](\S+)\s+["']\s*\+\s*(\w+)\s*\)/, 'execFile("$1", [$2])');
|
|
72
|
-
}
|
|
73
|
-
// Match: exec(`cmd ${arg}`) -> execFile("cmd", [arg])
|
|
74
|
-
const templateMatch = line.match(/\bexec\s*\(\s*`(\S+)\s+\$\{(\w+)\}`/);
|
|
75
|
-
if (templateMatch) {
|
|
76
|
-
return line.replace(/\bexec\s*\(\s*`(\S+)\s+\$\{(\w+)\}`\s*\)/, 'execFile("$1", [$2])');
|
|
77
|
-
}
|
|
78
|
-
// Match: exec(variable) -> execFile with guidance
|
|
79
|
-
const varMatch = line.match(/\bexec\s*\(\s*(\w+)\s*\)/);
|
|
80
|
-
if (varMatch) {
|
|
81
|
-
return line.replace(/\bexec\s*\(\s*(\w+)\s*\)/, 'execFile($1.split(" ")[0], $1.split(" ").slice(1))');
|
|
82
|
-
}
|
|
83
|
-
// Fallback: comment with guidance
|
|
84
|
-
return '// SECURITY: Use execFile(command, [args]) instead of exec() - ' + line.trim();
|
|
85
|
-
}
|
|
66
|
+
description: "Use execFile() or spawn() with shell: false",
|
|
67
|
+
fix: (line) => line.replace(/\bexec\s*\(/, 'execFile(')
|
|
86
68
|
},
|
|
87
69
|
"spawn-shell": {
|
|
88
70
|
description: "Use spawn with shell: false",
|
|
89
71
|
fix: (line) => line.replace(/shell\s*:\s*true/i, 'shell: false')
|
|
90
72
|
},
|
|
91
73
|
"dangerous-subprocess": {
|
|
92
|
-
description: "Use subprocess.run with list arguments
|
|
93
|
-
fix: (line) =>
|
|
94
|
-
// Replace shell=True with shell=False
|
|
95
|
-
let fixed = line.replace(/shell\s*=\s*True/, 'shell=False');
|
|
96
|
-
// Replace string command with shlex.split() for safe list form
|
|
97
|
-
fixed = fixed.replace(
|
|
98
|
-
/subprocess\.(call|run|Popen)\s*\(\s*["'](.+?)["']/,
|
|
99
|
-
'subprocess.$1(shlex.split("$2")'
|
|
100
|
-
);
|
|
101
|
-
return fixed;
|
|
102
|
-
}
|
|
74
|
+
description: "Use subprocess.run with list arguments",
|
|
75
|
+
fix: (line) => line.replace(/subprocess\.(call|run|Popen)\s*\(\s*["'](.+?)["']\s*,\s*shell\s*=\s*True/, 'subprocess.$1(["$2".split()], shell=False')
|
|
103
76
|
},
|
|
104
77
|
"dangerous-system-call": {
|
|
105
78
|
description: "Use subprocess.run instead of os.system",
|
|
106
|
-
fix: (line) =>
|
|
107
|
-
const match = line.match(/os\.system\s*\(\s*(.+?)\s*\)/);
|
|
108
|
-
if (match) {
|
|
109
|
-
return line.replace(
|
|
110
|
-
/os\.system\s*\(\s*(.+?)\s*\)/,
|
|
111
|
-
'subprocess.run(shlex.split($1), shell=False)'
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
return '# SECURITY: Replace os.system() with subprocess.run(shlex.split(cmd), shell=False)\n# ' + line.trim();
|
|
115
|
-
}
|
|
79
|
+
fix: (line) => line.replace(/os\.system\s*\(/, 'subprocess.run([')
|
|
116
80
|
},
|
|
117
81
|
"command-injection-exec": {
|
|
118
82
|
description: "Use exec.Command with separate arguments",
|
|
@@ -233,11 +197,8 @@ export const FIX_TEMPLATES = {
|
|
|
233
197
|
// INSECURE DESERIALIZATION
|
|
234
198
|
// ===========================================
|
|
235
199
|
"pickle": {
|
|
236
|
-
description: "Use JSON instead of pickle
|
|
237
|
-
fix: (line) =>
|
|
238
|
-
const fixed = line.replace(/pickle\.(load|loads)\s*\(/, 'json.$1(');
|
|
239
|
-
return fixed + ' # NOTE: data must be JSON-formatted';
|
|
240
|
-
}
|
|
200
|
+
description: "Use JSON instead of pickle",
|
|
201
|
+
fix: (line) => line.replace(/pickle\.(load|loads)\s*\(/, 'json.$1(')
|
|
241
202
|
},
|
|
242
203
|
"yaml-load": {
|
|
243
204
|
description: "Use yaml.safe_load()",
|
|
@@ -248,8 +209,8 @@ export const FIX_TEMPLATES = {
|
|
|
248
209
|
fix: (line) => line.replace(/marshal\.(load|loads)\s*\(/, 'json.$1(')
|
|
249
210
|
},
|
|
250
211
|
"shelve": {
|
|
251
|
-
description: "Use JSON or SQLite instead of shelve
|
|
252
|
-
fix: (line) =>
|
|
212
|
+
description: "Use JSON or SQLite instead of shelve",
|
|
213
|
+
fix: (line) => line.replace(/shelve\.open\s*\(/, 'json.load(open(')
|
|
253
214
|
},
|
|
254
215
|
"node-serialize": {
|
|
255
216
|
description: "Use JSON.parse instead of node-serialize",
|
|
@@ -296,12 +257,12 @@ export const FIX_TEMPLATES = {
|
|
|
296
257
|
// PATH TRAVERSAL
|
|
297
258
|
// ===========================================
|
|
298
259
|
"path-traversal": {
|
|
299
|
-
description: "
|
|
260
|
+
description: "Sanitize file paths and use basename",
|
|
300
261
|
fix: (line, lang) => {
|
|
301
|
-
if (lang === 'python') return line.replace(/open\s*\(\s*(\w+)/, 'open(os.path.
|
|
302
|
-
if (lang === 'go') return line.replace(/os\.Open\s*\(\s*(\w+)/, 'os.Open(filepath.
|
|
303
|
-
if (lang === 'java') return line.replace(/new File\s*\(\s*(\w+)/, 'new File($1).
|
|
304
|
-
return line.replace(/readFileSync\s*\(\s*(\w+)/, 'readFileSync(path.
|
|
262
|
+
if (lang === 'python') return line.replace(/open\s*\(\s*(\w+)/, 'open(os.path.basename($1)');
|
|
263
|
+
if (lang === 'go') return line.replace(/os\.Open\s*\(\s*(\w+)/, 'os.Open(filepath.Base($1)');
|
|
264
|
+
if (lang === 'java') return line.replace(/new File\s*\(\s*(\w+)/, 'new File(new File($1).getName()');
|
|
265
|
+
return line.replace(/readFileSync\s*\(\s*(\w+)/, 'readFileSync(path.basename($1)');
|
|
305
266
|
}
|
|
306
267
|
},
|
|
307
268
|
|
|
@@ -446,14 +407,7 @@ export const FIX_TEMPLATES = {
|
|
|
446
407
|
// ===========================================
|
|
447
408
|
"prototype-pollution": {
|
|
448
409
|
description: "Validate object keys before assignment",
|
|
449
|
-
fix: (line) =>
|
|
450
|
-
// Only fix simple single-line assignments like: obj[key] = value
|
|
451
|
-
// Reject lines with multiple bracket accesses or chained assignments
|
|
452
|
-
if (/^\s*\w+\[\w+\]\s*=\s*[^[=]+$/.test(line)) {
|
|
453
|
-
return line.replace(/(\w+)\[(\w+)\]\s*=/, 'if (!["__proto__", "constructor", "prototype"].includes($2)) $1[$2] =');
|
|
454
|
-
}
|
|
455
|
-
return '// SECURITY: Validate key is not __proto__/constructor/prototype before assignment\n// ' + line.trim();
|
|
456
|
-
}
|
|
410
|
+
fix: (line) => line.replace(/(\w+)\[(\w+)\]\s*=/, 'if (!["__proto__", "constructor", "prototype"].includes($2)) $1[$2] =')
|
|
457
411
|
},
|
|
458
412
|
|
|
459
413
|
// ===========================================
|
|
@@ -489,7 +443,7 @@ export const FIX_TEMPLATES = {
|
|
|
489
443
|
// ===========================================
|
|
490
444
|
"helmet-missing": {
|
|
491
445
|
description: "Add helmet middleware for security headers",
|
|
492
|
-
fix: (line) => '
|
|
446
|
+
fix: (line) => 'app.use(helmet()); // Add security headers\n' + line
|
|
493
447
|
},
|
|
494
448
|
|
|
495
449
|
// ===========================================
|
|
@@ -541,10 +495,7 @@ export const FIX_TEMPLATES = {
|
|
|
541
495
|
},
|
|
542
496
|
"run-shell-form": {
|
|
543
497
|
description: "Use exec form for RUN commands",
|
|
544
|
-
fix: (line) => line.replace(/RUN\s+(.+)$/,
|
|
545
|
-
const escaped = cmd.replace(/"/g, '\\"');
|
|
546
|
-
return `RUN ["/bin/sh", "-c", "${escaped}"]`;
|
|
547
|
-
})
|
|
498
|
+
fix: (line) => line.replace(/RUN\s+(.+)$/, 'RUN ["/bin/sh", "-c", "$1"]')
|
|
548
499
|
},
|
|
549
500
|
"sudo-in-dockerfile": {
|
|
550
501
|
description: "Avoid sudo in Dockerfile - use USER directive",
|
|
@@ -2,9 +2,6 @@
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { existsSync, readFileSync } from "fs";
|
|
4
4
|
import { detectLanguage, runAnalyzer, generateFix } from '../utils.js';
|
|
5
|
-
import { deduplicateFindings } from '../dedup.js';
|
|
6
|
-
import { applyContextFilter, detectFrameworks, applyFrameworkAdjustments } from '../context.js';
|
|
7
|
-
import { loadConfig, shouldExcludeFile, applyConfig } from '../config.js';
|
|
8
5
|
|
|
9
6
|
export const fixSecuritySchema = {
|
|
10
7
|
file_path: z.string().describe("Path to the file to fix"),
|
|
@@ -50,48 +47,24 @@ export async function fixSecurity({ file_path, verbosity }) {
|
|
|
50
47
|
};
|
|
51
48
|
}
|
|
52
49
|
|
|
53
|
-
|
|
54
|
-
const config = loadConfig(file_path);
|
|
50
|
+
const issues = runAnalyzer(file_path);
|
|
55
51
|
|
|
56
|
-
|
|
57
|
-
if (shouldExcludeFile(file_path, config)) {
|
|
58
|
-
return {
|
|
59
|
-
content: [{ type: "text", text: JSON.stringify({ file: file_path, message: "File excluded by configuration", fixes_applied: 0 }) }]
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const rawIssues = runAnalyzer(file_path);
|
|
64
|
-
|
|
65
|
-
if (rawIssues.error || !Array.isArray(rawIssues) || rawIssues.length === 0) {
|
|
52
|
+
if (issues.error || !Array.isArray(issues) || issues.length === 0) {
|
|
66
53
|
return {
|
|
67
54
|
content: [{
|
|
68
55
|
type: "text",
|
|
69
56
|
text: JSON.stringify({
|
|
70
|
-
message:
|
|
71
|
-
details:
|
|
57
|
+
message: issues.error ? "Error scanning file" : "No security issues found",
|
|
58
|
+
details: issues
|
|
72
59
|
})
|
|
73
60
|
}]
|
|
74
61
|
};
|
|
75
62
|
}
|
|
76
63
|
|
|
77
|
-
// Cross-engine deduplication
|
|
78
|
-
const dedupedIssues = deduplicateFindings(rawIssues);
|
|
79
|
-
|
|
80
64
|
// Read and fix the file
|
|
81
65
|
const content = readFileSync(file_path, 'utf-8');
|
|
82
66
|
const lines = content.split('\n');
|
|
83
67
|
const language = detectLanguage(file_path);
|
|
84
|
-
|
|
85
|
-
// Context-aware filtering (suppress known module imports)
|
|
86
|
-
const contextFiltered = applyContextFilter(dedupedIssues, file_path, language);
|
|
87
|
-
|
|
88
|
-
// Framework-aware severity adjustment
|
|
89
|
-
const frameworks = detectFrameworks(file_path, language);
|
|
90
|
-
const frameworkAdjusted = applyFrameworkAdjustments(contextFiltered, frameworks);
|
|
91
|
-
|
|
92
|
-
// Apply .scannerrc configuration (rule suppression, severity/confidence thresholds)
|
|
93
|
-
const issues = applyConfig(frameworkAdjusted, file_path, config);
|
|
94
|
-
|
|
95
68
|
const fixes = [];
|
|
96
69
|
|
|
97
70
|
// Apply fixes (process in reverse order to preserve line numbers)
|
package/src/tools/scan-prompt.js
CHANGED
|
@@ -39,12 +39,6 @@ const CATEGORY_WEIGHTS = {
|
|
|
39
39
|
"prompt-injection-privilege": 0.85,
|
|
40
40
|
"prompt-injection-multi-turn": 0.7,
|
|
41
41
|
"prompt-injection-output": 0.9,
|
|
42
|
-
// OpenClaw-specific categories
|
|
43
|
-
"data_exfiltration": 1.0,
|
|
44
|
-
"messaging_abuse": 0.95,
|
|
45
|
-
"credential_theft": 1.0,
|
|
46
|
-
"autonomous_harm": 0.9,
|
|
47
|
-
"service_attack": 0.95,
|
|
48
42
|
"unknown": 0.5
|
|
49
43
|
};
|
|
50
44
|
|
|
@@ -195,69 +189,6 @@ function loadPromptInjectionRules() {
|
|
|
195
189
|
}
|
|
196
190
|
}
|
|
197
191
|
|
|
198
|
-
// Load OpenClaw-specific rules
|
|
199
|
-
function loadOpenClawRules() {
|
|
200
|
-
try {
|
|
201
|
-
const rulesPath = join(__dirname, '..', '..', 'rules', 'openclaw.security.yaml');
|
|
202
|
-
if (!existsSync(rulesPath)) {
|
|
203
|
-
return [];
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const yaml = readFileSync(rulesPath, 'utf-8');
|
|
207
|
-
const rules = [];
|
|
208
|
-
|
|
209
|
-
const ruleBlocks = yaml.split(/^ - id:/m).slice(1);
|
|
210
|
-
|
|
211
|
-
for (const block of ruleBlocks) {
|
|
212
|
-
const lines = (' - id:' + block).split('\n');
|
|
213
|
-
const rule = {
|
|
214
|
-
id: '',
|
|
215
|
-
severity: 'WARNING',
|
|
216
|
-
message: '',
|
|
217
|
-
patterns: [],
|
|
218
|
-
metadata: {}
|
|
219
|
-
};
|
|
220
|
-
|
|
221
|
-
let inPatterns = false;
|
|
222
|
-
|
|
223
|
-
for (const line of lines) {
|
|
224
|
-
if (line.match(/^\s+- id:\s*/)) {
|
|
225
|
-
rule.id = line.replace(/^\s+- id:\s*/, '').trim();
|
|
226
|
-
} else if (line.match(/^\s+severity:\s*/)) {
|
|
227
|
-
rule.severity = line.replace(/^\s+severity:\s*/, '').trim();
|
|
228
|
-
} else if (line.match(/^\s+category:\s*/)) {
|
|
229
|
-
rule.metadata.category = line.replace(/^\s+category:\s*/, '').trim();
|
|
230
|
-
} else if (line.match(/^\s+action:\s*/)) {
|
|
231
|
-
rule.metadata.action = line.replace(/^\s+action:\s*/, '').trim();
|
|
232
|
-
} else if (line.match(/^\s+message:\s*/)) {
|
|
233
|
-
rule.message = line.replace(/^\s+message:\s*["']?/, '').replace(/["']$/, '').trim();
|
|
234
|
-
} else if (line.match(/^\s+patterns:\s*$/)) {
|
|
235
|
-
inPatterns = true;
|
|
236
|
-
} else if (inPatterns && line.match(/^\s+- /)) {
|
|
237
|
-
let pattern = line.replace(/^\s+- /, '').trim();
|
|
238
|
-
pattern = pattern.replace(/^["']|["']$/g, '');
|
|
239
|
-
pattern = pattern.replace(/\\\\/g, '\\');
|
|
240
|
-
if (pattern) rule.patterns.push(pattern);
|
|
241
|
-
} else if (line.match(/^\s+\w+:/) && !line.match(/^\s+- /)) {
|
|
242
|
-
inPatterns = false;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
if (rule.id && rule.patterns.length > 0) {
|
|
247
|
-
// Set confidence and risk score based on severity
|
|
248
|
-
rule.metadata.confidence = rule.severity === 'CRITICAL' ? 'HIGH' : 'MEDIUM';
|
|
249
|
-
rule.metadata.risk_score = rule.severity === 'CRITICAL' ? '90' : '70';
|
|
250
|
-
rules.push(rule);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
return rules;
|
|
255
|
-
} catch (error) {
|
|
256
|
-
console.error("Error loading OpenClaw rules:", error.message);
|
|
257
|
-
return [];
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
192
|
// Calculate risk score from findings
|
|
262
193
|
function calculateRiskScore(findings, context) {
|
|
263
194
|
if (findings.length === 0) return 0;
|
|
@@ -446,8 +377,7 @@ export async function scanAgentPrompt({ prompt_text, context, verbosity }) {
|
|
|
446
377
|
// Load rules
|
|
447
378
|
const agentRules = loadAgentAttackRules();
|
|
448
379
|
const promptRules = loadPromptInjectionRules();
|
|
449
|
-
const
|
|
450
|
-
const allRules = [...agentRules, ...promptRules, ...openclawRules];
|
|
380
|
+
const allRules = [...agentRules, ...promptRules];
|
|
451
381
|
|
|
452
382
|
// 2.7: Extract content from code blocks and append to scan text
|
|
453
383
|
let expandedText = prompt_text;
|