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.
@@ -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
+ }