@rigour-labs/core 2.22.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -0
- package/dist/context.test.js +2 -3
- package/dist/environment.test.js +2 -1
- package/dist/gates/agent-team.d.ts +2 -1
- package/dist/gates/agent-team.js +1 -0
- package/dist/gates/base.d.ts +3 -1
- package/dist/gates/base.js +3 -0
- package/dist/gates/checkpoint.d.ts +2 -1
- package/dist/gates/checkpoint.js +3 -2
- package/dist/gates/context-window-artifacts.d.ts +2 -1
- package/dist/gates/context-window-artifacts.js +6 -3
- package/dist/gates/context.d.ts +2 -1
- package/dist/gates/context.js +1 -0
- package/dist/gates/coverage.js +3 -1
- package/dist/gates/dependency.js +5 -5
- package/dist/gates/duplication-drift.d.ts +2 -1
- package/dist/gates/duplication-drift.js +4 -1
- package/dist/gates/environment.js +4 -4
- package/dist/gates/hallucinated-imports.d.ts +21 -2
- package/dist/gates/hallucinated-imports.js +116 -2
- package/dist/gates/inconsistent-error-handling.d.ts +2 -1
- package/dist/gates/inconsistent-error-handling.js +21 -7
- package/dist/gates/promise-safety.d.ts +68 -0
- package/dist/gates/promise-safety.js +509 -0
- package/dist/gates/retry-loop-breaker.d.ts +2 -1
- package/dist/gates/retry-loop-breaker.js +2 -1
- package/dist/gates/runner.js +34 -1
- package/dist/gates/safety.d.ts +2 -1
- package/dist/gates/safety.js +2 -1
- package/dist/gates/security-patterns.d.ts +2 -1
- package/dist/gates/security-patterns.js +1 -0
- package/dist/gates/structure.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/services/fix-packet-service.d.ts +0 -1
- package/dist/services/fix-packet-service.js +9 -14
- package/dist/services/score-history.d.ts +54 -0
- package/dist/services/score-history.js +122 -0
- package/dist/templates/index.js +169 -0
- package/dist/types/fix-packet.d.ts +5 -5
- package/dist/types/fix-packet.js +1 -1
- package/dist/types/index.d.ts +153 -0
- package/dist/types/index.js +19 -0
- package/package.json +21 -1
- package/src/context.test.ts +0 -256
- package/src/discovery.test.ts +0 -88
- package/src/discovery.ts +0 -112
- package/src/environment.test.ts +0 -115
- package/src/gates/agent-team.test.ts +0 -134
- package/src/gates/agent-team.ts +0 -210
- package/src/gates/ast-handlers/base.ts +0 -13
- package/src/gates/ast-handlers/python.ts +0 -145
- package/src/gates/ast-handlers/python_parser.py +0 -181
- package/src/gates/ast-handlers/typescript.ts +0 -264
- package/src/gates/ast-handlers/universal.ts +0 -184
- package/src/gates/ast.ts +0 -54
- package/src/gates/base.ts +0 -28
- package/src/gates/checkpoint.test.ts +0 -135
- package/src/gates/checkpoint.ts +0 -311
- package/src/gates/content.ts +0 -51
- package/src/gates/context-window-artifacts.ts +0 -277
- package/src/gates/context.ts +0 -270
- package/src/gates/coverage.ts +0 -74
- package/src/gates/dependency.ts +0 -108
- package/src/gates/duplication-drift.ts +0 -231
- package/src/gates/environment.ts +0 -94
- package/src/gates/file.ts +0 -46
- package/src/gates/hallucinated-imports.ts +0 -361
- package/src/gates/inconsistent-error-handling.ts +0 -254
- package/src/gates/retry-loop-breaker.ts +0 -151
- package/src/gates/runner.ts +0 -188
- package/src/gates/safety.ts +0 -56
- package/src/gates/security-patterns.test.ts +0 -162
- package/src/gates/security-patterns.ts +0 -306
- package/src/gates/structure.ts +0 -36
- package/src/index.ts +0 -13
- package/src/pattern-index/embeddings.ts +0 -84
- package/src/pattern-index/index.ts +0 -59
- package/src/pattern-index/indexer.test.ts +0 -276
- package/src/pattern-index/indexer.ts +0 -1023
- package/src/pattern-index/matcher.test.ts +0 -293
- package/src/pattern-index/matcher.ts +0 -493
- package/src/pattern-index/overrides.ts +0 -235
- package/src/pattern-index/security.ts +0 -151
- package/src/pattern-index/staleness.test.ts +0 -313
- package/src/pattern-index/staleness.ts +0 -568
- package/src/pattern-index/types.ts +0 -339
- package/src/safety.test.ts +0 -53
- package/src/services/adaptive-thresholds.test.ts +0 -189
- package/src/services/adaptive-thresholds.ts +0 -275
- package/src/services/context-engine.ts +0 -104
- package/src/services/fix-packet-service.ts +0 -42
- package/src/services/state-service.ts +0 -138
- package/src/smoke.test.ts +0 -18
- package/src/templates/index.ts +0 -338
- package/src/types/fix-packet.ts +0 -32
- package/src/types/index.ts +0 -200
- package/src/utils/logger.ts +0 -43
- package/src/utils/scanner.test.ts +0 -37
- package/src/utils/scanner.ts +0 -43
- package/tsconfig.json +0 -10
- package/vitest.config.ts +0 -7
- package/vitest.setup.ts +0 -30
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
import { execa } from 'execa';
|
|
2
|
-
import { ASTHandler, ASTHandlerContext } from './base.js';
|
|
3
|
-
import { Failure } from '../../types/index.js';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
import { fileURLToPath } from 'url';
|
|
6
|
-
|
|
7
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
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
|
-
|
|
32
|
-
export class PythonHandler extends ASTHandler {
|
|
33
|
-
supports(file: string): boolean {
|
|
34
|
-
return /\.py$/.test(file);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async run(context: ASTHandlerContext): Promise<Failure[]> {
|
|
38
|
-
const failures: Failure[] = [];
|
|
39
|
-
const scriptPath = path.join(__dirname, 'python_parser.py');
|
|
40
|
-
|
|
41
|
-
// Dynamic command detection for cross-platform support (Mac/Linux usually python3, Windows usually python)
|
|
42
|
-
let pythonCmd = 'python3';
|
|
43
|
-
try {
|
|
44
|
-
await execa('python3', ['--version']);
|
|
45
|
-
} catch (e) {
|
|
46
|
-
try {
|
|
47
|
-
await execa('python', ['--version']);
|
|
48
|
-
pythonCmd = 'python';
|
|
49
|
-
} catch (e2) {
|
|
50
|
-
// Both missing - handled by main catch
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
try {
|
|
55
|
-
const { stdout } = await execa(pythonCmd, [scriptPath], {
|
|
56
|
-
input: context.content,
|
|
57
|
-
cwd: context.cwd
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
const result: PythonAnalysisResult = JSON.parse(stdout);
|
|
61
|
-
if (result.error) return [];
|
|
62
|
-
|
|
63
|
-
const astConfig = this.config.ast || {};
|
|
64
|
-
const safetyConfig = this.config.safety || {};
|
|
65
|
-
const maxComplexity = astConfig.complexity || 10;
|
|
66
|
-
const maxParams = astConfig.max_params || 5;
|
|
67
|
-
const maxMethods = astConfig.max_methods || 10;
|
|
68
|
-
|
|
69
|
-
// Process metrics (complexity, params, methods)
|
|
70
|
-
const metrics = result.metrics || [];
|
|
71
|
-
for (const item of metrics) {
|
|
72
|
-
if (item.type === 'function') {
|
|
73
|
-
if (item.parameters && item.parameters > maxParams) {
|
|
74
|
-
failures.push({
|
|
75
|
-
id: 'AST_MAX_PARAMS',
|
|
76
|
-
title: `Function '${item.name}' has ${item.parameters} parameters (max: ${maxParams})`,
|
|
77
|
-
details: `High parameter count detected in ${context.file} at line ${item.lineno}`,
|
|
78
|
-
files: [context.file],
|
|
79
|
-
hint: `Reduce number of parameters or use an options object.`
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
if (item.complexity && item.complexity > maxComplexity) {
|
|
83
|
-
failures.push({
|
|
84
|
-
id: 'AST_COMPLEXITY',
|
|
85
|
-
title: `Function '${item.name}' has complexity of ${item.complexity} (max: ${maxComplexity})`,
|
|
86
|
-
details: `High complexity detected in ${context.file} at line ${item.lineno}`,
|
|
87
|
-
files: [context.file],
|
|
88
|
-
hint: `Refactor '${item.name}' into smaller, more focused functions.`
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
} else if (item.type === 'class') {
|
|
92
|
-
if (item.methods && item.methods > maxMethods) {
|
|
93
|
-
failures.push({
|
|
94
|
-
id: 'AST_MAX_METHODS',
|
|
95
|
-
title: `Class '${item.name}' has ${item.methods} methods (max: ${maxMethods})`,
|
|
96
|
-
details: `God Object pattern detected in ${context.file} at line ${item.lineno}`,
|
|
97
|
-
files: [context.file],
|
|
98
|
-
hint: `Split class '${item.name}' into smaller services.`
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
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
|
-
|
|
127
|
-
} catch (e: any) {
|
|
128
|
-
// If python3 is missing, we skip AST but other gates still run
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return failures;
|
|
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
|
-
}
|
|
145
|
-
}
|
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
import ast
|
|
2
|
-
import sys
|
|
3
|
-
import json
|
|
4
|
-
import re
|
|
5
|
-
|
|
6
|
-
class MetricsVisitor(ast.NodeVisitor):
|
|
7
|
-
def __init__(self):
|
|
8
|
-
self.metrics = []
|
|
9
|
-
|
|
10
|
-
def visit_FunctionDef(self, node):
|
|
11
|
-
self.analyze_function(node)
|
|
12
|
-
self.generic_visit(node)
|
|
13
|
-
|
|
14
|
-
def visit_AsyncFunctionDef(self, node):
|
|
15
|
-
self.analyze_function(node)
|
|
16
|
-
self.generic_visit(node)
|
|
17
|
-
|
|
18
|
-
def visit_ClassDef(self, node):
|
|
19
|
-
methods = [n for n in node.body if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))]
|
|
20
|
-
self.metrics.append({
|
|
21
|
-
"type": "class",
|
|
22
|
-
"name": node.name,
|
|
23
|
-
"methods": len(methods),
|
|
24
|
-
"lineno": node.lineno
|
|
25
|
-
})
|
|
26
|
-
self.generic_visit(node)
|
|
27
|
-
|
|
28
|
-
def analyze_function(self, node):
|
|
29
|
-
complexity = 1
|
|
30
|
-
for n in ast.walk(node):
|
|
31
|
-
if isinstance(n, (ast.If, ast.While, ast.For, ast.AsyncFor, ast.Try, ast.ExceptHandler, ast.With, ast.AsyncWith)):
|
|
32
|
-
complexity += 1
|
|
33
|
-
elif isinstance(n, ast.BoolOp):
|
|
34
|
-
complexity += len(n.values) - 1
|
|
35
|
-
elif isinstance(n, ast.IfExp):
|
|
36
|
-
complexity += 1
|
|
37
|
-
|
|
38
|
-
params = len(node.args.args) + len(node.args.kwonlyargs)
|
|
39
|
-
if node.args.vararg: params += 1
|
|
40
|
-
if node.args.kwarg: params += 1
|
|
41
|
-
|
|
42
|
-
self.metrics.append({
|
|
43
|
-
"type": "function",
|
|
44
|
-
"name": node.name,
|
|
45
|
-
"complexity": complexity,
|
|
46
|
-
"parameters": params,
|
|
47
|
-
"lineno": node.lineno
|
|
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
|
-
|
|
159
|
-
def analyze_code(content):
|
|
160
|
-
try:
|
|
161
|
-
tree = ast.parse(content)
|
|
162
|
-
|
|
163
|
-
# Collect metrics
|
|
164
|
-
visitor = MetricsVisitor()
|
|
165
|
-
visitor.visit(tree)
|
|
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
|
-
}
|
|
176
|
-
except Exception as e:
|
|
177
|
-
return {"error": str(e)}
|
|
178
|
-
|
|
179
|
-
if __name__ == "__main__":
|
|
180
|
-
content = sys.stdin.read()
|
|
181
|
-
print(json.dumps(analyze_code(content)))
|
|
@@ -1,264 +0,0 @@
|
|
|
1
|
-
import ts from 'typescript';
|
|
2
|
-
import { ASTHandler, ASTHandlerContext } from './base.js';
|
|
3
|
-
import { Failure } from '../../types/index.js';
|
|
4
|
-
import micromatch from 'micromatch';
|
|
5
|
-
import path from 'path';
|
|
6
|
-
|
|
7
|
-
export class TypeScriptHandler extends ASTHandler {
|
|
8
|
-
supports(file: string): boolean {
|
|
9
|
-
return /\.(ts|js|tsx|jsx)$/.test(file);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
async run(context: ASTHandlerContext): Promise<Failure[]> {
|
|
13
|
-
const failures: Failure[] = [];
|
|
14
|
-
const sourceFile = ts.createSourceFile(context.file, context.content, ts.ScriptTarget.Latest, true);
|
|
15
|
-
this.analyzeSourceFile(sourceFile, context.file, failures);
|
|
16
|
-
return failures;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
private analyzeSourceFile(sourceFile: ts.SourceFile, relativePath: string, failures: Failure[]) {
|
|
20
|
-
const astConfig = this.config.ast || {};
|
|
21
|
-
const stalenessConfig = (this.config as any).staleness || {};
|
|
22
|
-
const stalenessRules = stalenessConfig.rules || {};
|
|
23
|
-
const maxComplexity = astConfig.complexity || 10;
|
|
24
|
-
const maxMethods = astConfig.max_methods || 10;
|
|
25
|
-
const maxParams = astConfig.max_params || 5;
|
|
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
|
-
|
|
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`,
|
|
69
|
-
details: `Use 'const' or 'let' instead of 'var' in ${relativePath}:${line}`,
|
|
70
|
-
files: [relativePath],
|
|
71
|
-
line,
|
|
72
|
-
hint: `Replace 'var' with 'const' (preferred) or 'let' for modern JavaScript.`
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// no-commonjs: Forbid require() in favor of import
|
|
78
|
-
if (isRuleEnabled('no-commonjs') && ts.isCallExpression(node)) {
|
|
79
|
-
if (ts.isIdentifier(node.expression) && node.expression.text === 'require') {
|
|
80
|
-
const line = sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1;
|
|
81
|
-
addFailure({
|
|
82
|
-
id: 'STALENESS_NO_COMMONJS',
|
|
83
|
-
title: `CommonJS require()`,
|
|
84
|
-
details: `Use ES6 'import' instead of 'require()' in ${relativePath}:${line}`,
|
|
85
|
-
files: [relativePath],
|
|
86
|
-
line,
|
|
87
|
-
hint: `Replace require('module') with import module from 'module'.`
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// no-arguments: Forbid 'arguments' object (use rest params)
|
|
93
|
-
if (isRuleEnabled('no-arguments') && ts.isIdentifier(node) && node.text === 'arguments') {
|
|
94
|
-
// Check if it's actually the arguments keyword and not a variable named arguments
|
|
95
|
-
const parent = node.parent;
|
|
96
|
-
if (!ts.isVariableDeclaration(parent) && !ts.isPropertyAccessExpression(parent)) {
|
|
97
|
-
const line = sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1;
|
|
98
|
-
addFailure({
|
|
99
|
-
id: 'STALENESS_NO_ARGUMENTS',
|
|
100
|
-
title: `Legacy 'arguments' object`,
|
|
101
|
-
details: `Use rest parameters (...args) instead of 'arguments' in ${relativePath}:${line}`,
|
|
102
|
-
files: [relativePath],
|
|
103
|
-
line,
|
|
104
|
-
hint: `Replace 'arguments' with rest parameters: function(...args) { }`
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// === SECURITY CHECKS (Prototype Pollution) ===
|
|
110
|
-
|
|
111
|
-
// Check for direct __proto__ access: obj.__proto__
|
|
112
|
-
if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.name) && node.name.text === '__proto__') {
|
|
113
|
-
const line = sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1;
|
|
114
|
-
addFailure({
|
|
115
|
-
id: 'SECURITY_PROTOTYPE_POLLUTION',
|
|
116
|
-
title: `Direct __proto__ access`,
|
|
117
|
-
details: `Prototype pollution vulnerability in ${relativePath}:${line}`,
|
|
118
|
-
files: [relativePath],
|
|
119
|
-
line,
|
|
120
|
-
hint: `Use Object.getPrototypeOf() or Object.setPrototypeOf() instead of __proto__.`
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Check for bracket notation __proto__ access: obj["__proto__"]
|
|
125
|
-
if (ts.isElementAccessExpression(node) && ts.isStringLiteral(node.argumentExpression)) {
|
|
126
|
-
const accessKey = node.argumentExpression.text;
|
|
127
|
-
if (accessKey === '__proto__' || accessKey === 'constructor' || accessKey === 'prototype') {
|
|
128
|
-
const line = sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1;
|
|
129
|
-
addFailure({
|
|
130
|
-
id: 'SECURITY_PROTOTYPE_POLLUTION',
|
|
131
|
-
title: `Unsafe bracket notation access to '${accessKey}'`,
|
|
132
|
-
details: `Potential prototype pollution via bracket notation in ${relativePath}:${line}`,
|
|
133
|
-
files: [relativePath],
|
|
134
|
-
line,
|
|
135
|
-
hint: `Block access to '${accessKey}' property when handling user input. Use allowlist for object keys.`
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Check for Object.assign with user-controllable input (common prototype pollution pattern)
|
|
141
|
-
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
|
|
142
|
-
const propAccess = node.expression;
|
|
143
|
-
if (ts.isIdentifier(propAccess.expression) && propAccess.expression.text === 'Object' &&
|
|
144
|
-
ts.isIdentifier(propAccess.name) && propAccess.name.text === 'assign') {
|
|
145
|
-
// This is Object.assign() - warn if first arg is empty object (merge pattern)
|
|
146
|
-
if (node.arguments.length >= 2) {
|
|
147
|
-
const firstArg = node.arguments[0];
|
|
148
|
-
if (ts.isObjectLiteralExpression(firstArg) && firstArg.properties.length === 0) {
|
|
149
|
-
const line = sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1;
|
|
150
|
-
addFailure({
|
|
151
|
-
id: 'SECURITY_PROTOTYPE_POLLUTION_MERGE',
|
|
152
|
-
title: `Object.assign() merge pattern`,
|
|
153
|
-
details: `Object.assign({}, ...) can propagate prototype pollution in ${relativePath}:${line}`,
|
|
154
|
-
files: [relativePath],
|
|
155
|
-
line,
|
|
156
|
-
hint: `Validate and sanitize source objects before merging. Block __proto__ and constructor keys.`
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// === COMPLEXITY CHECKS ===
|
|
164
|
-
|
|
165
|
-
if (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node) || ts.isArrowFunction(node)) {
|
|
166
|
-
const name = this.getNodeName(node);
|
|
167
|
-
|
|
168
|
-
if (node.parameters.length > maxParams) {
|
|
169
|
-
addFailure({
|
|
170
|
-
id: 'AST_MAX_PARAMS',
|
|
171
|
-
title: `Function '${name}' has ${node.parameters.length} parameters (max: ${maxParams})`,
|
|
172
|
-
details: `High parameter count detected in ${relativePath}`,
|
|
173
|
-
files: [relativePath],
|
|
174
|
-
hint: `Reduce number of parameters or use an options object.`
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
let complexity = 1;
|
|
179
|
-
const countComplexity = (n: ts.Node) => {
|
|
180
|
-
if (ts.isIfStatement(n) || ts.isCaseClause(n) || ts.isDefaultClause(n) ||
|
|
181
|
-
ts.isForStatement(n) || ts.isForInStatement(n) || ts.isForOfStatement(n) ||
|
|
182
|
-
ts.isWhileStatement(n) || ts.isDoStatement(n) || ts.isConditionalExpression(n)) {
|
|
183
|
-
complexity++;
|
|
184
|
-
}
|
|
185
|
-
if (ts.isBinaryExpression(n)) {
|
|
186
|
-
if (n.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken ||
|
|
187
|
-
n.operatorToken.kind === ts.SyntaxKind.BarBarToken) {
|
|
188
|
-
complexity++;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
ts.forEachChild(n, countComplexity);
|
|
192
|
-
};
|
|
193
|
-
ts.forEachChild(node, countComplexity);
|
|
194
|
-
|
|
195
|
-
if (complexity > maxComplexity) {
|
|
196
|
-
addFailure({
|
|
197
|
-
id: 'AST_COMPLEXITY',
|
|
198
|
-
title: `Function '${name}' has cyclomatic complexity of ${complexity} (max: ${maxComplexity})`,
|
|
199
|
-
details: `High complexity detected in ${relativePath}`,
|
|
200
|
-
files: [relativePath],
|
|
201
|
-
hint: `Refactor '${name}' into smaller, more focused functions.`
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
if (ts.isClassDeclaration(node)) {
|
|
207
|
-
const name = node.name?.text || 'Anonymous Class';
|
|
208
|
-
const methods = node.members.filter(ts.isMethodDeclaration);
|
|
209
|
-
|
|
210
|
-
if (methods.length > maxMethods) {
|
|
211
|
-
addFailure({
|
|
212
|
-
id: 'AST_MAX_METHODS',
|
|
213
|
-
title: `Class '${name}' has ${methods.length} methods (max: ${maxMethods})`,
|
|
214
|
-
details: `God Object pattern detected in ${relativePath}`,
|
|
215
|
-
files: [relativePath],
|
|
216
|
-
hint: `Class '${name}' is becoming too large. Split it into smaller services.`
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
if (ts.isImportDeclaration(node)) {
|
|
222
|
-
const importPath = (node.moduleSpecifier as ts.StringLiteral).text;
|
|
223
|
-
this.checkBoundary(importPath, relativePath, failures);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
ts.forEachChild(node, visit);
|
|
227
|
-
};
|
|
228
|
-
|
|
229
|
-
ts.forEachChild(sourceFile, visit);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
private checkBoundary(importPath: string, relativePath: string, failures: Failure[]) {
|
|
233
|
-
const boundaries = (this.config as any).architecture?.boundaries || [];
|
|
234
|
-
for (const rule of boundaries) {
|
|
235
|
-
if (micromatch.isMatch(relativePath, rule.from)) {
|
|
236
|
-
const resolved = importPath.startsWith('.')
|
|
237
|
-
? path.join(path.dirname(relativePath), importPath)
|
|
238
|
-
: importPath;
|
|
239
|
-
|
|
240
|
-
if (rule.mode === 'deny' && micromatch.isMatch(resolved, rule.to)) {
|
|
241
|
-
failures.push({
|
|
242
|
-
id: 'ARCH_BOUNDARY',
|
|
243
|
-
title: `Architectural Violation`,
|
|
244
|
-
details: `'${relativePath}' is forbidden from importing '${importPath}' (denied by boundary rule).`,
|
|
245
|
-
files: [relativePath],
|
|
246
|
-
hint: `Remove this import to maintain architectural layering.`
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
private getNodeName(node: ts.Node): string {
|
|
254
|
-
if (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node)) {
|
|
255
|
-
return node.name?.getText() || 'anonymous';
|
|
256
|
-
}
|
|
257
|
-
if (ts.isArrowFunction(node)) {
|
|
258
|
-
const parent = node.parent;
|
|
259
|
-
if (ts.isVariableDeclaration(parent)) return parent.name.getText();
|
|
260
|
-
return 'anonymous arrow';
|
|
261
|
-
}
|
|
262
|
-
return 'unknown';
|
|
263
|
-
}
|
|
264
|
-
}
|