ai-code-audit 0.1.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 +150 -0
- package/dist/auditor.d.ts +6 -0
- package/dist/auditor.js +197 -0
- package/dist/auditor.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +231 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/rules/ai-specific.d.ts +6 -0
- package/dist/rules/ai-specific.js +125 -0
- package/dist/rules/ai-specific.js.map +1 -0
- package/dist/rules/index.d.ts +6 -0
- package/dist/rules/index.js +15 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/rules/quality.d.ts +7 -0
- package/dist/rules/quality.js +126 -0
- package/dist/rules/quality.js.map +1 -0
- package/dist/rules/security.d.ts +8 -0
- package/dist/rules/security.js +171 -0
- package/dist/rules/security.js.map +1 -0
- package/dist/types.d.ts +29 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +38 -0
- package/src/auditor.ts +226 -0
- package/src/cli.ts +274 -0
- package/src/index.ts +5 -0
- package/src/rules/ai-specific.ts +170 -0
- package/src/rules/index.ts +12 -0
- package/src/rules/quality.ts +169 -0
- package/src/rules/security.ts +247 -0
- package/src/types.ts +33 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import type { Rule, Finding } from '../types.js';
|
|
2
|
+
|
|
3
|
+
function findMatches(
|
|
4
|
+
content: string,
|
|
5
|
+
pattern: RegExp,
|
|
6
|
+
rule: string,
|
|
7
|
+
severity: 'info' | 'warning' | 'error',
|
|
8
|
+
message: string,
|
|
9
|
+
filename: string
|
|
10
|
+
): Finding[] {
|
|
11
|
+
const findings: Finding[] = [];
|
|
12
|
+
const lines = content.split('\n');
|
|
13
|
+
|
|
14
|
+
for (let i = 0; i < lines.length; i++) {
|
|
15
|
+
const line = lines[i];
|
|
16
|
+
let match;
|
|
17
|
+
|
|
18
|
+
pattern.lastIndex = 0;
|
|
19
|
+
|
|
20
|
+
while ((match = pattern.exec(line)) !== null) {
|
|
21
|
+
findings.push({
|
|
22
|
+
rule,
|
|
23
|
+
severity,
|
|
24
|
+
message,
|
|
25
|
+
file: filename,
|
|
26
|
+
line: i + 1,
|
|
27
|
+
column: match.index + 1,
|
|
28
|
+
snippet: line.trim(),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (!pattern.global) break;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return findings;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const missingErrorHandling: Rule = {
|
|
39
|
+
name: 'missing-error-handling',
|
|
40
|
+
description: 'Detects async operations without try/catch',
|
|
41
|
+
severity: 'warning',
|
|
42
|
+
languages: ['javascript', 'typescript'],
|
|
43
|
+
check: (content, filename) => {
|
|
44
|
+
const findings: Finding[] = [];
|
|
45
|
+
const lines = content.split('\n');
|
|
46
|
+
|
|
47
|
+
// Look for await outside of try blocks
|
|
48
|
+
let inTry = 0;
|
|
49
|
+
|
|
50
|
+
for (let i = 0; i < lines.length; i++) {
|
|
51
|
+
const line = lines[i];
|
|
52
|
+
|
|
53
|
+
if (/\btry\s*\{/.test(line)) inTry++;
|
|
54
|
+
if (/\}\s*catch/.test(line)) inTry = Math.max(0, inTry - 1);
|
|
55
|
+
|
|
56
|
+
if (inTry === 0 && /\bawait\s+/.test(line)) {
|
|
57
|
+
// Check if it's in an async function without surrounding try
|
|
58
|
+
findings.push({
|
|
59
|
+
rule: 'missing-error-handling',
|
|
60
|
+
severity: 'warning',
|
|
61
|
+
message: 'Async operation without error handling',
|
|
62
|
+
file: filename,
|
|
63
|
+
line: i + 1,
|
|
64
|
+
column: line.indexOf('await') + 1,
|
|
65
|
+
snippet: line.trim(),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return findings;
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const emptyCatch: Rule = {
|
|
75
|
+
name: 'empty-catch',
|
|
76
|
+
description: 'Detects empty catch blocks that swallow errors',
|
|
77
|
+
severity: 'warning',
|
|
78
|
+
languages: ['javascript', 'typescript'],
|
|
79
|
+
check: (content, filename) => {
|
|
80
|
+
const pattern = /catch\s*\([^)]*\)\s*\{\s*\}/g;
|
|
81
|
+
return findMatches(
|
|
82
|
+
content,
|
|
83
|
+
pattern,
|
|
84
|
+
'empty-catch',
|
|
85
|
+
'warning',
|
|
86
|
+
'Empty catch block swallows errors',
|
|
87
|
+
filename
|
|
88
|
+
);
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const consoleLog: Rule = {
|
|
93
|
+
name: 'console-log',
|
|
94
|
+
description: 'Detects console.log left in code',
|
|
95
|
+
severity: 'info',
|
|
96
|
+
languages: ['javascript', 'typescript'],
|
|
97
|
+
check: (content, filename) => {
|
|
98
|
+
const pattern = /console\.log\s*\(/g;
|
|
99
|
+
return findMatches(
|
|
100
|
+
content,
|
|
101
|
+
pattern,
|
|
102
|
+
'console-log',
|
|
103
|
+
'info',
|
|
104
|
+
'console.log statement found',
|
|
105
|
+
filename
|
|
106
|
+
);
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export const todoFixme: Rule = {
|
|
111
|
+
name: 'todo-fixme',
|
|
112
|
+
description: 'Detects TODO/FIXME comments',
|
|
113
|
+
severity: 'info',
|
|
114
|
+
languages: ['javascript', 'typescript', 'python', 'ruby', 'go'],
|
|
115
|
+
check: (content, filename) => {
|
|
116
|
+
const pattern = /\/\/\s*(?:TODO|FIXME|HACK|XXX|BUG)[\s:]/gi;
|
|
117
|
+
return findMatches(
|
|
118
|
+
content,
|
|
119
|
+
pattern,
|
|
120
|
+
'todo-fixme',
|
|
121
|
+
'info',
|
|
122
|
+
'TODO/FIXME comment found',
|
|
123
|
+
filename
|
|
124
|
+
);
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export const deepNesting: Rule = {
|
|
129
|
+
name: 'deep-nesting',
|
|
130
|
+
description: 'Detects deeply nested code (>4 levels)',
|
|
131
|
+
severity: 'warning',
|
|
132
|
+
languages: ['javascript', 'typescript'],
|
|
133
|
+
check: (content, filename) => {
|
|
134
|
+
const findings: Finding[] = [];
|
|
135
|
+
const lines = content.split('\n');
|
|
136
|
+
let depth = 0;
|
|
137
|
+
const MAX_DEPTH = 4;
|
|
138
|
+
|
|
139
|
+
for (let i = 0; i < lines.length; i++) {
|
|
140
|
+
const line = lines[i];
|
|
141
|
+
const opens = (line.match(/\{/g) || []).length;
|
|
142
|
+
const closes = (line.match(/\}/g) || []).length;
|
|
143
|
+
|
|
144
|
+
depth += opens - closes;
|
|
145
|
+
|
|
146
|
+
if (depth > MAX_DEPTH && opens > 0) {
|
|
147
|
+
findings.push({
|
|
148
|
+
rule: 'deep-nesting',
|
|
149
|
+
severity: 'warning',
|
|
150
|
+
message: `Code nested ${depth} levels deep (max: ${MAX_DEPTH})`,
|
|
151
|
+
file: filename,
|
|
152
|
+
line: i + 1,
|
|
153
|
+
column: 1,
|
|
154
|
+
snippet: line.trim(),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return findings;
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
export const qualityRules: Rule[] = [
|
|
164
|
+
missingErrorHandling,
|
|
165
|
+
emptyCatch,
|
|
166
|
+
consoleLog,
|
|
167
|
+
todoFixme,
|
|
168
|
+
deepNesting,
|
|
169
|
+
];
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import type { Rule, Finding } from '../types.js';
|
|
2
|
+
|
|
3
|
+
function findMatches(
|
|
4
|
+
content: string,
|
|
5
|
+
pattern: RegExp,
|
|
6
|
+
rule: string,
|
|
7
|
+
severity: 'info' | 'warning' | 'error',
|
|
8
|
+
message: string,
|
|
9
|
+
filename: string
|
|
10
|
+
): Finding[] {
|
|
11
|
+
const findings: Finding[] = [];
|
|
12
|
+
const lines = content.split('\n');
|
|
13
|
+
|
|
14
|
+
for (let i = 0; i < lines.length; i++) {
|
|
15
|
+
const line = lines[i];
|
|
16
|
+
let match;
|
|
17
|
+
|
|
18
|
+
// Reset regex for global patterns
|
|
19
|
+
pattern.lastIndex = 0;
|
|
20
|
+
|
|
21
|
+
while ((match = pattern.exec(line)) !== null) {
|
|
22
|
+
findings.push({
|
|
23
|
+
rule,
|
|
24
|
+
severity,
|
|
25
|
+
message,
|
|
26
|
+
file: filename,
|
|
27
|
+
line: i + 1,
|
|
28
|
+
column: match.index + 1,
|
|
29
|
+
snippet: line.trim(),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Prevent infinite loop for non-global regex
|
|
33
|
+
if (!pattern.global) break;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return findings;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const sqlInjection: Rule = {
|
|
41
|
+
name: 'sql-injection',
|
|
42
|
+
description: 'Detects SQL queries with string interpolation',
|
|
43
|
+
severity: 'error',
|
|
44
|
+
languages: ['javascript', 'typescript', 'python'],
|
|
45
|
+
check: (content, filename) => {
|
|
46
|
+
const patterns = [
|
|
47
|
+
// Template literals in SQL
|
|
48
|
+
/(?:query|execute|sql)\s*\(\s*`[^`]*\$\{/gi,
|
|
49
|
+
// String concatenation in SQL
|
|
50
|
+
/(?:query|execute|sql)\s*\(\s*['"][^'"]*['"]\s*\+/gi,
|
|
51
|
+
// Python f-strings in SQL
|
|
52
|
+
/(?:execute|cursor\.execute)\s*\(\s*f['"][^'"]*\{/gi,
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
const findings: Finding[] = [];
|
|
56
|
+
for (const pattern of patterns) {
|
|
57
|
+
findings.push(
|
|
58
|
+
...findMatches(
|
|
59
|
+
content,
|
|
60
|
+
pattern,
|
|
61
|
+
'sql-injection',
|
|
62
|
+
'error',
|
|
63
|
+
'SQL injection risk: query uses string interpolation',
|
|
64
|
+
filename
|
|
65
|
+
)
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
return findings;
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const commandInjection: Rule = {
|
|
73
|
+
name: 'command-injection',
|
|
74
|
+
description: 'Detects shell commands with unsanitized input',
|
|
75
|
+
severity: 'error',
|
|
76
|
+
languages: ['javascript', 'typescript', 'python'],
|
|
77
|
+
check: (content, filename) => {
|
|
78
|
+
const patterns = [
|
|
79
|
+
// exec with template literal
|
|
80
|
+
/(?:exec|execSync|spawn|spawnSync)\s*\(\s*`[^`]*\$\{/gi,
|
|
81
|
+
// exec with concatenation
|
|
82
|
+
/(?:exec|execSync|spawn|spawnSync)\s*\(\s*['"][^'"]*['"]\s*\+/gi,
|
|
83
|
+
// Python subprocess with f-string
|
|
84
|
+
/subprocess\.(?:run|call|Popen)\s*\(\s*f['"][^'"]*\{/gi,
|
|
85
|
+
// shell=True in Python
|
|
86
|
+
/subprocess\.(?:run|call|Popen)\s*\([^)]*shell\s*=\s*True/gi,
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
const findings: Finding[] = [];
|
|
90
|
+
for (const pattern of patterns) {
|
|
91
|
+
findings.push(
|
|
92
|
+
...findMatches(
|
|
93
|
+
content,
|
|
94
|
+
pattern,
|
|
95
|
+
'command-injection',
|
|
96
|
+
'error',
|
|
97
|
+
'Command injection risk: shell command with unsanitized input',
|
|
98
|
+
filename
|
|
99
|
+
)
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
return findings;
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const hardcodedSecret: Rule = {
|
|
107
|
+
name: 'hardcoded-secret',
|
|
108
|
+
description: 'Detects hardcoded API keys, passwords, tokens',
|
|
109
|
+
severity: 'error',
|
|
110
|
+
languages: ['javascript', 'typescript', 'python', 'ruby', 'go'],
|
|
111
|
+
check: (content, filename) => {
|
|
112
|
+
const patterns = [
|
|
113
|
+
// API keys
|
|
114
|
+
/(?:api[_-]?key|apikey)\s*[:=]\s*['"][a-zA-Z0-9_\-]{20,}['"]/gi,
|
|
115
|
+
// AWS keys
|
|
116
|
+
/(?:AKIA|ABIA|ACCA|ASIA)[A-Z0-9]{16}/g,
|
|
117
|
+
// Generic secrets
|
|
118
|
+
/(?:password|passwd|pwd|secret|token)\s*[:=]\s*['"][^'"]{8,}['"]/gi,
|
|
119
|
+
// Bearer tokens
|
|
120
|
+
/['"]Bearer\s+[a-zA-Z0-9_\-\.]+['"]/g,
|
|
121
|
+
// Private keys
|
|
122
|
+
/-----BEGIN\s+(?:RSA\s+)?PRIVATE\s+KEY-----/g,
|
|
123
|
+
// GitHub tokens
|
|
124
|
+
/gh[pousr]_[A-Za-z0-9_]{36,}/g,
|
|
125
|
+
// Slack tokens
|
|
126
|
+
/xox[baprs]-[A-Za-z0-9-]+/g,
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
const findings: Finding[] = [];
|
|
130
|
+
for (const pattern of patterns) {
|
|
131
|
+
findings.push(
|
|
132
|
+
...findMatches(
|
|
133
|
+
content,
|
|
134
|
+
pattern,
|
|
135
|
+
'hardcoded-secret',
|
|
136
|
+
'error',
|
|
137
|
+
'Hardcoded secret detected',
|
|
138
|
+
filename
|
|
139
|
+
)
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
return findings;
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export const unsafeEval: Rule = {
|
|
147
|
+
name: 'unsafe-eval',
|
|
148
|
+
description: 'Detects use of eval() or Function()',
|
|
149
|
+
severity: 'error',
|
|
150
|
+
languages: ['javascript', 'typescript'],
|
|
151
|
+
check: (content, filename) => {
|
|
152
|
+
const patterns = [
|
|
153
|
+
/\beval\s*\(/g,
|
|
154
|
+
/new\s+Function\s*\(/g,
|
|
155
|
+
/setTimeout\s*\(\s*['"`]/g,
|
|
156
|
+
/setInterval\s*\(\s*['"`]/g,
|
|
157
|
+
];
|
|
158
|
+
|
|
159
|
+
const findings: Finding[] = [];
|
|
160
|
+
for (const pattern of patterns) {
|
|
161
|
+
findings.push(
|
|
162
|
+
...findMatches(
|
|
163
|
+
content,
|
|
164
|
+
pattern,
|
|
165
|
+
'unsafe-eval',
|
|
166
|
+
'error',
|
|
167
|
+
'Unsafe eval() or Function() usage detected',
|
|
168
|
+
filename
|
|
169
|
+
)
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
return findings;
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
export const pathTraversal: Rule = {
|
|
177
|
+
name: 'path-traversal',
|
|
178
|
+
description: 'Detects unsanitized file path operations',
|
|
179
|
+
severity: 'error',
|
|
180
|
+
languages: ['javascript', 'typescript', 'python'],
|
|
181
|
+
check: (content, filename) => {
|
|
182
|
+
const patterns = [
|
|
183
|
+
// Direct path concatenation
|
|
184
|
+
/(?:readFile|writeFile|unlink|rmdir|mkdir)(?:Sync)?\s*\(\s*(?:req\.|request\.|params\.|query\.)/gi,
|
|
185
|
+
// Python path with user input
|
|
186
|
+
/open\s*\(\s*(?:request\.|args\.|kwargs\.)/gi,
|
|
187
|
+
// Path join with user input
|
|
188
|
+
/path\.join\s*\([^)]*(?:req\.|request\.|params\.|query\.)/gi,
|
|
189
|
+
];
|
|
190
|
+
|
|
191
|
+
const findings: Finding[] = [];
|
|
192
|
+
for (const pattern of patterns) {
|
|
193
|
+
findings.push(
|
|
194
|
+
...findMatches(
|
|
195
|
+
content,
|
|
196
|
+
pattern,
|
|
197
|
+
'path-traversal',
|
|
198
|
+
'error',
|
|
199
|
+
'Path traversal risk: file operation with user input',
|
|
200
|
+
filename
|
|
201
|
+
)
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
return findings;
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
export const xssRisk: Rule = {
|
|
209
|
+
name: 'xss-risk',
|
|
210
|
+
description: 'Detects potential XSS in HTML generation',
|
|
211
|
+
severity: 'warning',
|
|
212
|
+
languages: ['javascript', 'typescript'],
|
|
213
|
+
check: (content, filename) => {
|
|
214
|
+
const patterns = [
|
|
215
|
+
// innerHTML with variable
|
|
216
|
+
/\.innerHTML\s*=\s*[^'"]/g,
|
|
217
|
+
// document.write
|
|
218
|
+
/document\.write\s*\(/g,
|
|
219
|
+
// dangerouslySetInnerHTML
|
|
220
|
+
/dangerouslySetInnerHTML\s*=\s*\{\s*\{\s*__html\s*:/g,
|
|
221
|
+
];
|
|
222
|
+
|
|
223
|
+
const findings: Finding[] = [];
|
|
224
|
+
for (const pattern of patterns) {
|
|
225
|
+
findings.push(
|
|
226
|
+
...findMatches(
|
|
227
|
+
content,
|
|
228
|
+
pattern,
|
|
229
|
+
'xss-risk',
|
|
230
|
+
'warning',
|
|
231
|
+
'Potential XSS risk: unsafe HTML manipulation',
|
|
232
|
+
filename
|
|
233
|
+
)
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
return findings;
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
export const securityRules: Rule[] = [
|
|
241
|
+
sqlInjection,
|
|
242
|
+
commandInjection,
|
|
243
|
+
hardcodedSecret,
|
|
244
|
+
unsafeEval,
|
|
245
|
+
pathTraversal,
|
|
246
|
+
xssRisk,
|
|
247
|
+
];
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type Severity = 'info' | 'warning' | 'error';
|
|
2
|
+
|
|
3
|
+
export interface Finding {
|
|
4
|
+
rule: string;
|
|
5
|
+
severity: Severity;
|
|
6
|
+
message: string;
|
|
7
|
+
file: string;
|
|
8
|
+
line: number;
|
|
9
|
+
column: number;
|
|
10
|
+
snippet?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface Rule {
|
|
14
|
+
name: string;
|
|
15
|
+
description: string;
|
|
16
|
+
severity: Severity;
|
|
17
|
+
languages: string[];
|
|
18
|
+
check: (content: string, filename: string) => Finding[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface Config {
|
|
22
|
+
rules: Record<string, Severity | 'off'>;
|
|
23
|
+
ignore: string[];
|
|
24
|
+
languages: string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface AuditResult {
|
|
28
|
+
findings: Finding[];
|
|
29
|
+
filesScanned: number;
|
|
30
|
+
errorCount: number;
|
|
31
|
+
warningCount: number;
|
|
32
|
+
infoCount: number;
|
|
33
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"rootDir": "src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"sourceMap": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*"],
|
|
16
|
+
"exclude": ["node_modules", "dist"]
|
|
17
|
+
}
|