@rigour-labs/core 2.16.0 → 2.17.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.
@@ -3,4 +3,5 @@ import { Failure } from '../../types/index.js';
3
3
  export declare class PythonHandler extends ASTHandler {
4
4
  supports(file: string): boolean;
5
5
  run(context: ASTHandlerContext): Promise<Failure[]>;
6
+ private getSecurityHint;
6
7
  }
@@ -29,16 +29,19 @@ export class PythonHandler extends ASTHandler {
29
29
  input: context.content,
30
30
  cwd: context.cwd
31
31
  });
32
- const metrics = JSON.parse(stdout);
33
- if (metrics.error)
32
+ const result = JSON.parse(stdout);
33
+ if (result.error)
34
34
  return [];
35
35
  const astConfig = this.config.ast || {};
36
+ const safetyConfig = this.config.safety || {};
36
37
  const maxComplexity = astConfig.complexity || 10;
37
38
  const maxParams = astConfig.max_params || 5;
38
39
  const maxMethods = astConfig.max_methods || 10;
40
+ // Process metrics (complexity, params, methods)
41
+ const metrics = result.metrics || [];
39
42
  for (const item of metrics) {
40
43
  if (item.type === 'function') {
41
- if (item.parameters > maxParams) {
44
+ if (item.parameters && item.parameters > maxParams) {
42
45
  failures.push({
43
46
  id: 'AST_MAX_PARAMS',
44
47
  title: `Function '${item.name}' has ${item.parameters} parameters (max: ${maxParams})`,
@@ -47,7 +50,7 @@ export class PythonHandler extends ASTHandler {
47
50
  hint: `Reduce number of parameters or use an options object.`
48
51
  });
49
52
  }
50
- if (item.complexity > maxComplexity) {
53
+ if (item.complexity && item.complexity > maxComplexity) {
51
54
  failures.push({
52
55
  id: 'AST_COMPLEXITY',
53
56
  title: `Function '${item.name}' has complexity of ${item.complexity} (max: ${maxComplexity})`,
@@ -58,7 +61,7 @@ export class PythonHandler extends ASTHandler {
58
61
  }
59
62
  }
60
63
  else if (item.type === 'class') {
61
- if (item.methods > maxMethods) {
64
+ if (item.methods && item.methods > maxMethods) {
62
65
  failures.push({
63
66
  id: 'AST_MAX_METHODS',
64
67
  title: `Class '${item.name}' has ${item.methods} methods (max: ${maxMethods})`,
@@ -69,10 +72,41 @@ export class PythonHandler extends ASTHandler {
69
72
  }
70
73
  }
71
74
  }
75
+ // Process security issues (CSRF, hardcoded secrets, SQL injection, etc.)
76
+ const securityIssues = result.security || [];
77
+ for (const issue of securityIssues) {
78
+ const issueIdMap = {
79
+ 'hardcoded_secret': 'SECURITY_HARDCODED_SECRET',
80
+ 'csrf_disabled': 'SECURITY_CSRF_DISABLED',
81
+ 'code_injection': 'SECURITY_CODE_INJECTION',
82
+ 'insecure_deserialization': 'SECURITY_INSECURE_DESERIALIZATION',
83
+ 'command_injection': 'SECURITY_COMMAND_INJECTION',
84
+ 'sql_injection': 'SECURITY_SQL_INJECTION'
85
+ };
86
+ const id = issueIdMap[issue.issue] || 'SECURITY_ISSUE';
87
+ failures.push({
88
+ id,
89
+ title: issue.message,
90
+ details: `Security issue in ${context.file} at line ${issue.lineno}: ${issue.name}`,
91
+ files: [context.file],
92
+ hint: this.getSecurityHint(issue.issue)
93
+ });
94
+ }
72
95
  }
73
96
  catch (e) {
74
97
  // If python3 is missing, we skip AST but other gates still run
75
98
  }
76
99
  return failures;
77
100
  }
101
+ getSecurityHint(issueType) {
102
+ const hints = {
103
+ 'hardcoded_secret': 'Use environment variables: os.environ.get("SECRET_KEY")',
104
+ 'csrf_disabled': 'Enable CSRF protection for all forms handling sensitive data',
105
+ 'code_injection': 'Avoid eval/exec. Use safer alternatives like ast.literal_eval() for data parsing',
106
+ 'insecure_deserialization': 'Use json.loads() instead of pickle for untrusted data',
107
+ 'command_injection': 'Use subprocess with shell=False and pass arguments as a list',
108
+ 'sql_injection': 'Use parameterized queries: cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))'
109
+ };
110
+ return hints[issueType] || 'Review and fix the security issue.';
111
+ }
78
112
  }
@@ -92,6 +92,53 @@ export class TypeScriptHandler extends ASTHandler {
92
92
  });
93
93
  }
94
94
  }
95
+ // === SECURITY CHECKS (Prototype Pollution) ===
96
+ // Check for direct __proto__ access: obj.__proto__
97
+ if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.name) && node.name.text === '__proto__') {
98
+ const line = sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1;
99
+ addFailure({
100
+ id: 'SECURITY_PROTOTYPE_POLLUTION',
101
+ title: `Direct __proto__ access at line ${line}`,
102
+ details: `Prototype pollution vulnerability in ${relativePath}:${line}`,
103
+ files: [relativePath],
104
+ hint: `Use Object.getPrototypeOf() or Object.setPrototypeOf() instead of __proto__.`
105
+ });
106
+ }
107
+ // Check for bracket notation __proto__ access: obj["__proto__"]
108
+ if (ts.isElementAccessExpression(node) && ts.isStringLiteral(node.argumentExpression)) {
109
+ const accessKey = node.argumentExpression.text;
110
+ if (accessKey === '__proto__' || accessKey === 'constructor' || accessKey === 'prototype') {
111
+ const line = sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1;
112
+ addFailure({
113
+ id: 'SECURITY_PROTOTYPE_POLLUTION',
114
+ title: `Unsafe bracket notation access to '${accessKey}' at line ${line}`,
115
+ details: `Potential prototype pollution via bracket notation in ${relativePath}:${line}`,
116
+ files: [relativePath],
117
+ hint: `Block access to '${accessKey}' property when handling user input. Use allowlist for object keys.`
118
+ });
119
+ }
120
+ }
121
+ // Check for Object.assign with user-controllable input (common prototype pollution pattern)
122
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
123
+ const propAccess = node.expression;
124
+ if (ts.isIdentifier(propAccess.expression) && propAccess.expression.text === 'Object' &&
125
+ ts.isIdentifier(propAccess.name) && propAccess.name.text === 'assign') {
126
+ // This is Object.assign() - warn if first arg is empty object (merge pattern)
127
+ if (node.arguments.length >= 2) {
128
+ const firstArg = node.arguments[0];
129
+ if (ts.isObjectLiteralExpression(firstArg) && firstArg.properties.length === 0) {
130
+ const line = sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1;
131
+ addFailure({
132
+ id: 'SECURITY_PROTOTYPE_POLLUTION_MERGE',
133
+ title: `Object.assign() merge pattern at line ${line}`,
134
+ details: `Object.assign({}, ...) can propagate prototype pollution in ${relativePath}:${line}`,
135
+ files: [relativePath],
136
+ hint: `Validate and sanitize source objects before merging. Block __proto__ and constructor keys.`
137
+ });
138
+ }
139
+ }
140
+ }
141
+ }
95
142
  // === COMPLEXITY CHECKS ===
96
143
  if (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node) || ts.isArrowFunction(node)) {
97
144
  const name = this.getNodeName(node);
@@ -145,6 +145,128 @@ const BUILT_IN_DEPRECATIONS = [
145
145
  replacement: "useRouter from 'next/navigation' in App Router",
146
146
  severity: 'info',
147
147
  reason: 'Use next/navigation for App Router projects'
148
+ },
149
+ // ============================================================
150
+ // SECURITY PATTERNS - Cross-language security vulnerabilities
151
+ // ============================================================
152
+ // Python CSRF disabled
153
+ {
154
+ pattern: 'csrf\\s*=\\s*False',
155
+ deprecatedIn: 'security',
156
+ replacement: "Never disable CSRF protection. Remove 'csrf = False' and use proper CSRF tokens.",
157
+ severity: 'error',
158
+ reason: 'CSRF protection is critical for security. Disabling it exposes users to cross-site request forgery attacks.'
159
+ },
160
+ {
161
+ pattern: 'WTF_CSRF_ENABLED\\s*=\\s*False',
162
+ deprecatedIn: 'security',
163
+ replacement: "Never disable CSRF. Remove 'WTF_CSRF_ENABLED = False' from config.",
164
+ severity: 'error',
165
+ reason: 'Flask-WTF CSRF protection should never be disabled in production.'
166
+ },
167
+ {
168
+ pattern: "@csrf_exempt",
169
+ deprecatedIn: 'security',
170
+ replacement: "Remove @csrf_exempt decorator. Use proper CSRF token handling instead.",
171
+ severity: 'error',
172
+ reason: 'csrf_exempt bypasses CSRF protection, creating security vulnerabilities.'
173
+ },
174
+ // Python hardcoded secrets
175
+ {
176
+ pattern: "SECRET_KEY\\s*=\\s*['\"][^'\"]{1,50}['\"]",
177
+ deprecatedIn: 'security',
178
+ replacement: "Use os.environ.get('SECRET_KEY') or secrets.token_hex(32)",
179
+ severity: 'error',
180
+ reason: 'Hardcoded secrets are exposed in version control and logs. Use environment variables.'
181
+ },
182
+ {
183
+ pattern: "API_KEY\\s*=\\s*['\"][^'\"]+['\"]",
184
+ deprecatedIn: 'security',
185
+ replacement: "Use os.environ.get('API_KEY') for API credentials",
186
+ severity: 'error',
187
+ reason: 'Hardcoded API keys are a security risk. Use environment variables.'
188
+ },
189
+ {
190
+ pattern: "PASSWORD\\s*=\\s*['\"][^'\"]+['\"]",
191
+ deprecatedIn: 'security',
192
+ replacement: "Never hardcode passwords. Use environment variables or secret managers.",
193
+ severity: 'error',
194
+ reason: 'Hardcoded passwords are a critical security vulnerability.'
195
+ },
196
+ // JavaScript/TypeScript prototype pollution
197
+ {
198
+ pattern: '\\.__proto__',
199
+ deprecatedIn: 'security',
200
+ replacement: "Use Object.getPrototypeOf() or Object.setPrototypeOf() instead of __proto__",
201
+ severity: 'error',
202
+ reason: 'Direct __proto__ access enables prototype pollution attacks.'
203
+ },
204
+ {
205
+ pattern: '\\[\\s*[\'"]__proto__[\'"]\\s*\\]',
206
+ deprecatedIn: 'security',
207
+ replacement: "Never allow user input to access __proto__. Validate and sanitize object keys.",
208
+ severity: 'error',
209
+ reason: 'Bracket notation access to __proto__ is a prototype pollution vector.'
210
+ },
211
+ {
212
+ pattern: '\\[\\s*[\'"]constructor[\'"]\\s*\\]\\s*\\[',
213
+ deprecatedIn: 'security',
214
+ replacement: "Block access to constructor property from user input.",
215
+ severity: 'error',
216
+ reason: 'constructor[constructor] pattern enables prototype pollution.'
217
+ },
218
+ // SQL Injection patterns
219
+ {
220
+ pattern: 'cursor\\.execute\\s*\\(\\s*f[\'"]',
221
+ deprecatedIn: 'security',
222
+ replacement: "Use parameterized queries: cursor.execute('SELECT * FROM users WHERE id = %s', (user_id,))",
223
+ severity: 'error',
224
+ reason: 'F-string SQL queries are vulnerable to SQL injection attacks.'
225
+ },
226
+ {
227
+ pattern: '\\.execute\\s*\\([^)]*\\+[^)]*\\)',
228
+ deprecatedIn: 'security',
229
+ replacement: "Use parameterized queries instead of string concatenation.",
230
+ severity: 'error',
231
+ reason: 'String concatenation in SQL queries enables SQL injection.'
232
+ },
233
+ // XSS patterns
234
+ {
235
+ pattern: 'dangerouslySetInnerHTML',
236
+ deprecatedIn: 'security',
237
+ replacement: "Sanitize HTML with DOMPurify before using dangerouslySetInnerHTML, or use safe alternatives.",
238
+ severity: 'warning',
239
+ reason: 'dangerouslySetInnerHTML can lead to XSS vulnerabilities if content is not sanitized.'
240
+ },
241
+ {
242
+ pattern: '\\.innerHTML\\s*=',
243
+ deprecatedIn: 'security',
244
+ replacement: "Use textContent for text, or sanitize HTML before setting innerHTML.",
245
+ severity: 'warning',
246
+ reason: 'Direct innerHTML assignment can lead to XSS attacks.'
247
+ },
248
+ // Insecure session/cookie settings
249
+ {
250
+ pattern: 'SESSION_COOKIE_SECURE\\s*=\\s*False',
251
+ deprecatedIn: 'security',
252
+ replacement: "Set SESSION_COOKIE_SECURE = True in production",
253
+ severity: 'error',
254
+ reason: 'Insecure cookies can be intercepted over HTTP connections.'
255
+ },
256
+ {
257
+ pattern: 'SESSION_COOKIE_HTTPONLY\\s*=\\s*False',
258
+ deprecatedIn: 'security',
259
+ replacement: "Set SESSION_COOKIE_HTTPONLY = True to prevent XSS cookie theft",
260
+ severity: 'error',
261
+ reason: 'Non-HTTPOnly cookies are accessible via JavaScript, enabling XSS attacks.'
262
+ },
263
+ // Debug mode in production
264
+ {
265
+ pattern: 'DEBUG\\s*=\\s*True',
266
+ deprecatedIn: 'security',
267
+ replacement: "Use DEBUG = os.environ.get('DEBUG', 'False') == 'True'",
268
+ severity: 'warning',
269
+ reason: 'Debug mode in production exposes sensitive information and stack traces.'
148
270
  }
149
271
  ];
150
272
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rigour-labs/core",
3
- "version": "2.16.0",
3
+ "version": "2.17.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -6,6 +6,29 @@ import { fileURLToPath } from 'url';
6
6
 
7
7
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
8
 
9
+ interface SecurityIssue {
10
+ type: string;
11
+ issue: string;
12
+ name: string;
13
+ lineno: number;
14
+ message: string;
15
+ }
16
+
17
+ interface MetricItem {
18
+ type: string;
19
+ name: string;
20
+ complexity?: number;
21
+ parameters?: number;
22
+ methods?: number;
23
+ lineno: number;
24
+ }
25
+
26
+ interface PythonAnalysisResult {
27
+ metrics?: MetricItem[];
28
+ security?: SecurityIssue[];
29
+ error?: string;
30
+ }
31
+
9
32
  export class PythonHandler extends ASTHandler {
10
33
  supports(file: string): boolean {
11
34
  return /\.py$/.test(file);
@@ -34,17 +57,20 @@ export class PythonHandler extends ASTHandler {
34
57
  cwd: context.cwd
35
58
  });
36
59
 
37
- const metrics = JSON.parse(stdout);
38
- if (metrics.error) return [];
60
+ const result: PythonAnalysisResult = JSON.parse(stdout);
61
+ if (result.error) return [];
39
62
 
40
63
  const astConfig = this.config.ast || {};
64
+ const safetyConfig = this.config.safety || {};
41
65
  const maxComplexity = astConfig.complexity || 10;
42
66
  const maxParams = astConfig.max_params || 5;
43
67
  const maxMethods = astConfig.max_methods || 10;
44
68
 
69
+ // Process metrics (complexity, params, methods)
70
+ const metrics = result.metrics || [];
45
71
  for (const item of metrics) {
46
72
  if (item.type === 'function') {
47
- if (item.parameters > maxParams) {
73
+ if (item.parameters && item.parameters > maxParams) {
48
74
  failures.push({
49
75
  id: 'AST_MAX_PARAMS',
50
76
  title: `Function '${item.name}' has ${item.parameters} parameters (max: ${maxParams})`,
@@ -53,7 +79,7 @@ export class PythonHandler extends ASTHandler {
53
79
  hint: `Reduce number of parameters or use an options object.`
54
80
  });
55
81
  }
56
- if (item.complexity > maxComplexity) {
82
+ if (item.complexity && item.complexity > maxComplexity) {
57
83
  failures.push({
58
84
  id: 'AST_COMPLEXITY',
59
85
  title: `Function '${item.name}' has complexity of ${item.complexity} (max: ${maxComplexity})`,
@@ -63,7 +89,7 @@ export class PythonHandler extends ASTHandler {
63
89
  });
64
90
  }
65
91
  } else if (item.type === 'class') {
66
- if (item.methods > maxMethods) {
92
+ if (item.methods && item.methods > maxMethods) {
67
93
  failures.push({
68
94
  id: 'AST_MAX_METHODS',
69
95
  title: `Class '${item.name}' has ${item.methods} methods (max: ${maxMethods})`,
@@ -75,10 +101,45 @@ export class PythonHandler extends ASTHandler {
75
101
  }
76
102
  }
77
103
 
104
+ // Process security issues (CSRF, hardcoded secrets, SQL injection, etc.)
105
+ const securityIssues = result.security || [];
106
+ for (const issue of securityIssues) {
107
+ const issueIdMap: Record<string, string> = {
108
+ 'hardcoded_secret': 'SECURITY_HARDCODED_SECRET',
109
+ 'csrf_disabled': 'SECURITY_CSRF_DISABLED',
110
+ 'code_injection': 'SECURITY_CODE_INJECTION',
111
+ 'insecure_deserialization': 'SECURITY_INSECURE_DESERIALIZATION',
112
+ 'command_injection': 'SECURITY_COMMAND_INJECTION',
113
+ 'sql_injection': 'SECURITY_SQL_INJECTION'
114
+ };
115
+
116
+ const id = issueIdMap[issue.issue] || 'SECURITY_ISSUE';
117
+
118
+ failures.push({
119
+ id,
120
+ title: issue.message,
121
+ details: `Security issue in ${context.file} at line ${issue.lineno}: ${issue.name}`,
122
+ files: [context.file],
123
+ hint: this.getSecurityHint(issue.issue)
124
+ });
125
+ }
126
+
78
127
  } catch (e: any) {
79
128
  // If python3 is missing, we skip AST but other gates still run
80
129
  }
81
130
 
82
131
  return failures;
83
132
  }
133
+
134
+ private getSecurityHint(issueType: string): string {
135
+ const hints: Record<string, string> = {
136
+ 'hardcoded_secret': 'Use environment variables: os.environ.get("SECRET_KEY")',
137
+ 'csrf_disabled': 'Enable CSRF protection for all forms handling sensitive data',
138
+ 'code_injection': 'Avoid eval/exec. Use safer alternatives like ast.literal_eval() for data parsing',
139
+ 'insecure_deserialization': 'Use json.loads() instead of pickle for untrusted data',
140
+ 'command_injection': 'Use subprocess with shell=False and pass arguments as a list',
141
+ 'sql_injection': 'Use parameterized queries: cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))'
142
+ };
143
+ return hints[issueType] || 'Review and fix the security issue.';
144
+ }
84
145
  }
@@ -1,6 +1,7 @@
1
1
  import ast
2
2
  import sys
3
3
  import json
4
+ import re
4
5
 
5
6
  class MetricsVisitor(ast.NodeVisitor):
6
7
  def __init__(self):
@@ -33,7 +34,7 @@ class MetricsVisitor(ast.NodeVisitor):
33
34
  complexity += len(n.values) - 1
34
35
  elif isinstance(n, ast.IfExp):
35
36
  complexity += 1
36
-
37
+
37
38
  params = len(node.args.args) + len(node.args.kwonlyargs)
38
39
  if node.args.vararg: params += 1
39
40
  if node.args.kwarg: params += 1
@@ -46,12 +47,132 @@ class MetricsVisitor(ast.NodeVisitor):
46
47
  "lineno": node.lineno
47
48
  })
48
49
 
50
+
51
+ class SecurityVisitor(ast.NodeVisitor):
52
+ """Detects security issues in Python code via AST analysis."""
53
+
54
+ def __init__(self, content):
55
+ self.issues = []
56
+ self.content = content
57
+ self.lines = content.split('\n')
58
+
59
+ def visit_Assign(self, node):
60
+ """Check for hardcoded secrets and CSRF disabled."""
61
+ for target in node.targets:
62
+ if isinstance(target, ast.Name):
63
+ name = target.id
64
+ # Check for hardcoded SECRET_KEY
65
+ if name == 'SECRET_KEY':
66
+ if isinstance(node.value, ast.Constant) and isinstance(node.value.value, str):
67
+ self.issues.append({
68
+ "type": "security",
69
+ "issue": "hardcoded_secret",
70
+ "name": "SECRET_KEY",
71
+ "lineno": node.lineno,
72
+ "message": "Hardcoded SECRET_KEY detected. Use environment variables instead."
73
+ })
74
+ # Check for hardcoded passwords/tokens
75
+ if name.upper() in ('PASSWORD', 'API_KEY', 'TOKEN', 'AUTH_TOKEN', 'PRIVATE_KEY', 'AWS_SECRET_KEY'):
76
+ if isinstance(node.value, ast.Constant) and isinstance(node.value.value, str):
77
+ self.issues.append({
78
+ "type": "security",
79
+ "issue": "hardcoded_secret",
80
+ "name": name,
81
+ "lineno": node.lineno,
82
+ "message": f"Hardcoded {name} detected. Use environment variables instead."
83
+ })
84
+ # Check for csrf = False
85
+ if name.lower() in ('csrf', 'csrf_enabled', 'wtf_csrf_enabled'):
86
+ if isinstance(node.value, ast.Constant) and node.value.value == False:
87
+ self.issues.append({
88
+ "type": "security",
89
+ "issue": "csrf_disabled",
90
+ "name": name,
91
+ "lineno": node.lineno,
92
+ "message": "CSRF protection is disabled. This is a security vulnerability."
93
+ })
94
+ self.generic_visit(node)
95
+
96
+ def visit_Call(self, node):
97
+ """Check for dangerous function calls."""
98
+ func_name = None
99
+ if isinstance(node.func, ast.Name):
100
+ func_name = node.func.id
101
+ elif isinstance(node.func, ast.Attribute):
102
+ func_name = node.func.attr
103
+
104
+ # Check for eval/exec usage
105
+ if func_name in ('eval', 'exec'):
106
+ self.issues.append({
107
+ "type": "security",
108
+ "issue": "code_injection",
109
+ "name": func_name,
110
+ "lineno": node.lineno,
111
+ "message": f"Use of {func_name}() detected. This can lead to code injection vulnerabilities."
112
+ })
113
+
114
+ # Check for pickle usage (insecure deserialization)
115
+ if func_name in ('loads', 'load') and isinstance(node.func, ast.Attribute):
116
+ if isinstance(node.func.value, ast.Name) and node.func.value.id == 'pickle':
117
+ self.issues.append({
118
+ "type": "security",
119
+ "issue": "insecure_deserialization",
120
+ "name": "pickle",
121
+ "lineno": node.lineno,
122
+ "message": "Pickle deserialization is unsafe. Use json instead for untrusted data."
123
+ })
124
+
125
+ # Check for shell=True in subprocess
126
+ if func_name in ('run', 'call', 'Popen', 'check_output', 'check_call'):
127
+ for keyword in node.keywords:
128
+ if keyword.arg == 'shell' and isinstance(keyword.value, ast.Constant) and keyword.value.value == True:
129
+ self.issues.append({
130
+ "type": "security",
131
+ "issue": "command_injection",
132
+ "name": func_name,
133
+ "lineno": node.lineno,
134
+ "message": "shell=True in subprocess can lead to command injection."
135
+ })
136
+
137
+ self.generic_visit(node)
138
+
139
+ def check_sql_injection(self):
140
+ """Check for SQL injection patterns using regex on source."""
141
+ sql_patterns = [
142
+ (r'execute\s*\(\s*["\'].*%s', 'SQL string formatting with %s'),
143
+ (r'execute\s*\(\s*f["\']', 'SQL f-string'),
144
+ (r'execute\s*\(\s*["\'].*\+', 'SQL string concatenation'),
145
+ (r'cursor\.execute\s*\(\s*["\'].*\.format\s*\(', 'SQL .format()'),
146
+ ]
147
+ for pattern, desc in sql_patterns:
148
+ for i, line in enumerate(self.lines, 1):
149
+ if re.search(pattern, line, re.IGNORECASE):
150
+ self.issues.append({
151
+ "type": "security",
152
+ "issue": "sql_injection",
153
+ "name": desc,
154
+ "lineno": i,
155
+ "message": f"Potential SQL injection: {desc}. Use parameterized queries."
156
+ })
157
+
158
+
49
159
  def analyze_code(content):
50
160
  try:
51
161
  tree = ast.parse(content)
162
+
163
+ # Collect metrics
52
164
  visitor = MetricsVisitor()
53
165
  visitor.visit(tree)
54
- return visitor.metrics
166
+
167
+ # Collect security issues
168
+ security_visitor = SecurityVisitor(content)
169
+ security_visitor.visit(tree)
170
+ security_visitor.check_sql_injection()
171
+
172
+ return {
173
+ "metrics": visitor.metrics,
174
+ "security": security_visitor.issues
175
+ }
55
176
  except Exception as e:
56
177
  return {"error": str(e)}
57
178
 
@@ -103,6 +103,57 @@ export class TypeScriptHandler extends ASTHandler {
103
103
  }
104
104
  }
105
105
 
106
+ // === SECURITY CHECKS (Prototype Pollution) ===
107
+
108
+ // Check for direct __proto__ access: obj.__proto__
109
+ if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.name) && node.name.text === '__proto__') {
110
+ const line = sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1;
111
+ addFailure({
112
+ id: 'SECURITY_PROTOTYPE_POLLUTION',
113
+ title: `Direct __proto__ access at line ${line}`,
114
+ details: `Prototype pollution vulnerability in ${relativePath}:${line}`,
115
+ files: [relativePath],
116
+ hint: `Use Object.getPrototypeOf() or Object.setPrototypeOf() instead of __proto__.`
117
+ });
118
+ }
119
+
120
+ // Check for bracket notation __proto__ access: obj["__proto__"]
121
+ if (ts.isElementAccessExpression(node) && ts.isStringLiteral(node.argumentExpression)) {
122
+ const accessKey = node.argumentExpression.text;
123
+ if (accessKey === '__proto__' || accessKey === 'constructor' || accessKey === 'prototype') {
124
+ const line = sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1;
125
+ addFailure({
126
+ id: 'SECURITY_PROTOTYPE_POLLUTION',
127
+ title: `Unsafe bracket notation access to '${accessKey}' at line ${line}`,
128
+ details: `Potential prototype pollution via bracket notation in ${relativePath}:${line}`,
129
+ files: [relativePath],
130
+ hint: `Block access to '${accessKey}' property when handling user input. Use allowlist for object keys.`
131
+ });
132
+ }
133
+ }
134
+
135
+ // Check for Object.assign with user-controllable input (common prototype pollution pattern)
136
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
137
+ const propAccess = node.expression;
138
+ if (ts.isIdentifier(propAccess.expression) && propAccess.expression.text === 'Object' &&
139
+ ts.isIdentifier(propAccess.name) && propAccess.name.text === 'assign') {
140
+ // This is Object.assign() - warn if first arg is empty object (merge pattern)
141
+ if (node.arguments.length >= 2) {
142
+ const firstArg = node.arguments[0];
143
+ if (ts.isObjectLiteralExpression(firstArg) && firstArg.properties.length === 0) {
144
+ const line = sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1;
145
+ addFailure({
146
+ id: 'SECURITY_PROTOTYPE_POLLUTION_MERGE',
147
+ title: `Object.assign() merge pattern at line ${line}`,
148
+ details: `Object.assign({}, ...) can propagate prototype pollution in ${relativePath}:${line}`,
149
+ files: [relativePath],
150
+ hint: `Validate and sanitize source objects before merging. Block __proto__ and constructor keys.`
151
+ });
152
+ }
153
+ }
154
+ }
155
+ }
156
+
106
157
  // === COMPLEXITY CHECKS ===
107
158
 
108
159
  if (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node) || ts.isArrowFunction(node)) {
@@ -159,6 +159,136 @@ const BUILT_IN_DEPRECATIONS: DeprecationEntry[] = [
159
159
  replacement: "useRouter from 'next/navigation' in App Router",
160
160
  severity: 'info',
161
161
  reason: 'Use next/navigation for App Router projects'
162
+ },
163
+
164
+ // ============================================================
165
+ // SECURITY PATTERNS - Cross-language security vulnerabilities
166
+ // ============================================================
167
+
168
+ // Python CSRF disabled
169
+ {
170
+ pattern: 'csrf\\s*=\\s*False',
171
+ deprecatedIn: 'security',
172
+ replacement: "Never disable CSRF protection. Remove 'csrf = False' and use proper CSRF tokens.",
173
+ severity: 'error',
174
+ reason: 'CSRF protection is critical for security. Disabling it exposes users to cross-site request forgery attacks.'
175
+ },
176
+ {
177
+ pattern: 'WTF_CSRF_ENABLED\\s*=\\s*False',
178
+ deprecatedIn: 'security',
179
+ replacement: "Never disable CSRF. Remove 'WTF_CSRF_ENABLED = False' from config.",
180
+ severity: 'error',
181
+ reason: 'Flask-WTF CSRF protection should never be disabled in production.'
182
+ },
183
+ {
184
+ pattern: "@csrf_exempt",
185
+ deprecatedIn: 'security',
186
+ replacement: "Remove @csrf_exempt decorator. Use proper CSRF token handling instead.",
187
+ severity: 'error',
188
+ reason: 'csrf_exempt bypasses CSRF protection, creating security vulnerabilities.'
189
+ },
190
+
191
+ // Python hardcoded secrets
192
+ {
193
+ pattern: "SECRET_KEY\\s*=\\s*['\"][^'\"]{1,50}['\"]",
194
+ deprecatedIn: 'security',
195
+ replacement: "Use os.environ.get('SECRET_KEY') or secrets.token_hex(32)",
196
+ severity: 'error',
197
+ reason: 'Hardcoded secrets are exposed in version control and logs. Use environment variables.'
198
+ },
199
+ {
200
+ pattern: "API_KEY\\s*=\\s*['\"][^'\"]+['\"]",
201
+ deprecatedIn: 'security',
202
+ replacement: "Use os.environ.get('API_KEY') for API credentials",
203
+ severity: 'error',
204
+ reason: 'Hardcoded API keys are a security risk. Use environment variables.'
205
+ },
206
+ {
207
+ pattern: "PASSWORD\\s*=\\s*['\"][^'\"]+['\"]",
208
+ deprecatedIn: 'security',
209
+ replacement: "Never hardcode passwords. Use environment variables or secret managers.",
210
+ severity: 'error',
211
+ reason: 'Hardcoded passwords are a critical security vulnerability.'
212
+ },
213
+
214
+ // JavaScript/TypeScript prototype pollution
215
+ {
216
+ pattern: '\\.__proto__',
217
+ deprecatedIn: 'security',
218
+ replacement: "Use Object.getPrototypeOf() or Object.setPrototypeOf() instead of __proto__",
219
+ severity: 'error',
220
+ reason: 'Direct __proto__ access enables prototype pollution attacks.'
221
+ },
222
+ {
223
+ pattern: '\\[\\s*[\'"]__proto__[\'"]\\s*\\]',
224
+ deprecatedIn: 'security',
225
+ replacement: "Never allow user input to access __proto__. Validate and sanitize object keys.",
226
+ severity: 'error',
227
+ reason: 'Bracket notation access to __proto__ is a prototype pollution vector.'
228
+ },
229
+ {
230
+ pattern: '\\[\\s*[\'"]constructor[\'"]\\s*\\]\\s*\\[',
231
+ deprecatedIn: 'security',
232
+ replacement: "Block access to constructor property from user input.",
233
+ severity: 'error',
234
+ reason: 'constructor[constructor] pattern enables prototype pollution.'
235
+ },
236
+
237
+ // SQL Injection patterns
238
+ {
239
+ pattern: 'cursor\\.execute\\s*\\(\\s*f[\'"]',
240
+ deprecatedIn: 'security',
241
+ replacement: "Use parameterized queries: cursor.execute('SELECT * FROM users WHERE id = %s', (user_id,))",
242
+ severity: 'error',
243
+ reason: 'F-string SQL queries are vulnerable to SQL injection attacks.'
244
+ },
245
+ {
246
+ pattern: '\\.execute\\s*\\([^)]*\\+[^)]*\\)',
247
+ deprecatedIn: 'security',
248
+ replacement: "Use parameterized queries instead of string concatenation.",
249
+ severity: 'error',
250
+ reason: 'String concatenation in SQL queries enables SQL injection.'
251
+ },
252
+
253
+ // XSS patterns
254
+ {
255
+ pattern: 'dangerouslySetInnerHTML',
256
+ deprecatedIn: 'security',
257
+ replacement: "Sanitize HTML with DOMPurify before using dangerouslySetInnerHTML, or use safe alternatives.",
258
+ severity: 'warning',
259
+ reason: 'dangerouslySetInnerHTML can lead to XSS vulnerabilities if content is not sanitized.'
260
+ },
261
+ {
262
+ pattern: '\\.innerHTML\\s*=',
263
+ deprecatedIn: 'security',
264
+ replacement: "Use textContent for text, or sanitize HTML before setting innerHTML.",
265
+ severity: 'warning',
266
+ reason: 'Direct innerHTML assignment can lead to XSS attacks.'
267
+ },
268
+
269
+ // Insecure session/cookie settings
270
+ {
271
+ pattern: 'SESSION_COOKIE_SECURE\\s*=\\s*False',
272
+ deprecatedIn: 'security',
273
+ replacement: "Set SESSION_COOKIE_SECURE = True in production",
274
+ severity: 'error',
275
+ reason: 'Insecure cookies can be intercepted over HTTP connections.'
276
+ },
277
+ {
278
+ pattern: 'SESSION_COOKIE_HTTPONLY\\s*=\\s*False',
279
+ deprecatedIn: 'security',
280
+ replacement: "Set SESSION_COOKIE_HTTPONLY = True to prevent XSS cookie theft",
281
+ severity: 'error',
282
+ reason: 'Non-HTTPOnly cookies are accessible via JavaScript, enabling XSS attacks.'
283
+ },
284
+
285
+ // Debug mode in production
286
+ {
287
+ pattern: 'DEBUG\\s*=\\s*True',
288
+ deprecatedIn: 'security',
289
+ replacement: "Use DEBUG = os.environ.get('DEBUG', 'False') == 'True'",
290
+ severity: 'warning',
291
+ reason: 'Debug mode in production exposes sensitive information and stack traces.'
162
292
  }
163
293
  ];
164
294