@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.
@@ -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
  }
@@ -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
- failures.push({
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
- failures.push({
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
- failures.push({
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
  /**
@@ -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',
@@ -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;
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rigour-labs/core",
3
- "version": "2.15.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
 
@@ -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
- failures.push({
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
- failures.push({
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
- failures.push({
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
 
@@ -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',
@@ -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(),