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
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 };
|