agent-security-scanner-mcp 1.0.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 +106 -0
- package/analyzer.py +119 -0
- package/index.js +269 -0
- package/package.json +48 -0
- package/rules/__init__.py +167 -0
- package/rules/dockerfile.security.yaml +291 -0
- package/rules/generic.secrets.yaml +503 -0
- package/rules/go.security.yaml +380 -0
- package/rules/java.security.yaml +453 -0
- package/rules/javascript.security.yaml +504 -0
- package/rules/python.security.yaml +602 -0
package/README.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# agent-security-scanner-mcp
|
|
2
|
+
|
|
3
|
+
An MCP (Model Context Protocol) server for security vulnerability scanning. Detects SQL injection, XSS, command injection, hardcoded secrets, and 160+ security issues.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g agent-security-scanner-mcp
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or run directly with npx:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx agent-security-scanner-mcp
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Requirements
|
|
18
|
+
|
|
19
|
+
- Node.js >= 18.0.0
|
|
20
|
+
- Python 3.x (for the analyzer)
|
|
21
|
+
|
|
22
|
+
## Configuration
|
|
23
|
+
|
|
24
|
+
### Claude Desktop
|
|
25
|
+
|
|
26
|
+
Add to your `claude_desktop_config.json`:
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"mcpServers": {
|
|
31
|
+
"security-scanner": {
|
|
32
|
+
"command": "npx",
|
|
33
|
+
"args": ["-y", "agent-security-scanner-mcp"]
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Claude Code
|
|
40
|
+
|
|
41
|
+
Add to your MCP settings:
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"mcpServers": {
|
|
46
|
+
"security-scanner": {
|
|
47
|
+
"command": "npx",
|
|
48
|
+
"args": ["-y", "agent-security-scanner-mcp"]
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Available Tools
|
|
55
|
+
|
|
56
|
+
### `scan_security`
|
|
57
|
+
|
|
58
|
+
Scan a file for security vulnerabilities and return issues with suggested fixes.
|
|
59
|
+
|
|
60
|
+
**Parameters:**
|
|
61
|
+
- `file_path` (string): Path to the file to scan
|
|
62
|
+
|
|
63
|
+
**Returns:** List of security issues with severity, CWE references, and fix suggestions.
|
|
64
|
+
|
|
65
|
+
### `fix_security`
|
|
66
|
+
|
|
67
|
+
Scan a file and return the fixed content with all security issues resolved.
|
|
68
|
+
|
|
69
|
+
**Parameters:**
|
|
70
|
+
- `file_path` (string): Path to the file to fix
|
|
71
|
+
|
|
72
|
+
**Returns:** Fixed file content with applied security fixes.
|
|
73
|
+
|
|
74
|
+
### `list_security_rules`
|
|
75
|
+
|
|
76
|
+
List all available security fix templates and their descriptions.
|
|
77
|
+
|
|
78
|
+
## Detected Vulnerabilities
|
|
79
|
+
|
|
80
|
+
| Category | Examples |
|
|
81
|
+
|----------|----------|
|
|
82
|
+
| Injection | SQL injection, Command injection, XSS |
|
|
83
|
+
| Secrets | Hardcoded API keys, passwords, private keys |
|
|
84
|
+
| Cryptography | Weak hashing (MD5, SHA1), insecure random |
|
|
85
|
+
| Deserialization | Pickle, unsafe YAML load |
|
|
86
|
+
| Network | SSL verification disabled, HTTP usage |
|
|
87
|
+
| Path Traversal | Unsanitized file paths |
|
|
88
|
+
|
|
89
|
+
## Supported Languages
|
|
90
|
+
|
|
91
|
+
- JavaScript / TypeScript
|
|
92
|
+
- Python
|
|
93
|
+
- Java
|
|
94
|
+
- Go
|
|
95
|
+
- Ruby
|
|
96
|
+
- PHP
|
|
97
|
+
- Dockerfile
|
|
98
|
+
- Generic (secrets detection)
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
MIT
|
|
103
|
+
|
|
104
|
+
## Repository
|
|
105
|
+
|
|
106
|
+
https://github.com/sinewaveai/agent-security-layer-fork
|
package/analyzer.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import json
|
|
3
|
+
import re
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
# Add the directory containing this script to the path
|
|
7
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
8
|
+
|
|
9
|
+
from rules import get_rules, get_rules_for_language, get_rule_stats
|
|
10
|
+
|
|
11
|
+
# File extension to language mapping
|
|
12
|
+
EXTENSION_MAP = {
|
|
13
|
+
'.py': 'python',
|
|
14
|
+
'.js': 'javascript',
|
|
15
|
+
'.ts': 'typescript',
|
|
16
|
+
'.tsx': 'typescript',
|
|
17
|
+
'.jsx': 'javascript',
|
|
18
|
+
'.java': 'java',
|
|
19
|
+
'.go': 'go',
|
|
20
|
+
'.rb': 'ruby',
|
|
21
|
+
'.php': 'php',
|
|
22
|
+
'.cs': 'csharp',
|
|
23
|
+
'.rs': 'rust',
|
|
24
|
+
'.c': 'c',
|
|
25
|
+
'.cpp': 'cpp',
|
|
26
|
+
'.h': 'c',
|
|
27
|
+
'.hpp': 'cpp',
|
|
28
|
+
'.sql': 'sql',
|
|
29
|
+
'.dockerfile': 'dockerfile',
|
|
30
|
+
'.yaml': 'yaml',
|
|
31
|
+
'.yml': 'yaml',
|
|
32
|
+
'.json': 'json',
|
|
33
|
+
'.tf': 'terraform',
|
|
34
|
+
'.hcl': 'terraform',
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
def detect_language(file_path):
|
|
38
|
+
"""Detect the programming language from file extension or name"""
|
|
39
|
+
basename = os.path.basename(file_path).lower()
|
|
40
|
+
|
|
41
|
+
if basename == 'dockerfile' or basename.startswith('dockerfile.'):
|
|
42
|
+
return 'dockerfile'
|
|
43
|
+
|
|
44
|
+
_, ext = os.path.splitext(file_path.lower())
|
|
45
|
+
return EXTENSION_MAP.get(ext, 'generic')
|
|
46
|
+
|
|
47
|
+
def analyze_file(file_path):
|
|
48
|
+
"""Analyze a single file for security vulnerabilities"""
|
|
49
|
+
issues = []
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
language = detect_language(file_path)
|
|
53
|
+
rules = get_rules_for_language(language)
|
|
54
|
+
|
|
55
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
56
|
+
lines = f.readlines()
|
|
57
|
+
content = ''.join(lines)
|
|
58
|
+
|
|
59
|
+
for line_index, line in enumerate(lines):
|
|
60
|
+
original_line = line
|
|
61
|
+
line = line.strip()
|
|
62
|
+
if not line:
|
|
63
|
+
continue
|
|
64
|
+
|
|
65
|
+
# Skip comment-only lines (basic detection)
|
|
66
|
+
if line.startswith('#') or line.startswith('//') or line.startswith('*'):
|
|
67
|
+
continue
|
|
68
|
+
|
|
69
|
+
for rule_id, rule in rules.items():
|
|
70
|
+
for pattern in rule['patterns']:
|
|
71
|
+
try:
|
|
72
|
+
# Use IGNORECASE for better detection (API_KEY vs api_key)
|
|
73
|
+
matches = re.finditer(pattern, line, re.IGNORECASE)
|
|
74
|
+
for match in matches:
|
|
75
|
+
# Calculate column based on original line (preserve indentation)
|
|
76
|
+
col_offset = len(original_line) - len(original_line.lstrip())
|
|
77
|
+
issues.append({
|
|
78
|
+
'ruleId': rule['id'],
|
|
79
|
+
'message': f"[{rule['name']}] {rule['message']}",
|
|
80
|
+
'line': line_index,
|
|
81
|
+
'column': match.start() + col_offset,
|
|
82
|
+
'length': match.end() - match.start(),
|
|
83
|
+
'severity': rule['severity'],
|
|
84
|
+
'metadata': rule.get('metadata', {})
|
|
85
|
+
})
|
|
86
|
+
except re.error:
|
|
87
|
+
# Skip invalid regex patterns
|
|
88
|
+
continue
|
|
89
|
+
|
|
90
|
+
except Exception as e:
|
|
91
|
+
return {'error': str(e)}
|
|
92
|
+
|
|
93
|
+
# Deduplicate issues (same rule, same line)
|
|
94
|
+
seen = set()
|
|
95
|
+
unique_issues = []
|
|
96
|
+
for issue in issues:
|
|
97
|
+
key = (issue['ruleId'], issue['line'], issue['column'])
|
|
98
|
+
if key not in seen:
|
|
99
|
+
seen.add(key)
|
|
100
|
+
unique_issues.append(issue)
|
|
101
|
+
|
|
102
|
+
return unique_issues
|
|
103
|
+
|
|
104
|
+
def main():
|
|
105
|
+
if len(sys.argv) < 2:
|
|
106
|
+
print(json.dumps({'error': 'No file path provided'}))
|
|
107
|
+
sys.exit(1)
|
|
108
|
+
|
|
109
|
+
file_path = sys.argv[1]
|
|
110
|
+
|
|
111
|
+
if not os.path.exists(file_path):
|
|
112
|
+
print(json.dumps({'error': f'File not found: {file_path}'}))
|
|
113
|
+
sys.exit(1)
|
|
114
|
+
|
|
115
|
+
results = analyze_file(file_path)
|
|
116
|
+
print(json.dumps(results))
|
|
117
|
+
|
|
118
|
+
if __name__ == '__main__':
|
|
119
|
+
main()
|
package/index.js
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { execSync } from "child_process";
|
|
7
|
+
import { readFileSync, existsSync } from "fs";
|
|
8
|
+
import { dirname, join } from "path";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
10
|
+
|
|
11
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
|
|
13
|
+
// Security fix templates
|
|
14
|
+
const FIX_TEMPLATES = {
|
|
15
|
+
"sql-injection": {
|
|
16
|
+
description: "Use parameterized queries instead of string concatenation",
|
|
17
|
+
fix: (line) => line.replace(/["']([^"']*)\s*["']\s*\+\s*(\w+)/, '"$1?", [$2]')
|
|
18
|
+
},
|
|
19
|
+
"innerhtml": {
|
|
20
|
+
description: "Use textContent or DOMPurify.sanitize()",
|
|
21
|
+
fix: (line) => line.replace(/\.innerHTML\s*=/, '.textContent =')
|
|
22
|
+
},
|
|
23
|
+
"child-process-exec": {
|
|
24
|
+
description: "Use execFile() or spawn() with shell: false",
|
|
25
|
+
fix: (line) => line.replace(/\bexec\s*\(/, 'execFile(')
|
|
26
|
+
},
|
|
27
|
+
"hardcoded": {
|
|
28
|
+
description: "Use environment variables",
|
|
29
|
+
fix: (line, lang) => {
|
|
30
|
+
if (lang === 'python') {
|
|
31
|
+
return line.replace(/=\s*["'][^"']+["']/, '= os.environ.get("SECRET")');
|
|
32
|
+
}
|
|
33
|
+
return line.replace(/[:=]\s*["'][^"']+["']/, ': process.env.SECRET');
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"md5": {
|
|
37
|
+
description: "Use SHA-256 or stronger",
|
|
38
|
+
fix: (line) => line.replace(/md5/gi, 'sha256')
|
|
39
|
+
},
|
|
40
|
+
"sha1": {
|
|
41
|
+
description: "Use SHA-256 or stronger",
|
|
42
|
+
fix: (line) => line.replace(/sha1/gi, 'sha256')
|
|
43
|
+
},
|
|
44
|
+
"pickle": {
|
|
45
|
+
description: "Use JSON instead of pickle",
|
|
46
|
+
fix: (line) => line.replace(/pickle\.(load|loads)/, 'json.$1')
|
|
47
|
+
},
|
|
48
|
+
"yaml.load": {
|
|
49
|
+
description: "Use yaml.safe_load()",
|
|
50
|
+
fix: (line) => line.replace(/yaml\.load\s*\(/, 'yaml.safe_load(')
|
|
51
|
+
},
|
|
52
|
+
"verify=false": {
|
|
53
|
+
description: "Enable SSL verification",
|
|
54
|
+
fix: (line) => line.replace(/verify\s*=\s*False/i, 'verify=True')
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Detect language from file extension
|
|
59
|
+
function detectLanguage(filePath) {
|
|
60
|
+
const ext = filePath.split('.').pop().toLowerCase();
|
|
61
|
+
const langMap = {
|
|
62
|
+
'py': 'python', 'js': 'javascript', 'ts': 'typescript',
|
|
63
|
+
'tsx': 'typescript', 'jsx': 'javascript', 'java': 'java',
|
|
64
|
+
'go': 'go', 'rb': 'ruby', 'php': 'php'
|
|
65
|
+
};
|
|
66
|
+
return langMap[ext] || 'generic';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Run the Python analyzer
|
|
70
|
+
function runAnalyzer(filePath) {
|
|
71
|
+
try {
|
|
72
|
+
const analyzerPath = join(__dirname, 'analyzer.py');
|
|
73
|
+
const result = execSync(`python3 "${analyzerPath}" "${filePath}"`, {
|
|
74
|
+
encoding: 'utf-8',
|
|
75
|
+
timeout: 30000
|
|
76
|
+
});
|
|
77
|
+
return JSON.parse(result);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
return { error: error.message };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Generate fix suggestion for an issue
|
|
84
|
+
function generateFix(issue, line, language) {
|
|
85
|
+
const ruleId = issue.ruleId.toLowerCase();
|
|
86
|
+
|
|
87
|
+
for (const [pattern, template] of Object.entries(FIX_TEMPLATES)) {
|
|
88
|
+
if (ruleId.includes(pattern)) {
|
|
89
|
+
return {
|
|
90
|
+
description: template.description,
|
|
91
|
+
original: line,
|
|
92
|
+
fixed: template.fix(line, language)
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
description: "Review and fix manually based on the security rule",
|
|
99
|
+
original: line,
|
|
100
|
+
fixed: null
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Create MCP Server
|
|
105
|
+
const server = new McpServer(
|
|
106
|
+
{
|
|
107
|
+
name: "security-scanner",
|
|
108
|
+
version: "1.0.0",
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
capabilities: {
|
|
112
|
+
tools: {},
|
|
113
|
+
},
|
|
114
|
+
}
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// Register scan_security tool
|
|
118
|
+
server.tool(
|
|
119
|
+
"scan_security",
|
|
120
|
+
"Scan a file for security vulnerabilities and return issues with suggested fixes",
|
|
121
|
+
{
|
|
122
|
+
file_path: z.string().describe("Path to the file to scan")
|
|
123
|
+
},
|
|
124
|
+
async ({ file_path }) => {
|
|
125
|
+
if (!existsSync(file_path)) {
|
|
126
|
+
return {
|
|
127
|
+
content: [{ type: "text", text: JSON.stringify({ error: "File not found" }) }]
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const issues = runAnalyzer(file_path);
|
|
132
|
+
|
|
133
|
+
if (issues.error) {
|
|
134
|
+
return {
|
|
135
|
+
content: [{ type: "text", text: JSON.stringify(issues) }]
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Read file content for fix suggestions
|
|
140
|
+
const content = readFileSync(file_path, 'utf-8');
|
|
141
|
+
const lines = content.split('\n');
|
|
142
|
+
const language = detectLanguage(file_path);
|
|
143
|
+
|
|
144
|
+
// Enhance issues with fix suggestions
|
|
145
|
+
const enhancedIssues = issues.map(issue => {
|
|
146
|
+
const line = lines[issue.line] || '';
|
|
147
|
+
const fix = generateFix(issue, line, language);
|
|
148
|
+
return {
|
|
149
|
+
...issue,
|
|
150
|
+
line_content: line.trim(),
|
|
151
|
+
suggested_fix: fix
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
content: [{
|
|
157
|
+
type: "text",
|
|
158
|
+
text: JSON.stringify({
|
|
159
|
+
file: file_path,
|
|
160
|
+
language: language,
|
|
161
|
+
issues_count: enhancedIssues.length,
|
|
162
|
+
issues: enhancedIssues
|
|
163
|
+
}, null, 2)
|
|
164
|
+
}]
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
// Register fix_security tool
|
|
170
|
+
server.tool(
|
|
171
|
+
"fix_security",
|
|
172
|
+
"Scan a file and return the fixed content with all security issues resolved",
|
|
173
|
+
{
|
|
174
|
+
file_path: z.string().describe("Path to the file to fix")
|
|
175
|
+
},
|
|
176
|
+
async ({ file_path }) => {
|
|
177
|
+
if (!existsSync(file_path)) {
|
|
178
|
+
return {
|
|
179
|
+
content: [{ type: "text", text: JSON.stringify({ error: "File not found" }) }]
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const issues = runAnalyzer(file_path);
|
|
184
|
+
|
|
185
|
+
if (issues.error || !Array.isArray(issues) || issues.length === 0) {
|
|
186
|
+
return {
|
|
187
|
+
content: [{
|
|
188
|
+
type: "text",
|
|
189
|
+
text: JSON.stringify({
|
|
190
|
+
message: issues.error ? "Error scanning file" : "No security issues found",
|
|
191
|
+
details: issues
|
|
192
|
+
})
|
|
193
|
+
}]
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Read and fix the file
|
|
198
|
+
const content = readFileSync(file_path, 'utf-8');
|
|
199
|
+
const lines = content.split('\n');
|
|
200
|
+
const language = detectLanguage(file_path);
|
|
201
|
+
const fixes = [];
|
|
202
|
+
|
|
203
|
+
// Apply fixes (process in reverse order to preserve line numbers)
|
|
204
|
+
const sortedIssues = [...issues].sort((a, b) => b.line - a.line);
|
|
205
|
+
|
|
206
|
+
for (const issue of sortedIssues) {
|
|
207
|
+
const lineIndex = issue.line;
|
|
208
|
+
if (lineIndex >= 0 && lineIndex < lines.length) {
|
|
209
|
+
const originalLine = lines[lineIndex];
|
|
210
|
+
const fix = generateFix(issue, originalLine, language);
|
|
211
|
+
|
|
212
|
+
if (fix.fixed && fix.fixed !== originalLine) {
|
|
213
|
+
lines[lineIndex] = fix.fixed;
|
|
214
|
+
fixes.push({
|
|
215
|
+
line: lineIndex + 1,
|
|
216
|
+
rule: issue.ruleId,
|
|
217
|
+
original: originalLine.trim(),
|
|
218
|
+
fixed: fix.fixed.trim(),
|
|
219
|
+
description: fix.description
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
content: [{
|
|
227
|
+
type: "text",
|
|
228
|
+
text: JSON.stringify({
|
|
229
|
+
file: file_path,
|
|
230
|
+
fixes_applied: fixes.length,
|
|
231
|
+
fixes: fixes,
|
|
232
|
+
fixed_content: lines.join('\n')
|
|
233
|
+
}, null, 2)
|
|
234
|
+
}]
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
// Register list_security_rules tool
|
|
240
|
+
server.tool(
|
|
241
|
+
"list_security_rules",
|
|
242
|
+
"List all available security fix templates and their descriptions",
|
|
243
|
+
{},
|
|
244
|
+
async () => {
|
|
245
|
+
const rules = Object.entries(FIX_TEMPLATES).map(([id, template]) => ({
|
|
246
|
+
pattern: id,
|
|
247
|
+
description: template.description
|
|
248
|
+
}));
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
content: [{
|
|
252
|
+
type: "text",
|
|
253
|
+
text: JSON.stringify({ rules }, null, 2)
|
|
254
|
+
}]
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
// Start the server with stdio transport
|
|
260
|
+
async function main() {
|
|
261
|
+
const transport = new StdioServerTransport();
|
|
262
|
+
await server.connect(transport);
|
|
263
|
+
console.error("Security Scanner MCP Server running on stdio");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
main().catch((error) => {
|
|
267
|
+
console.error("Fatal error:", error);
|
|
268
|
+
process.exit(1);
|
|
269
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agent-security-scanner-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for security vulnerability scanning - detects SQL injection, XSS, command injection, hardcoded secrets, and more",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"agent-security-scanner-mcp": "./index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node index.js"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"mcp",
|
|
15
|
+
"model-context-protocol",
|
|
16
|
+
"claude",
|
|
17
|
+
"security",
|
|
18
|
+
"scanner",
|
|
19
|
+
"vulnerability",
|
|
20
|
+
"sast",
|
|
21
|
+
"code-analysis",
|
|
22
|
+
"sql-injection",
|
|
23
|
+
"xss",
|
|
24
|
+
"secrets-detection"
|
|
25
|
+
],
|
|
26
|
+
"author": "",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/sinewaveai/agent-security-layer-fork.git"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/sinewaveai/agent-security-layer-fork#readme",
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/sinewaveai/agent-security-layer-fork/issues"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=18.0.0"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@modelcontextprotocol/sdk": "^1.25.3",
|
|
41
|
+
"zod": "^4.3.6"
|
|
42
|
+
},
|
|
43
|
+
"files": [
|
|
44
|
+
"index.js",
|
|
45
|
+
"analyzer.py",
|
|
46
|
+
"rules/**"
|
|
47
|
+
]
|
|
48
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Rule loader for YAML-based security rules
|
|
2
|
+
# Aligned with Semgrep registry format
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
import yaml
|
|
9
|
+
HAS_YAML = True
|
|
10
|
+
except ImportError:
|
|
11
|
+
HAS_YAML = False
|
|
12
|
+
|
|
13
|
+
RULES_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
14
|
+
|
|
15
|
+
def load_yaml_rules():
|
|
16
|
+
"""Load all YAML rule files from the rules directory"""
|
|
17
|
+
rules = {}
|
|
18
|
+
|
|
19
|
+
if not HAS_YAML:
|
|
20
|
+
print("Warning: PyYAML not installed. Using fallback rules.")
|
|
21
|
+
return rules
|
|
22
|
+
|
|
23
|
+
for filename in os.listdir(RULES_DIR):
|
|
24
|
+
if filename.endswith('.yaml') or filename.endswith('.yml'):
|
|
25
|
+
filepath = os.path.join(RULES_DIR, filename)
|
|
26
|
+
try:
|
|
27
|
+
with open(filepath, 'r', encoding='utf-8') as f:
|
|
28
|
+
data = yaml.safe_load(f)
|
|
29
|
+
if data and 'rules' in data:
|
|
30
|
+
for rule in data['rules']:
|
|
31
|
+
rule_id = rule.get('id', '')
|
|
32
|
+
if rule_id:
|
|
33
|
+
rules[rule_id] = {
|
|
34
|
+
'id': rule_id,
|
|
35
|
+
'name': rule_id.split('.')[-1].replace('-', ' ').title(),
|
|
36
|
+
'patterns': rule.get('patterns', []),
|
|
37
|
+
'message': rule.get('message', ''),
|
|
38
|
+
'severity': rule.get('severity', 'WARNING').lower(),
|
|
39
|
+
'languages': rule.get('languages', ['generic']),
|
|
40
|
+
'metadata': rule.get('metadata', {})
|
|
41
|
+
}
|
|
42
|
+
except Exception as e:
|
|
43
|
+
print(f"Error loading {filename}: {e}")
|
|
44
|
+
|
|
45
|
+
return rules
|
|
46
|
+
|
|
47
|
+
# Fallback rules if YAML is not available
|
|
48
|
+
FALLBACK_RULES = {
|
|
49
|
+
'python.lang.security.audit.sql-injection': {
|
|
50
|
+
'id': 'python.lang.security.audit.sql-injection',
|
|
51
|
+
'name': 'SQL Injection',
|
|
52
|
+
'patterns': [
|
|
53
|
+
r'execute\s*\(\s*["\'].*\$\{.*\}.*["\']',
|
|
54
|
+
r'query\s*\(\s*["\'].*\+.*["\']',
|
|
55
|
+
r'SELECT\s+.*\s+FROM\s+.*\s+WHERE\s+.*=.*\+',
|
|
56
|
+
r'cursor\.execute\s*\(\s*[^)]*%.*\)',
|
|
57
|
+
],
|
|
58
|
+
'message': 'Possible SQL injection vulnerability detected. Use parameterized queries.',
|
|
59
|
+
'severity': 'error',
|
|
60
|
+
'languages': ['python'],
|
|
61
|
+
'metadata': {
|
|
62
|
+
'cwe': 'CWE-89',
|
|
63
|
+
'owasp': 'A03:2021 - Injection',
|
|
64
|
+
'confidence': 'MEDIUM'
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
'javascript.browser.security.dom-based-xss': {
|
|
68
|
+
'id': 'javascript.browser.security.dom-based-xss',
|
|
69
|
+
'name': 'Cross-Site Scripting (XSS)',
|
|
70
|
+
'patterns': [
|
|
71
|
+
r'innerHTML\s*=\s*',
|
|
72
|
+
r'outerHTML\s*=\s*',
|
|
73
|
+
r'document\.write\s*\(',
|
|
74
|
+
r'\.html\s*\(\s*[^)]*\+',
|
|
75
|
+
],
|
|
76
|
+
'message': 'Possible XSS vulnerability. Sanitize user input before rendering.',
|
|
77
|
+
'severity': 'error',
|
|
78
|
+
'languages': ['javascript', 'typescript'],
|
|
79
|
+
'metadata': {
|
|
80
|
+
'cwe': 'CWE-79',
|
|
81
|
+
'owasp': 'A03:2021 - Injection',
|
|
82
|
+
'confidence': 'MEDIUM'
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
'generic.secrets.security.hardcoded-secret': {
|
|
86
|
+
'id': 'generic.secrets.security.hardcoded-secret',
|
|
87
|
+
'name': 'Hardcoded Secrets',
|
|
88
|
+
'patterns': [
|
|
89
|
+
r'(api|secret|private)[_-]?key\s*[:=]\s*["\'][^"\']{20,}["\']',
|
|
90
|
+
r'password\s*[:=]\s*["\'][^"\']{6,}["\']',
|
|
91
|
+
r'["\'](sk_live_[A-Za-z0-9]{24,})["\']',
|
|
92
|
+
r'["\'](sk_test_[A-Za-z0-9]{24,})["\']',
|
|
93
|
+
r'["\'](ghp_[A-Za-z0-9]{30,})["\']',
|
|
94
|
+
],
|
|
95
|
+
'message': 'Possible hardcoded secret detected. Use environment variables instead.',
|
|
96
|
+
'severity': 'warning',
|
|
97
|
+
'languages': ['generic'],
|
|
98
|
+
'metadata': {
|
|
99
|
+
'cwe': 'CWE-798',
|
|
100
|
+
'owasp': 'A07:2021 - Identification and Authentication Failures',
|
|
101
|
+
'confidence': 'HIGH'
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
def get_rules():
|
|
107
|
+
"""Get all rules - from YAML if available, otherwise fallback"""
|
|
108
|
+
yaml_rules = load_yaml_rules()
|
|
109
|
+
if yaml_rules:
|
|
110
|
+
return yaml_rules
|
|
111
|
+
return FALLBACK_RULES
|
|
112
|
+
|
|
113
|
+
def get_rules_for_language(language):
|
|
114
|
+
"""Get rules applicable to a specific language"""
|
|
115
|
+
all_rules = get_rules()
|
|
116
|
+
applicable_rules = {}
|
|
117
|
+
|
|
118
|
+
language = language.lower()
|
|
119
|
+
|
|
120
|
+
for rule_id, rule in all_rules.items():
|
|
121
|
+
rule_languages = [lang.lower() for lang in rule.get('languages', ['generic'])]
|
|
122
|
+
if language in rule_languages or 'generic' in rule_languages:
|
|
123
|
+
applicable_rules[rule_id] = rule
|
|
124
|
+
|
|
125
|
+
return applicable_rules
|
|
126
|
+
|
|
127
|
+
def get_rules_by_category(category):
|
|
128
|
+
"""Get rules by category (e.g., 'injection', 'crypto', 'secrets')"""
|
|
129
|
+
all_rules = get_rules()
|
|
130
|
+
category_rules = {}
|
|
131
|
+
|
|
132
|
+
category = category.lower()
|
|
133
|
+
|
|
134
|
+
for rule_id, rule in all_rules.items():
|
|
135
|
+
if category in rule_id.lower():
|
|
136
|
+
category_rules[rule_id] = rule
|
|
137
|
+
|
|
138
|
+
return category_rules
|
|
139
|
+
|
|
140
|
+
def get_rule_stats():
|
|
141
|
+
"""Get statistics about loaded rules"""
|
|
142
|
+
all_rules = get_rules()
|
|
143
|
+
|
|
144
|
+
stats = {
|
|
145
|
+
'total': len(all_rules),
|
|
146
|
+
'by_severity': {'error': 0, 'warning': 0, 'info': 0},
|
|
147
|
+
'by_language': {},
|
|
148
|
+
'by_category': {}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
for rule_id, rule in all_rules.items():
|
|
152
|
+
severity = rule.get('severity', 'warning').lower()
|
|
153
|
+
stats['by_severity'][severity] = stats['by_severity'].get(severity, 0) + 1
|
|
154
|
+
|
|
155
|
+
for lang in rule.get('languages', ['generic']):
|
|
156
|
+
lang = lang.lower()
|
|
157
|
+
stats['by_language'][lang] = stats['by_language'].get(lang, 0) + 1
|
|
158
|
+
|
|
159
|
+
parts = rule_id.split('.')
|
|
160
|
+
if len(parts) >= 3:
|
|
161
|
+
category = parts[2] if parts[2] != 'lang' else parts[3] if len(parts) > 3 else parts[2]
|
|
162
|
+
stats['by_category'][category] = stats['by_category'].get(category, 0) + 1
|
|
163
|
+
|
|
164
|
+
return stats
|
|
165
|
+
|
|
166
|
+
# Export for backward compatibility
|
|
167
|
+
RULES = get_rules()
|