@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.
- package/dist/gates/ast-handlers/python.d.ts +1 -0
- package/dist/gates/ast-handlers/python.js +39 -5
- package/dist/gates/ast-handlers/typescript.js +47 -0
- package/dist/pattern-index/staleness.js +122 -0
- package/package.json +1 -1
- package/src/gates/ast-handlers/python.ts +66 -5
- package/src/gates/ast-handlers/python_parser.py +123 -2
- package/src/gates/ast-handlers/typescript.ts +51 -0
- package/src/pattern-index/staleness.ts +130 -0
|
@@ -29,16 +29,19 @@ export class PythonHandler extends ASTHandler {
|
|
|
29
29
|
input: context.content,
|
|
30
30
|
cwd: context.cwd
|
|
31
31
|
});
|
|
32
|
-
const
|
|
33
|
-
if (
|
|
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
|
@@ -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
|
|
38
|
-
if (
|
|
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
|
-
|
|
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
|
|