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/src/auditor.ts ADDED
@@ -0,0 +1,226 @@
1
+ import { readFileSync, existsSync } from 'node:fs';
2
+ import { extname } from 'node:path';
3
+ import { allRules } from './rules/index.js';
4
+ import type { Finding, AuditResult, Config, Severity } from './types.js';
5
+
6
+ const EXTENSION_MAP: Record<string, string> = {
7
+ '.ts': 'typescript',
8
+ '.tsx': 'typescript',
9
+ '.js': 'javascript',
10
+ '.jsx': 'javascript',
11
+ '.mjs': 'javascript',
12
+ '.cjs': 'javascript',
13
+ '.py': 'python',
14
+ '.rb': 'ruby',
15
+ '.go': 'go',
16
+ };
17
+
18
+ const DEFAULT_CONFIG: Config = {
19
+ rules: {},
20
+ ignore: [],
21
+ languages: ['typescript', 'javascript', 'python'],
22
+ };
23
+
24
+ function getLanguage(filename: string): string | null {
25
+ const ext = extname(filename).toLowerCase();
26
+ return EXTENSION_MAP[ext] || null;
27
+ }
28
+
29
+ function shouldIgnore(finding: Finding, config: Config): boolean {
30
+ const ruleSeverity = config.rules[finding.rule];
31
+ return ruleSeverity === 'off';
32
+ }
33
+
34
+ function adjustSeverity(finding: Finding, config: Config): Finding {
35
+ const ruleSeverity = config.rules[finding.rule];
36
+ if (ruleSeverity && ruleSeverity !== 'off') {
37
+ return { ...finding, severity: ruleSeverity };
38
+ }
39
+ return finding;
40
+ }
41
+
42
+ export function auditFile(filename: string, config: Config = DEFAULT_CONFIG): Finding[] {
43
+ const language = getLanguage(filename);
44
+
45
+ if (!language) {
46
+ return [];
47
+ }
48
+
49
+ if (!config.languages.includes(language)) {
50
+ return [];
51
+ }
52
+
53
+ let content: string;
54
+ try {
55
+ content = readFileSync(filename, 'utf-8');
56
+ } catch {
57
+ return [];
58
+ }
59
+
60
+ const findings: Finding[] = [];
61
+
62
+ for (const rule of allRules) {
63
+ if (!rule.languages.includes(language)) {
64
+ continue;
65
+ }
66
+
67
+ const ruleFindings = rule.check(content, filename);
68
+
69
+ for (const finding of ruleFindings) {
70
+ if (!shouldIgnore(finding, config)) {
71
+ findings.push(adjustSeverity(finding, config));
72
+ }
73
+ }
74
+ }
75
+
76
+ return findings;
77
+ }
78
+
79
+ export function auditContent(content: string, filename: string, config: Config = DEFAULT_CONFIG): Finding[] {
80
+ const language = getLanguage(filename);
81
+
82
+ if (!language) {
83
+ return [];
84
+ }
85
+
86
+ const findings: Finding[] = [];
87
+
88
+ for (const rule of allRules) {
89
+ if (!rule.languages.includes(language)) {
90
+ continue;
91
+ }
92
+
93
+ const ruleFindings = rule.check(content, filename);
94
+
95
+ for (const finding of ruleFindings) {
96
+ if (!shouldIgnore(finding, config)) {
97
+ findings.push(adjustSeverity(finding, config));
98
+ }
99
+ }
100
+ }
101
+
102
+ return findings;
103
+ }
104
+
105
+ export function auditDiff(diff: string, config: Config = DEFAULT_CONFIG): Finding[] {
106
+ const findings: Finding[] = [];
107
+ let currentFile = '';
108
+ let lineOffset = 0;
109
+ const addedLines: Map<string, { line: number; content: string }[]> = new Map();
110
+
111
+ // Parse the diff
112
+ const lines = diff.split('\n');
113
+
114
+ for (const line of lines) {
115
+ // New file
116
+ if (line.startsWith('+++ b/')) {
117
+ currentFile = line.slice(6);
118
+ addedLines.set(currentFile, []);
119
+ continue;
120
+ }
121
+
122
+ // Hunk header
123
+ if (line.startsWith('@@')) {
124
+ const match = line.match(/@@ -\d+(?:,\d+)? \+(\d+)/);
125
+ if (match) {
126
+ lineOffset = parseInt(match[1], 10) - 1;
127
+ }
128
+ continue;
129
+ }
130
+
131
+ // Added line
132
+ if (line.startsWith('+') && !line.startsWith('+++')) {
133
+ lineOffset++;
134
+ const fileLines = addedLines.get(currentFile);
135
+ if (fileLines) {
136
+ fileLines.push({
137
+ line: lineOffset,
138
+ content: line.slice(1),
139
+ });
140
+ }
141
+ continue;
142
+ }
143
+
144
+ // Context or removed line
145
+ if (!line.startsWith('-')) {
146
+ lineOffset++;
147
+ }
148
+ }
149
+
150
+ // Audit added lines for each file
151
+ for (const [filename, lines] of addedLines) {
152
+ if (lines.length === 0) continue;
153
+
154
+ const content = lines.map((l) => l.content).join('\n');
155
+ const contentFindings = auditContent(content, filename, config);
156
+
157
+ // Adjust line numbers back to original file
158
+ for (const finding of contentFindings) {
159
+ const originalLine = lines[finding.line - 1];
160
+ if (originalLine) {
161
+ findings.push({
162
+ ...finding,
163
+ line: originalLine.line,
164
+ });
165
+ }
166
+ }
167
+ }
168
+
169
+ return findings;
170
+ }
171
+
172
+ export function summarize(findings: Finding[]): AuditResult {
173
+ let errorCount = 0;
174
+ let warningCount = 0;
175
+ let infoCount = 0;
176
+
177
+ const files = new Set<string>();
178
+
179
+ for (const finding of findings) {
180
+ files.add(finding.file);
181
+
182
+ switch (finding.severity) {
183
+ case 'error':
184
+ errorCount++;
185
+ break;
186
+ case 'warning':
187
+ warningCount++;
188
+ break;
189
+ case 'info':
190
+ infoCount++;
191
+ break;
192
+ }
193
+ }
194
+
195
+ return {
196
+ findings,
197
+ filesScanned: files.size,
198
+ errorCount,
199
+ warningCount,
200
+ infoCount,
201
+ };
202
+ }
203
+
204
+ export function loadConfig(configPath?: string): Config {
205
+ const paths = configPath
206
+ ? [configPath]
207
+ : ['.ai-code-audit.json', '.ai-code-audit.config.json'];
208
+
209
+ for (const path of paths) {
210
+ if (existsSync(path)) {
211
+ try {
212
+ const content = readFileSync(path, 'utf-8');
213
+ const userConfig = JSON.parse(content);
214
+ return {
215
+ ...DEFAULT_CONFIG,
216
+ ...userConfig,
217
+ rules: { ...DEFAULT_CONFIG.rules, ...userConfig.rules },
218
+ };
219
+ } catch {
220
+ // Invalid config, use defaults
221
+ }
222
+ }
223
+ }
224
+
225
+ return DEFAULT_CONFIG;
226
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,274 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync } from 'node:fs';
4
+ import { resolve } from 'node:path';
5
+ import { auditFile, auditDiff, summarize, loadConfig } from './auditor.js';
6
+ import type { Finding, AuditResult, Severity } from './types.js';
7
+
8
+ const VERSION = '0.1.0';
9
+
10
+ const COLORS = {
11
+ reset: '\x1b[0m',
12
+ bold: '\x1b[1m',
13
+ dim: '\x1b[2m',
14
+ red: '\x1b[31m',
15
+ yellow: '\x1b[33m',
16
+ blue: '\x1b[34m',
17
+ cyan: '\x1b[36m',
18
+ gray: '\x1b[90m',
19
+ };
20
+
21
+ const SEVERITY_COLORS: Record<Severity, string> = {
22
+ error: COLORS.red,
23
+ warning: COLORS.yellow,
24
+ info: COLORS.blue,
25
+ };
26
+
27
+ const HELP = `
28
+ ${COLORS.bold}ai-code-audit${COLORS.reset} v${VERSION}
29
+ Security and quality linter for AI-generated code
30
+
31
+ ${COLORS.bold}USAGE:${COLORS.reset}
32
+ aca [options] <files...>
33
+ git diff | aca --stdin
34
+
35
+ ${COLORS.bold}OPTIONS:${COLORS.reset}
36
+ -s, --stdin Read diff from stdin
37
+ -f, --format <type> Output format: text, json, sarif (default: text)
38
+ -c, --config <file> Config file path
39
+ --severity <level> Minimum severity: info, warning, error (default: warning)
40
+ -q, --quiet Only output errors
41
+ -h, --help Show help
42
+ -v, --version Show version
43
+
44
+ ${COLORS.bold}EXAMPLES:${COLORS.reset}
45
+ aca src/api.ts # Audit a single file
46
+ aca src/**/*.ts # Audit multiple files
47
+ git diff HEAD~1 | aca --stdin # Audit a diff
48
+ git diff --cached | aca --stdin # Audit staged changes
49
+
50
+ ${COLORS.bold}GIT HOOK:${COLORS.reset}
51
+ Add to .git/hooks/pre-commit:
52
+ git diff --cached | aca --stdin --severity error
53
+
54
+ `;
55
+
56
+ interface Options {
57
+ stdin: boolean;
58
+ format: 'text' | 'json' | 'sarif';
59
+ configPath?: string;
60
+ severity: Severity;
61
+ quiet: boolean;
62
+ files: string[];
63
+ }
64
+
65
+ function parseArgs(args: string[]): Options {
66
+ const options: Options = {
67
+ stdin: false,
68
+ format: 'text',
69
+ severity: 'warning',
70
+ quiet: false,
71
+ files: [],
72
+ };
73
+
74
+ for (let i = 0; i < args.length; i++) {
75
+ const arg = args[i];
76
+
77
+ switch (arg) {
78
+ case '-h':
79
+ case '--help':
80
+ console.log(HELP);
81
+ process.exit(0);
82
+ break;
83
+
84
+ case '-v':
85
+ case '--version':
86
+ console.log(`ai-code-audit v${VERSION}`);
87
+ process.exit(0);
88
+ break;
89
+
90
+ case '-s':
91
+ case '--stdin':
92
+ options.stdin = true;
93
+ break;
94
+
95
+ case '-f':
96
+ case '--format':
97
+ options.format = args[++i] as 'text' | 'json' | 'sarif';
98
+ break;
99
+
100
+ case '-c':
101
+ case '--config':
102
+ options.configPath = args[++i];
103
+ break;
104
+
105
+ case '--severity':
106
+ options.severity = args[++i] as Severity;
107
+ break;
108
+
109
+ case '-q':
110
+ case '--quiet':
111
+ options.quiet = true;
112
+ options.severity = 'error';
113
+ break;
114
+
115
+ default:
116
+ if (!arg.startsWith('-')) {
117
+ options.files.push(arg);
118
+ }
119
+ break;
120
+ }
121
+ }
122
+
123
+ return options;
124
+ }
125
+
126
+ function filterBySeverity(findings: Finding[], minSeverity: Severity): Finding[] {
127
+ const severityOrder: Severity[] = ['info', 'warning', 'error'];
128
+ const minIndex = severityOrder.indexOf(minSeverity);
129
+
130
+ return findings.filter((f) => severityOrder.indexOf(f.severity) >= minIndex);
131
+ }
132
+
133
+ function formatText(result: AuditResult): string {
134
+ const output: string[] = [];
135
+
136
+ // Group findings by file
137
+ const byFile = new Map<string, Finding[]>();
138
+ for (const finding of result.findings) {
139
+ const existing = byFile.get(finding.file) || [];
140
+ existing.push(finding);
141
+ byFile.set(finding.file, existing);
142
+ }
143
+
144
+ for (const [file, findings] of byFile) {
145
+ output.push(`\n${COLORS.bold}${file}${COLORS.reset}`);
146
+
147
+ for (const f of findings) {
148
+ const color = SEVERITY_COLORS[f.severity];
149
+ const loc = `${f.line}:${f.column}`.padEnd(8);
150
+ const sev = f.severity.padEnd(8);
151
+ output.push(
152
+ ` ${COLORS.gray}${loc}${COLORS.reset} ${color}${sev}${COLORS.reset} ${f.message} ${COLORS.dim}${f.rule}${COLORS.reset}`
153
+ );
154
+ }
155
+ }
156
+
157
+ output.push('');
158
+
159
+ const { errorCount, warningCount, infoCount } = result;
160
+ const total = errorCount + warningCount + infoCount;
161
+
162
+ if (total === 0) {
163
+ output.push(`${COLORS.cyan}✓ No issues found${COLORS.reset}`);
164
+ } else {
165
+ const parts = [];
166
+ if (errorCount > 0) parts.push(`${COLORS.red}${errorCount} error${errorCount !== 1 ? 's' : ''}${COLORS.reset}`);
167
+ if (warningCount > 0) parts.push(`${COLORS.yellow}${warningCount} warning${warningCount !== 1 ? 's' : ''}${COLORS.reset}`);
168
+ if (infoCount > 0) parts.push(`${COLORS.blue}${infoCount} info${COLORS.reset}`);
169
+ output.push(parts.join(', '));
170
+ }
171
+
172
+ return output.join('\n');
173
+ }
174
+
175
+ function formatJson(result: AuditResult): string {
176
+ return JSON.stringify(result, null, 2);
177
+ }
178
+
179
+ function formatSarif(result: AuditResult): string {
180
+ const sarif = {
181
+ $schema: 'https://json.schemastore.org/sarif-2.1.0.json',
182
+ version: '2.1.0',
183
+ runs: [
184
+ {
185
+ tool: {
186
+ driver: {
187
+ name: 'ai-code-audit',
188
+ version: VERSION,
189
+ rules: result.findings.map((f) => ({
190
+ id: f.rule,
191
+ shortDescription: { text: f.message },
192
+ })),
193
+ },
194
+ },
195
+ results: result.findings.map((f) => ({
196
+ ruleId: f.rule,
197
+ level: f.severity === 'error' ? 'error' : f.severity === 'warning' ? 'warning' : 'note',
198
+ message: { text: f.message },
199
+ locations: [
200
+ {
201
+ physicalLocation: {
202
+ artifactLocation: { uri: f.file },
203
+ region: { startLine: f.line, startColumn: f.column },
204
+ },
205
+ },
206
+ ],
207
+ })),
208
+ },
209
+ ],
210
+ };
211
+
212
+ return JSON.stringify(sarif, null, 2);
213
+ }
214
+
215
+ async function main(): Promise<void> {
216
+ const args = process.argv.slice(2);
217
+
218
+ if (args.length === 0) {
219
+ console.log(HELP);
220
+ process.exit(0);
221
+ }
222
+
223
+ const options = parseArgs(args);
224
+ const config = loadConfig(options.configPath);
225
+
226
+ let findings: Finding[] = [];
227
+
228
+ if (options.stdin) {
229
+ // Read diff from stdin
230
+ const chunks: Buffer[] = [];
231
+ for await (const chunk of process.stdin) {
232
+ chunks.push(chunk);
233
+ }
234
+ const diff = Buffer.concat(chunks).toString('utf-8');
235
+ findings = auditDiff(diff, config);
236
+ } else if (options.files.length > 0) {
237
+ // Audit files
238
+ for (const file of options.files) {
239
+ const filePath = resolve(file);
240
+ findings.push(...auditFile(filePath, config));
241
+ }
242
+ } else {
243
+ console.error('Error: No files specified. Use --stdin for diff input.');
244
+ process.exit(1);
245
+ }
246
+
247
+ // Filter by severity
248
+ findings = filterBySeverity(findings, options.severity);
249
+
250
+ const result = summarize(findings);
251
+
252
+ // Output
253
+ switch (options.format) {
254
+ case 'json':
255
+ console.log(formatJson(result));
256
+ break;
257
+ case 'sarif':
258
+ console.log(formatSarif(result));
259
+ break;
260
+ default:
261
+ console.log(formatText(result));
262
+ break;
263
+ }
264
+
265
+ // Exit with error if there are errors
266
+ if (result.errorCount > 0) {
267
+ process.exit(1);
268
+ }
269
+ }
270
+
271
+ main().catch((err) => {
272
+ console.error('Fatal error:', err);
273
+ process.exit(1);
274
+ });
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ // ai-code-audit - Security and quality linter for AI-generated code
2
+
3
+ export { auditFile, auditContent, auditDiff, summarize, loadConfig } from './auditor.js';
4
+ export { allRules, securityRules, qualityRules, aiSpecificRules } from './rules/index.js';
5
+ export type { Finding, Rule, AuditResult, Config, Severity } from './types.js';
@@ -0,0 +1,170 @@
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 aiPlaceholder: Rule = {
39
+ name: 'ai-placeholder',
40
+ description: 'Detects placeholder text left by AI',
41
+ severity: 'error',
42
+ languages: ['javascript', 'typescript', 'python', 'ruby', 'go'],
43
+ check: (content, filename) => {
44
+ const patterns = [
45
+ /\/\/\s*(?:Add|TODO:?\s*add|Implement)\s+(?:your|the)\s+/gi,
46
+ /\/\/\s*(?:Replace|Update)\s+(?:this|with)\s+/gi,
47
+ /#\s*(?:Add|TODO:?\s*add|Implement)\s+(?:your|the)\s+/gi,
48
+ /\/\/\s*\.\.\.\s*(?:rest|more|additional|other)/gi,
49
+ /\/\*\s*(?:Add|Implement|Your)\s+.*\s+here\s*\*\//gi,
50
+ /['"](?:your-api-key|YOUR_API_KEY|api-key-here|replace-me|CHANGE_ME)['"]/gi,
51
+ /(?:example|placeholder|dummy)[@.](?:com|org|io)/gi,
52
+ ];
53
+
54
+ const findings: Finding[] = [];
55
+ for (const pattern of patterns) {
56
+ findings.push(
57
+ ...findMatches(
58
+ content,
59
+ pattern,
60
+ 'ai-placeholder',
61
+ 'error',
62
+ 'AI placeholder text detected - needs implementation',
63
+ filename
64
+ )
65
+ );
66
+ }
67
+ return findings;
68
+ },
69
+ };
70
+
71
+ export const incompleteImpl: Rule = {
72
+ name: 'incomplete-impl',
73
+ description: 'Detects incomplete implementations',
74
+ severity: 'warning',
75
+ languages: ['javascript', 'typescript', 'python'],
76
+ check: (content, filename) => {
77
+ const patterns = [
78
+ /throw\s+new\s+Error\s*\(\s*['"](?:Not\s+implemented|TODO|FIXME)['"]/gi,
79
+ /raise\s+NotImplementedError/gi,
80
+ /pass\s*#\s*(?:TODO|FIXME)/gi,
81
+ /return\s+(?:null|undefined|None)\s*;?\s*\/\/\s*(?:TODO|FIXME)/gi,
82
+ ];
83
+
84
+ const findings: Finding[] = [];
85
+ for (const pattern of patterns) {
86
+ findings.push(
87
+ ...findMatches(
88
+ content,
89
+ pattern,
90
+ 'incomplete-impl',
91
+ 'warning',
92
+ 'Incomplete implementation detected',
93
+ filename
94
+ )
95
+ );
96
+ }
97
+ return findings;
98
+ },
99
+ };
100
+
101
+ export const typeAnyAbuse: Rule = {
102
+ name: 'type-any-abuse',
103
+ description: 'Detects excessive use of any type',
104
+ severity: 'warning',
105
+ languages: ['typescript'],
106
+ check: (content, filename) => {
107
+ if (!filename.endsWith('.ts') && !filename.endsWith('.tsx')) {
108
+ return [];
109
+ }
110
+
111
+ const pattern = /:\s*any\b/g;
112
+ const findings = findMatches(
113
+ content,
114
+ pattern,
115
+ 'type-any-abuse',
116
+ 'warning',
117
+ 'Unsafe "any" type usage - consider using a specific type',
118
+ filename
119
+ );
120
+
121
+ // Only report if there are many instances
122
+ if (findings.length > 3) {
123
+ return findings;
124
+ }
125
+ return [];
126
+ },
127
+ };
128
+
129
+ export const excessiveComments: Rule = {
130
+ name: 'excessive-comments',
131
+ description: 'Detects over-commented obvious code',
132
+ severity: 'info',
133
+ languages: ['javascript', 'typescript'],
134
+ check: (content, filename) => {
135
+ const findings: Finding[] = [];
136
+ const lines = content.split('\n');
137
+ let commentLines = 0;
138
+ let codeLines = 0;
139
+
140
+ for (const line of lines) {
141
+ const trimmed = line.trim();
142
+ if (trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*')) {
143
+ commentLines++;
144
+ } else if (trimmed.length > 0) {
145
+ codeLines++;
146
+ }
147
+ }
148
+
149
+ // If comments exceed 50% of code, flag it
150
+ if (codeLines > 10 && commentLines > codeLines * 0.5) {
151
+ findings.push({
152
+ rule: 'excessive-comments',
153
+ severity: 'info',
154
+ message: `High comment ratio (${commentLines} comments / ${codeLines} code lines) - may be over-documented`,
155
+ file: filename,
156
+ line: 1,
157
+ column: 1,
158
+ });
159
+ }
160
+
161
+ return findings;
162
+ },
163
+ };
164
+
165
+ export const aiSpecificRules: Rule[] = [
166
+ aiPlaceholder,
167
+ incompleteImpl,
168
+ typeAnyAbuse,
169
+ excessiveComments,
170
+ ];
@@ -0,0 +1,12 @@
1
+ import { securityRules } from './security.js';
2
+ import { qualityRules } from './quality.js';
3
+ import { aiSpecificRules } from './ai-specific.js';
4
+ import type { Rule } from '../types.js';
5
+
6
+ export const allRules: Rule[] = [
7
+ ...securityRules,
8
+ ...qualityRules,
9
+ ...aiSpecificRules,
10
+ ];
11
+
12
+ export { securityRules, qualityRules, aiSpecificRules };