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 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()