@rigour-labs/core 2.15.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 +125 -3
- package/dist/pattern-index/staleness.js +122 -0
- package/dist/templates/index.js +13 -0
- package/dist/types/index.d.ts +44 -0
- package/dist/types/index.js +14 -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 +136 -3
- package/src/pattern-index/staleness.ts +130 -0
- package/src/templates/index.ts +13 -0
- package/src/types/index.ts +14 -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
|
}
|
|
@@ -14,14 +14,136 @@ export class TypeScriptHandler extends ASTHandler {
|
|
|
14
14
|
}
|
|
15
15
|
analyzeSourceFile(sourceFile, relativePath, failures) {
|
|
16
16
|
const astConfig = this.config.ast || {};
|
|
17
|
+
const stalenessConfig = this.config.staleness || {};
|
|
18
|
+
const stalenessRules = stalenessConfig.rules || {};
|
|
17
19
|
const maxComplexity = astConfig.complexity || 10;
|
|
18
20
|
const maxMethods = astConfig.max_methods || 10;
|
|
19
21
|
const maxParams = astConfig.max_params || 5;
|
|
22
|
+
// Limit failures per file to avoid output bloat on large files
|
|
23
|
+
const MAX_FAILURES_PER_FILE = 50;
|
|
24
|
+
const fileFailureCount = {};
|
|
25
|
+
const addFailure = (failure) => {
|
|
26
|
+
const ruleId = failure.id;
|
|
27
|
+
fileFailureCount[ruleId] = (fileFailureCount[ruleId] || 0) + 1;
|
|
28
|
+
if (fileFailureCount[ruleId] <= MAX_FAILURES_PER_FILE) {
|
|
29
|
+
failures.push(failure);
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
// Add summary failure once when limit is reached
|
|
33
|
+
if (fileFailureCount[ruleId] === MAX_FAILURES_PER_FILE + 1) {
|
|
34
|
+
failures.push({
|
|
35
|
+
id: `${ruleId}_LIMIT_EXCEEDED`,
|
|
36
|
+
title: `More than ${MAX_FAILURES_PER_FILE} ${ruleId} violations in ${relativePath}`,
|
|
37
|
+
details: `Truncated output: showing first ${MAX_FAILURES_PER_FILE} violations. Consider fixing the root cause.`,
|
|
38
|
+
files: [relativePath],
|
|
39
|
+
hint: `This file has many violations. Fix them systematically or exclude the file if it's legacy code.`
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
};
|
|
44
|
+
// Helper to check if a staleness rule is enabled
|
|
45
|
+
const isRuleEnabled = (rule) => {
|
|
46
|
+
if (!stalenessConfig.enabled)
|
|
47
|
+
return false;
|
|
48
|
+
return stalenessRules[rule] !== false; // Enabled by default if staleness is on
|
|
49
|
+
};
|
|
20
50
|
const visit = (node) => {
|
|
51
|
+
// === STALENESS CHECKS (Rule-based) ===
|
|
52
|
+
// no-var: Forbid legacy 'var' keyword
|
|
53
|
+
if (isRuleEnabled('no-var') && ts.isVariableStatement(node)) {
|
|
54
|
+
const declarationList = node.declarationList;
|
|
55
|
+
// NodeFlags: Let = 1, Const = 2, None = 0 (var)
|
|
56
|
+
if ((declarationList.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) === 0) {
|
|
57
|
+
const line = sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1;
|
|
58
|
+
addFailure({
|
|
59
|
+
id: 'STALENESS_NO_VAR',
|
|
60
|
+
title: `Stale 'var' keyword at line ${line}`,
|
|
61
|
+
details: `Use 'const' or 'let' instead of 'var' in ${relativePath}:${line}`,
|
|
62
|
+
files: [relativePath],
|
|
63
|
+
hint: `Replace 'var' with 'const' (preferred) or 'let' for modern JavaScript.`
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// no-commonjs: Forbid require() in favor of import
|
|
68
|
+
if (isRuleEnabled('no-commonjs') && ts.isCallExpression(node)) {
|
|
69
|
+
if (ts.isIdentifier(node.expression) && node.expression.text === 'require') {
|
|
70
|
+
const line = sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1;
|
|
71
|
+
addFailure({
|
|
72
|
+
id: 'STALENESS_NO_COMMONJS',
|
|
73
|
+
title: `CommonJS require() at line ${line}`,
|
|
74
|
+
details: `Use ES6 'import' instead of 'require()' in ${relativePath}:${line}`,
|
|
75
|
+
files: [relativePath],
|
|
76
|
+
hint: `Replace require('module') with import module from 'module'.`
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// no-arguments: Forbid 'arguments' object (use rest params)
|
|
81
|
+
if (isRuleEnabled('no-arguments') && ts.isIdentifier(node) && node.text === 'arguments') {
|
|
82
|
+
// Check if it's actually the arguments keyword and not a variable named arguments
|
|
83
|
+
const parent = node.parent;
|
|
84
|
+
if (!ts.isVariableDeclaration(parent) && !ts.isPropertyAccessExpression(parent)) {
|
|
85
|
+
const line = sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1;
|
|
86
|
+
addFailure({
|
|
87
|
+
id: 'STALENESS_NO_ARGUMENTS',
|
|
88
|
+
title: `Legacy 'arguments' object at line ${line}`,
|
|
89
|
+
details: `Use rest parameters (...args) instead of 'arguments' in ${relativePath}:${line}`,
|
|
90
|
+
files: [relativePath],
|
|
91
|
+
hint: `Replace 'arguments' with rest parameters: function(...args) { }`
|
|
92
|
+
});
|
|
93
|
+
}
|
|
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
|
+
}
|
|
142
|
+
// === COMPLEXITY CHECKS ===
|
|
21
143
|
if (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node) || ts.isArrowFunction(node)) {
|
|
22
144
|
const name = this.getNodeName(node);
|
|
23
145
|
if (node.parameters.length > maxParams) {
|
|
24
|
-
|
|
146
|
+
addFailure({
|
|
25
147
|
id: 'AST_MAX_PARAMS',
|
|
26
148
|
title: `Function '${name}' has ${node.parameters.length} parameters (max: ${maxParams})`,
|
|
27
149
|
details: `High parameter count detected in ${relativePath}`,
|
|
@@ -46,7 +168,7 @@ export class TypeScriptHandler extends ASTHandler {
|
|
|
46
168
|
};
|
|
47
169
|
ts.forEachChild(node, countComplexity);
|
|
48
170
|
if (complexity > maxComplexity) {
|
|
49
|
-
|
|
171
|
+
addFailure({
|
|
50
172
|
id: 'AST_COMPLEXITY',
|
|
51
173
|
title: `Function '${name}' has cyclomatic complexity of ${complexity} (max: ${maxComplexity})`,
|
|
52
174
|
details: `High complexity detected in ${relativePath}`,
|
|
@@ -59,7 +181,7 @@ export class TypeScriptHandler extends ASTHandler {
|
|
|
59
181
|
const name = node.name?.text || 'Anonymous Class';
|
|
60
182
|
const methods = node.members.filter(ts.isMethodDeclaration);
|
|
61
183
|
if (methods.length > maxMethods) {
|
|
62
|
-
|
|
184
|
+
addFailure({
|
|
63
185
|
id: 'AST_MAX_METHODS',
|
|
64
186
|
title: `Class '${name}' has ${methods.length} methods (max: ${maxMethods})`,
|
|
65
187
|
details: `God Object pattern detected in ${relativePath}`,
|
|
@@ -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/dist/templates/index.js
CHANGED
|
@@ -192,6 +192,19 @@ export const UNIVERSAL_CONFIG = {
|
|
|
192
192
|
auto_classify: true,
|
|
193
193
|
doc_sources: {},
|
|
194
194
|
},
|
|
195
|
+
staleness: {
|
|
196
|
+
enabled: false,
|
|
197
|
+
rules: {
|
|
198
|
+
'no-var': true,
|
|
199
|
+
'no-commonjs': false,
|
|
200
|
+
'no-arguments': false,
|
|
201
|
+
'prefer-arrow': false,
|
|
202
|
+
'prefer-template': false,
|
|
203
|
+
'prefer-spread': false,
|
|
204
|
+
'prefer-rest': false,
|
|
205
|
+
'prefer-const': false,
|
|
206
|
+
},
|
|
207
|
+
},
|
|
195
208
|
},
|
|
196
209
|
output: {
|
|
197
210
|
report_path: 'rigour-report.json',
|
package/dist/types/index.d.ts
CHANGED
|
@@ -30,6 +30,16 @@ export declare const GatesSchema: z.ZodObject<{
|
|
|
30
30
|
max_class_dependencies?: number | undefined;
|
|
31
31
|
max_function_lines?: number | undefined;
|
|
32
32
|
}>>>;
|
|
33
|
+
staleness: z.ZodDefault<z.ZodOptional<z.ZodObject<{
|
|
34
|
+
enabled: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
35
|
+
rules: z.ZodDefault<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodBoolean>>>;
|
|
36
|
+
}, "strip", z.ZodTypeAny, {
|
|
37
|
+
enabled: boolean;
|
|
38
|
+
rules: Record<string, boolean>;
|
|
39
|
+
}, {
|
|
40
|
+
enabled?: boolean | undefined;
|
|
41
|
+
rules?: Record<string, boolean> | undefined;
|
|
42
|
+
}>>>;
|
|
33
43
|
dependencies: z.ZodDefault<z.ZodOptional<z.ZodObject<{
|
|
34
44
|
forbid: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodString, "many">>>;
|
|
35
45
|
trusted_registry: z.ZodOptional<z.ZodString>;
|
|
@@ -140,6 +150,10 @@ export declare const GatesSchema: z.ZodObject<{
|
|
|
140
150
|
max_class_dependencies: number;
|
|
141
151
|
max_function_lines: number;
|
|
142
152
|
};
|
|
153
|
+
staleness: {
|
|
154
|
+
enabled: boolean;
|
|
155
|
+
rules: Record<string, boolean>;
|
|
156
|
+
};
|
|
143
157
|
dependencies: {
|
|
144
158
|
forbid: string[];
|
|
145
159
|
trusted_registry?: string | undefined;
|
|
@@ -188,6 +202,10 @@ export declare const GatesSchema: z.ZodObject<{
|
|
|
188
202
|
max_class_dependencies?: number | undefined;
|
|
189
203
|
max_function_lines?: number | undefined;
|
|
190
204
|
} | undefined;
|
|
205
|
+
staleness?: {
|
|
206
|
+
enabled?: boolean | undefined;
|
|
207
|
+
rules?: Record<string, boolean> | undefined;
|
|
208
|
+
} | undefined;
|
|
191
209
|
dependencies?: {
|
|
192
210
|
forbid?: string[] | undefined;
|
|
193
211
|
trusted_registry?: string | undefined;
|
|
@@ -289,6 +307,16 @@ export declare const ConfigSchema: z.ZodObject<{
|
|
|
289
307
|
max_class_dependencies?: number | undefined;
|
|
290
308
|
max_function_lines?: number | undefined;
|
|
291
309
|
}>>>;
|
|
310
|
+
staleness: z.ZodDefault<z.ZodOptional<z.ZodObject<{
|
|
311
|
+
enabled: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
312
|
+
rules: z.ZodDefault<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodBoolean>>>;
|
|
313
|
+
}, "strip", z.ZodTypeAny, {
|
|
314
|
+
enabled: boolean;
|
|
315
|
+
rules: Record<string, boolean>;
|
|
316
|
+
}, {
|
|
317
|
+
enabled?: boolean | undefined;
|
|
318
|
+
rules?: Record<string, boolean> | undefined;
|
|
319
|
+
}>>>;
|
|
292
320
|
dependencies: z.ZodDefault<z.ZodOptional<z.ZodObject<{
|
|
293
321
|
forbid: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodString, "many">>>;
|
|
294
322
|
trusted_registry: z.ZodOptional<z.ZodString>;
|
|
@@ -399,6 +427,10 @@ export declare const ConfigSchema: z.ZodObject<{
|
|
|
399
427
|
max_class_dependencies: number;
|
|
400
428
|
max_function_lines: number;
|
|
401
429
|
};
|
|
430
|
+
staleness: {
|
|
431
|
+
enabled: boolean;
|
|
432
|
+
rules: Record<string, boolean>;
|
|
433
|
+
};
|
|
402
434
|
dependencies: {
|
|
403
435
|
forbid: string[];
|
|
404
436
|
trusted_registry?: string | undefined;
|
|
@@ -447,6 +479,10 @@ export declare const ConfigSchema: z.ZodObject<{
|
|
|
447
479
|
max_class_dependencies?: number | undefined;
|
|
448
480
|
max_function_lines?: number | undefined;
|
|
449
481
|
} | undefined;
|
|
482
|
+
staleness?: {
|
|
483
|
+
enabled?: boolean | undefined;
|
|
484
|
+
rules?: Record<string, boolean> | undefined;
|
|
485
|
+
} | undefined;
|
|
450
486
|
dependencies?: {
|
|
451
487
|
forbid?: string[] | undefined;
|
|
452
488
|
trusted_registry?: string | undefined;
|
|
@@ -514,6 +550,10 @@ export declare const ConfigSchema: z.ZodObject<{
|
|
|
514
550
|
max_class_dependencies: number;
|
|
515
551
|
max_function_lines: number;
|
|
516
552
|
};
|
|
553
|
+
staleness: {
|
|
554
|
+
enabled: boolean;
|
|
555
|
+
rules: Record<string, boolean>;
|
|
556
|
+
};
|
|
517
557
|
dependencies: {
|
|
518
558
|
forbid: string[];
|
|
519
559
|
trusted_registry?: string | undefined;
|
|
@@ -580,6 +620,10 @@ export declare const ConfigSchema: z.ZodObject<{
|
|
|
580
620
|
max_class_dependencies?: number | undefined;
|
|
581
621
|
max_function_lines?: number | undefined;
|
|
582
622
|
} | undefined;
|
|
623
|
+
staleness?: {
|
|
624
|
+
enabled?: boolean | undefined;
|
|
625
|
+
rules?: Record<string, boolean> | undefined;
|
|
626
|
+
} | undefined;
|
|
583
627
|
dependencies?: {
|
|
584
628
|
forbid?: string[] | undefined;
|
|
585
629
|
trusted_registry?: string | undefined;
|
package/dist/types/index.js
CHANGED
|
@@ -19,6 +19,20 @@ export const GatesSchema = z.object({
|
|
|
19
19
|
max_class_dependencies: z.number().optional().default(5),
|
|
20
20
|
max_function_lines: z.number().optional().default(50),
|
|
21
21
|
}).optional().default({}),
|
|
22
|
+
staleness: z.object({
|
|
23
|
+
enabled: z.boolean().optional().default(false),
|
|
24
|
+
// Rule-based staleness detection (toggle individual rules)
|
|
25
|
+
rules: z.record(z.boolean()).optional().default({
|
|
26
|
+
'no-var': true, // var → const/let (ES6+)
|
|
27
|
+
'no-commonjs': false, // require() → import
|
|
28
|
+
'no-arguments': false, // arguments → rest params
|
|
29
|
+
'prefer-arrow': false, // function → arrow function
|
|
30
|
+
'prefer-template': false, // 'a' + b → `a${b}`
|
|
31
|
+
'prefer-spread': false, // apply() → spread
|
|
32
|
+
'prefer-rest': false, // arguments → ...args
|
|
33
|
+
'prefer-const': false, // let (unchanged) → const
|
|
34
|
+
}),
|
|
35
|
+
}).optional().default({}),
|
|
22
36
|
dependencies: z.object({
|
|
23
37
|
forbid: z.array(z.string()).optional().default([]),
|
|
24
38
|
trusted_registry: z.string().optional(),
|
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
|
|
|
@@ -18,16 +18,149 @@ export class TypeScriptHandler extends ASTHandler {
|
|
|
18
18
|
|
|
19
19
|
private analyzeSourceFile(sourceFile: ts.SourceFile, relativePath: string, failures: Failure[]) {
|
|
20
20
|
const astConfig = this.config.ast || {};
|
|
21
|
+
const stalenessConfig = (this.config as any).staleness || {};
|
|
22
|
+
const stalenessRules = stalenessConfig.rules || {};
|
|
21
23
|
const maxComplexity = astConfig.complexity || 10;
|
|
22
24
|
const maxMethods = astConfig.max_methods || 10;
|
|
23
25
|
const maxParams = astConfig.max_params || 5;
|
|
24
26
|
|
|
27
|
+
// Limit failures per file to avoid output bloat on large files
|
|
28
|
+
const MAX_FAILURES_PER_FILE = 50;
|
|
29
|
+
const fileFailureCount: Record<string, number> = {};
|
|
30
|
+
|
|
31
|
+
const addFailure = (failure: Failure): boolean => {
|
|
32
|
+
const ruleId = failure.id;
|
|
33
|
+
fileFailureCount[ruleId] = (fileFailureCount[ruleId] || 0) + 1;
|
|
34
|
+
if (fileFailureCount[ruleId] <= MAX_FAILURES_PER_FILE) {
|
|
35
|
+
failures.push(failure);
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
// Add summary failure once when limit is reached
|
|
39
|
+
if (fileFailureCount[ruleId] === MAX_FAILURES_PER_FILE + 1) {
|
|
40
|
+
failures.push({
|
|
41
|
+
id: `${ruleId}_LIMIT_EXCEEDED`,
|
|
42
|
+
title: `More than ${MAX_FAILURES_PER_FILE} ${ruleId} violations in ${relativePath}`,
|
|
43
|
+
details: `Truncated output: showing first ${MAX_FAILURES_PER_FILE} violations. Consider fixing the root cause.`,
|
|
44
|
+
files: [relativePath],
|
|
45
|
+
hint: `This file has many violations. Fix them systematically or exclude the file if it's legacy code.`
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Helper to check if a staleness rule is enabled
|
|
52
|
+
const isRuleEnabled = (rule: string): boolean => {
|
|
53
|
+
if (!stalenessConfig.enabled) return false;
|
|
54
|
+
return stalenessRules[rule] !== false; // Enabled by default if staleness is on
|
|
55
|
+
};
|
|
56
|
+
|
|
25
57
|
const visit = (node: ts.Node) => {
|
|
58
|
+
// === STALENESS CHECKS (Rule-based) ===
|
|
59
|
+
|
|
60
|
+
// no-var: Forbid legacy 'var' keyword
|
|
61
|
+
if (isRuleEnabled('no-var') && ts.isVariableStatement(node)) {
|
|
62
|
+
const declarationList = node.declarationList;
|
|
63
|
+
// NodeFlags: Let = 1, Const = 2, None = 0 (var)
|
|
64
|
+
if ((declarationList.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) === 0) {
|
|
65
|
+
const line = sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1;
|
|
66
|
+
addFailure({
|
|
67
|
+
id: 'STALENESS_NO_VAR',
|
|
68
|
+
title: `Stale 'var' keyword at line ${line}`,
|
|
69
|
+
details: `Use 'const' or 'let' instead of 'var' in ${relativePath}:${line}`,
|
|
70
|
+
files: [relativePath],
|
|
71
|
+
hint: `Replace 'var' with 'const' (preferred) or 'let' for modern JavaScript.`
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// no-commonjs: Forbid require() in favor of import
|
|
77
|
+
if (isRuleEnabled('no-commonjs') && ts.isCallExpression(node)) {
|
|
78
|
+
if (ts.isIdentifier(node.expression) && node.expression.text === 'require') {
|
|
79
|
+
const line = sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1;
|
|
80
|
+
addFailure({
|
|
81
|
+
id: 'STALENESS_NO_COMMONJS',
|
|
82
|
+
title: `CommonJS require() at line ${line}`,
|
|
83
|
+
details: `Use ES6 'import' instead of 'require()' in ${relativePath}:${line}`,
|
|
84
|
+
files: [relativePath],
|
|
85
|
+
hint: `Replace require('module') with import module from 'module'.`
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// no-arguments: Forbid 'arguments' object (use rest params)
|
|
91
|
+
if (isRuleEnabled('no-arguments') && ts.isIdentifier(node) && node.text === 'arguments') {
|
|
92
|
+
// Check if it's actually the arguments keyword and not a variable named arguments
|
|
93
|
+
const parent = node.parent;
|
|
94
|
+
if (!ts.isVariableDeclaration(parent) && !ts.isPropertyAccessExpression(parent)) {
|
|
95
|
+
const line = sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1;
|
|
96
|
+
addFailure({
|
|
97
|
+
id: 'STALENESS_NO_ARGUMENTS',
|
|
98
|
+
title: `Legacy 'arguments' object at line ${line}`,
|
|
99
|
+
details: `Use rest parameters (...args) instead of 'arguments' in ${relativePath}:${line}`,
|
|
100
|
+
files: [relativePath],
|
|
101
|
+
hint: `Replace 'arguments' with rest parameters: function(...args) { }`
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
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
|
+
|
|
157
|
+
// === COMPLEXITY CHECKS ===
|
|
158
|
+
|
|
26
159
|
if (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node) || ts.isArrowFunction(node)) {
|
|
27
160
|
const name = this.getNodeName(node);
|
|
28
161
|
|
|
29
162
|
if (node.parameters.length > maxParams) {
|
|
30
|
-
|
|
163
|
+
addFailure({
|
|
31
164
|
id: 'AST_MAX_PARAMS',
|
|
32
165
|
title: `Function '${name}' has ${node.parameters.length} parameters (max: ${maxParams})`,
|
|
33
166
|
details: `High parameter count detected in ${relativePath}`,
|
|
@@ -54,7 +187,7 @@ export class TypeScriptHandler extends ASTHandler {
|
|
|
54
187
|
ts.forEachChild(node, countComplexity);
|
|
55
188
|
|
|
56
189
|
if (complexity > maxComplexity) {
|
|
57
|
-
|
|
190
|
+
addFailure({
|
|
58
191
|
id: 'AST_COMPLEXITY',
|
|
59
192
|
title: `Function '${name}' has cyclomatic complexity of ${complexity} (max: ${maxComplexity})`,
|
|
60
193
|
details: `High complexity detected in ${relativePath}`,
|
|
@@ -69,7 +202,7 @@ export class TypeScriptHandler extends ASTHandler {
|
|
|
69
202
|
const methods = node.members.filter(ts.isMethodDeclaration);
|
|
70
203
|
|
|
71
204
|
if (methods.length > maxMethods) {
|
|
72
|
-
|
|
205
|
+
addFailure({
|
|
73
206
|
id: 'AST_MAX_METHODS',
|
|
74
207
|
title: `Class '${name}' has ${methods.length} methods (max: ${maxMethods})`,
|
|
75
208
|
details: `God Object pattern detected in ${relativePath}`,
|
|
@@ -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
|
|
package/src/templates/index.ts
CHANGED
|
@@ -208,6 +208,19 @@ export const UNIVERSAL_CONFIG: Config = {
|
|
|
208
208
|
auto_classify: true,
|
|
209
209
|
doc_sources: {},
|
|
210
210
|
},
|
|
211
|
+
staleness: {
|
|
212
|
+
enabled: false,
|
|
213
|
+
rules: {
|
|
214
|
+
'no-var': true,
|
|
215
|
+
'no-commonjs': false,
|
|
216
|
+
'no-arguments': false,
|
|
217
|
+
'prefer-arrow': false,
|
|
218
|
+
'prefer-template': false,
|
|
219
|
+
'prefer-spread': false,
|
|
220
|
+
'prefer-rest': false,
|
|
221
|
+
'prefer-const': false,
|
|
222
|
+
},
|
|
223
|
+
},
|
|
211
224
|
},
|
|
212
225
|
output: {
|
|
213
226
|
report_path: 'rigour-report.json',
|
package/src/types/index.ts
CHANGED
|
@@ -20,6 +20,20 @@ export const GatesSchema = z.object({
|
|
|
20
20
|
max_class_dependencies: z.number().optional().default(5),
|
|
21
21
|
max_function_lines: z.number().optional().default(50),
|
|
22
22
|
}).optional().default({}),
|
|
23
|
+
staleness: z.object({
|
|
24
|
+
enabled: z.boolean().optional().default(false),
|
|
25
|
+
// Rule-based staleness detection (toggle individual rules)
|
|
26
|
+
rules: z.record(z.boolean()).optional().default({
|
|
27
|
+
'no-var': true, // var → const/let (ES6+)
|
|
28
|
+
'no-commonjs': false, // require() → import
|
|
29
|
+
'no-arguments': false, // arguments → rest params
|
|
30
|
+
'prefer-arrow': false, // function → arrow function
|
|
31
|
+
'prefer-template': false, // 'a' + b → `a${b}`
|
|
32
|
+
'prefer-spread': false, // apply() → spread
|
|
33
|
+
'prefer-rest': false, // arguments → ...args
|
|
34
|
+
'prefer-const': false, // let (unchanged) → const
|
|
35
|
+
}),
|
|
36
|
+
}).optional().default({}),
|
|
23
37
|
dependencies: z.object({
|
|
24
38
|
forbid: z.array(z.string()).optional().default([]),
|
|
25
39
|
trusted_registry: z.string().optional(),
|