Initial commit: ai-code-trust CLI tool v1.0.0
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.venv/
|
||||
*.log
|
||||
.verdict
|
||||
Generated
+51
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "ai-code-trust",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ai-code-trust",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "^25.9.3"
|
||||
},
|
||||
"bin": {
|
||||
"ai-code-trust": "dist/index.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "25.9.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.3.tgz",
|
||||
"integrity": "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": ">=7.24.0 <7.24.7"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.24.6",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
|
||||
"integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "ai-code-trust",
|
||||
"version": "1.0.0",
|
||||
"description": "CLI tool to audit and assess trust in AI-generated code",
|
||||
"main": "dist/index.js",
|
||||
"bin": {
|
||||
"ai-code-trust": "dist/index.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"test": "node --test dist/**/*.test.js",
|
||||
"lint": "tsc --noEmit"
|
||||
},
|
||||
"keywords": [
|
||||
"ai",
|
||||
"code",
|
||||
"trust",
|
||||
"audit",
|
||||
"security"
|
||||
],
|
||||
"author": "BunBun Labs",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"typescript": "^5.4.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": "^25.9.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
import { detectPatterns, getSeverityCounts, PatternMatch } from './detectors.js';
|
||||
import { analyzeFileContent, analyzeDirectory } from './analyzer.js';
|
||||
|
||||
test('detectPatterns finds AI comment patterns', () => {
|
||||
const code = `
|
||||
// This function handles the main logic
|
||||
function main() {
|
||||
return 42;
|
||||
}
|
||||
`;
|
||||
const result = detectPatterns(code);
|
||||
assert(result.patterns.length > 0, 'Should detect AI comment pattern');
|
||||
assert(result.patterns.some(p => p.description.includes('AI comment')));
|
||||
});
|
||||
|
||||
test('detectPatterns finds critical SQL injection', () => {
|
||||
const code = `
|
||||
const query = "SELECT * FROM users WHERE id = " + userId;
|
||||
db.execute(query);
|
||||
`;
|
||||
const result = detectPatterns(code);
|
||||
assert(result.patterns.some(p => p.severity === 'critical' && p.description.includes('SQL injection')));
|
||||
});
|
||||
|
||||
test('detectPatterns finds hardcoded credentials', () => {
|
||||
const code = `const apiKey = "sk-1234567890abcdef";
|
||||
const password = "secret123";
|
||||
`;
|
||||
const result = detectPatterns(code);
|
||||
assert(result.patterns.some(p => p.severity === 'critical' && p.description.includes('credential')));
|
||||
});
|
||||
|
||||
test('detectPatterns finds eval usage', () => {
|
||||
const code = `eval(userInput);`;
|
||||
const result = detectPatterns(code);
|
||||
assert(result.patterns.some(p => p.severity === 'critical' && p.description.includes('eval')));
|
||||
});
|
||||
|
||||
test('analyzeFileContent analyzes a single file', () => {
|
||||
const code = `const password = "hardcoded123";
|
||||
// This function handles authentication
|
||||
function auth() {
|
||||
return true;
|
||||
}
|
||||
`;
|
||||
const result = analyzeFileContent('test.js', code);
|
||||
assert(result !== null);
|
||||
assert(result!.trustScore < 100); // should have deductions
|
||||
assert(result!.detection.patterns.length > 0);
|
||||
});
|
||||
|
||||
test('analyzeDirectory returns summary', () => {
|
||||
// This test runs against the actual src directory
|
||||
const summary = analyzeDirectory('./src');
|
||||
assert(summary.scannedFiles > 0);
|
||||
assert(summary.totalLines > 0);
|
||||
assert(summary.averageTrustScore >= 0 && summary.averageTrustScore <= 100);
|
||||
assert(['low', 'medium', 'high', 'critical'].includes(summary.riskLevel));
|
||||
});
|
||||
|
||||
test('getSeverityCounts aggregates correctly', () => {
|
||||
const patterns: PatternMatch[] = [
|
||||
{ severity: 'low', line: 1, description: 'x', pattern: 'x' },
|
||||
{ severity: 'low', line: 2, description: 'x', pattern: 'x' },
|
||||
{ severity: 'high', line: 3, description: 'x', pattern: 'x' },
|
||||
];
|
||||
const counts = getSeverityCounts(patterns);
|
||||
assert.strictEqual(counts.low, 2);
|
||||
assert.strictEqual(counts.medium, 0);
|
||||
assert.strictEqual(counts.high, 1);
|
||||
assert.strictEqual(counts.critical, 0);
|
||||
});
|
||||
|
||||
test('clean code gets high trust score', () => {
|
||||
const code = `
|
||||
function add(a, b) {
|
||||
return a + b;
|
||||
}
|
||||
`;
|
||||
const result = detectPatterns(code);
|
||||
assert(result.confidence < 0.5); // low confidence of AI generation
|
||||
const file = analyzeFileContent('clean.js', code);
|
||||
assert(file !== null);
|
||||
assert(file!.trustScore >= 90, `Expected >= 90, got ${file!.trustScore}`);
|
||||
});
|
||||
|
||||
test('trust score penalizes multiple issues', () => {
|
||||
const code = `
|
||||
// This function handles user authentication
|
||||
function login(userId, password) {
|
||||
const query = "SELECT * FROM users WHERE id = " + userId;
|
||||
eval(password);
|
||||
return query;
|
||||
}
|
||||
`;
|
||||
const file = analyzeFileContent('bad.js', code);
|
||||
assert(file !== null);
|
||||
assert(file!.trustScore < 50, `Expected < 50, got ${file!.trustScore}`);
|
||||
assert(file!.detection.patterns.some(p => p.severity === 'critical'));
|
||||
});
|
||||
+208
@@ -0,0 +1,208 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { detectPatterns, getSeverityCounts, DetectionResult } from './detectors.js';
|
||||
|
||||
export interface FileResult {
|
||||
file: string;
|
||||
lines: number;
|
||||
detection: DetectionResult;
|
||||
trustScore: number; // 0-100, higher = more trustworthy
|
||||
}
|
||||
|
||||
export interface AnalysisConfig {
|
||||
include: string[];
|
||||
exclude: string[];
|
||||
maxSize: number; // max file size in bytes
|
||||
minScore: number; // minimum trust score threshold
|
||||
}
|
||||
|
||||
export interface AnalysisSummary {
|
||||
totalFiles: number;
|
||||
scannedFiles: number;
|
||||
totalLines: number;
|
||||
issuesFound: number;
|
||||
criticalIssues: number;
|
||||
highIssues: number;
|
||||
mediumIssues: number;
|
||||
lowIssues: number;
|
||||
averageTrustScore: number;
|
||||
filesWithIssues: number;
|
||||
cleanFiles: number;
|
||||
riskLevel: 'low' | 'medium' | 'high' | 'critical';
|
||||
fileResults: FileResult[];
|
||||
}
|
||||
|
||||
const DEFAULT_CONFIG: AnalysisConfig = {
|
||||
include: ['.js', '.ts', '.jsx', '.tsx', '.py', '.java', '.go', '.rb', '.php', '.cs', '.cpp', '.c', '.swift', '.kt', '.rs'],
|
||||
exclude: ['node_modules', 'dist', '.git', 'vendor', 'build', 'coverage', '.next', 'out', '__pycache__', '.venv', 'venv'],
|
||||
maxSize: 1024 * 1024, // 1MB
|
||||
minScore: 50
|
||||
};
|
||||
|
||||
function shouldScan(filePath: string, config: AnalysisConfig): boolean {
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
if (!config.include.includes(ext)) return false;
|
||||
|
||||
const parts = filePath.split(path.sep);
|
||||
for (const ex of config.exclude) {
|
||||
if (parts.some(part => part === ex)) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function calculateTrustScore(detection: DetectionResult, lines: number): number {
|
||||
const counts = getSeverityCounts(detection.patterns);
|
||||
|
||||
// Base score: 100
|
||||
let score = 100;
|
||||
|
||||
// Deduct for each severity level
|
||||
score -= counts.critical * 25;
|
||||
score -= counts.high * 15;
|
||||
score -= counts.medium * 8;
|
||||
score -= counts.low * 2;
|
||||
|
||||
// Deduct for AI pattern density (patterns per 100 lines)
|
||||
const density = (detection.patterns.length / Math.max(lines, 1)) * 100;
|
||||
score -= Math.min(density * 3, 30); // cap at 30 points
|
||||
|
||||
// Boost for very clean files
|
||||
if (detection.patterns.length === 0) {
|
||||
score += 5; // bonus for clean file
|
||||
}
|
||||
|
||||
// Clamp to 0-100
|
||||
return Math.max(0, Math.min(100, Math.round(score)));
|
||||
}
|
||||
|
||||
function analyzeFile(filePath: string, basePath: string): FileResult | null {
|
||||
try {
|
||||
const stat = fs.statSync(filePath);
|
||||
if (stat.size > DEFAULT_CONFIG.maxSize) {
|
||||
return null; // skip large files
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
const lines = content.split('\n').length;
|
||||
const detection = detectPatterns(content);
|
||||
const trustScore = calculateTrustScore(detection, lines);
|
||||
|
||||
return {
|
||||
file: path.relative(basePath, filePath),
|
||||
lines,
|
||||
detection,
|
||||
trustScore
|
||||
};
|
||||
} catch (err) {
|
||||
return null; // skip unreadable files
|
||||
}
|
||||
}
|
||||
|
||||
export function analyzeDirectory(dirPath: string, config: Partial<AnalysisConfig> = {}): AnalysisSummary {
|
||||
const fullConfig = { ...DEFAULT_CONFIG, ...config };
|
||||
const results: FileResult[] = [];
|
||||
let totalFiles = 0;
|
||||
let totalLines = 0;
|
||||
|
||||
function scanDir(dir: string) {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
let skip = false;
|
||||
for (const ex of fullConfig.exclude) {
|
||||
if (entry.name === ex) {
|
||||
skip = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!skip) {
|
||||
scanDir(fullPath);
|
||||
}
|
||||
} else if (entry.isFile()) {
|
||||
totalFiles++;
|
||||
if (shouldScan(fullPath, fullConfig)) {
|
||||
const result = analyzeFile(fullPath, dirPath);
|
||||
if (result) {
|
||||
results.push(result);
|
||||
totalLines += result.lines;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scanDir(dirPath);
|
||||
|
||||
// Calculate summary
|
||||
let issuesFound = 0;
|
||||
let criticalIssues = 0;
|
||||
let highIssues = 0;
|
||||
let mediumIssues = 0;
|
||||
let lowIssues = 0;
|
||||
let filesWithIssues = 0;
|
||||
let totalTrustScore = 0;
|
||||
|
||||
for (const result of results) {
|
||||
const counts = getSeverityCounts(result.detection.patterns);
|
||||
issuesFound += result.detection.patterns.length;
|
||||
criticalIssues += counts.critical || 0;
|
||||
highIssues += counts.high || 0;
|
||||
mediumIssues += counts.medium || 0;
|
||||
lowIssues += counts.low || 0;
|
||||
|
||||
if (result.detection.patterns.length > 0) {
|
||||
filesWithIssues++;
|
||||
}
|
||||
totalTrustScore += result.trustScore;
|
||||
}
|
||||
|
||||
const averageTrustScore = results.length > 0 ? Math.round(totalTrustScore / results.length) : 100;
|
||||
|
||||
// Determine risk level
|
||||
let riskLevel: 'low' | 'medium' | 'high' | 'critical' = 'low';
|
||||
if (criticalIssues > 0) {
|
||||
riskLevel = 'critical';
|
||||
} else if (highIssues > 0 || averageTrustScore < 50) {
|
||||
riskLevel = 'high';
|
||||
} else if (mediumIssues > 0 || averageTrustScore < 70) {
|
||||
riskLevel = 'medium';
|
||||
}
|
||||
|
||||
return {
|
||||
totalFiles,
|
||||
scannedFiles: results.length,
|
||||
totalLines,
|
||||
issuesFound,
|
||||
criticalIssues,
|
||||
highIssues,
|
||||
mediumIssues,
|
||||
lowIssues,
|
||||
averageTrustScore,
|
||||
filesWithIssues,
|
||||
cleanFiles: results.length - filesWithIssues,
|
||||
riskLevel,
|
||||
fileResults: results.sort((a, b) => a.trustScore - b.trustScore) // sort by trust score ascending
|
||||
};
|
||||
}
|
||||
|
||||
export function analyzeFileContent(filePath: string, content?: string): FileResult | null {
|
||||
try {
|
||||
const actualContent = content || fs.readFileSync(filePath, 'utf-8');
|
||||
const lines = actualContent.split('\n').length;
|
||||
const detection = detectPatterns(actualContent);
|
||||
const trustScore = calculateTrustScore(detection, lines);
|
||||
|
||||
return {
|
||||
file: path.basename(filePath),
|
||||
lines,
|
||||
detection,
|
||||
trustScore
|
||||
};
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
// AI-generated code pattern detectors
|
||||
// Heuristic-based detection of common AI coding tool patterns
|
||||
|
||||
export interface PatternMatch {
|
||||
pattern: string;
|
||||
severity: 'low' | 'medium' | 'high' | 'critical';
|
||||
description: string;
|
||||
line: number;
|
||||
}
|
||||
|
||||
export interface DetectionResult {
|
||||
confidence: number; // 0-1
|
||||
patterns: PatternMatch[];
|
||||
}
|
||||
|
||||
// Known AI-generated code signatures
|
||||
const AI_PATTERNS = [
|
||||
// Overly verbose comments explaining obvious code
|
||||
{
|
||||
regex: /\/\/\s*(This function|This method|This class|This variable|This code|This loop|This block)/i,
|
||||
severity: 'low' as const,
|
||||
description: 'Boilerplate AI comment pattern'
|
||||
},
|
||||
// Excessive inline documentation
|
||||
{
|
||||
regex: /\/\/.\s*(defines|creates|initializes|sets up|handles|manages|processes|implements)/i,
|
||||
severity: 'low' as const,
|
||||
description: 'Explanatory comment pattern typical of AI output'
|
||||
},
|
||||
// Generic error handling without context
|
||||
{
|
||||
regex: /catch\s*\([^)]*\)\s*\{\s*\n?\s*(console\.error|console\.log|print)\s*\(/i,
|
||||
severity: 'medium' as const,
|
||||
description: 'Generic catch-all error logging without meaningful handling'
|
||||
},
|
||||
// TODO/FIXME comments from incomplete generation
|
||||
{
|
||||
regex: /(TODO|FIXME|HACK|XXX|NOTE|REVIEW)\s*:/i,
|
||||
severity: 'medium' as const,
|
||||
description: 'Incomplete implementation marker'
|
||||
},
|
||||
// Suspicious variable names
|
||||
{
|
||||
regex: /\b(data|result|output|input|value|item|temp|tmp|obj|arr|str|num|val|func|cb)\d*\b/g,
|
||||
severity: 'low' as const,
|
||||
description: 'Generic variable naming pattern'
|
||||
},
|
||||
// Overly defensive null checks in sequence
|
||||
{
|
||||
regex: /if\s*\(\s*[^)]+\s*!==?\s*(null|undefined|None)\s*\)\s*\{[^}]*\}\s*(?:else\s*\{[^}]*\})?/g,
|
||||
severity: 'low' as const,
|
||||
description: 'Defensive null checking pattern'
|
||||
},
|
||||
// Copy-paste license headers without context
|
||||
{
|
||||
regex: /MIT License|Apache License|BSD License|GNU General Public License/i,
|
||||
severity: 'low' as const,
|
||||
description: 'License header (may be auto-inserted)'
|
||||
},
|
||||
// JSDoc with AI-style verbose descriptions
|
||||
{
|
||||
regex: /\/\*\*\s*\n\s*\*\s*(This is|This function|A utility|Helper function|Main function)/i,
|
||||
severity: 'low' as const,
|
||||
description: 'Verbose JSDoc pattern'
|
||||
},
|
||||
// Suspicious date handling
|
||||
{
|
||||
regex: /new\s+Date\s*\(\s*\)\s*\.[a-zA-Z]+\s*\(/i,
|
||||
severity: 'low' as const,
|
||||
description: 'Date manipulation without timezone consideration'
|
||||
},
|
||||
// JSON.parse without try-catch
|
||||
{
|
||||
regex: /JSON\.parse\s*\([^)]+\)\s*(?!\s*catch)/i,
|
||||
severity: 'high' as const,
|
||||
description: 'JSON.parse without error handling — common AI-generated security risk'
|
||||
},
|
||||
// eval or Function constructor usage
|
||||
{
|
||||
regex: /\beval\s*\(|new\s+Function\s*\(/i,
|
||||
severity: 'critical' as const,
|
||||
description: 'Dangerous eval/Function constructor usage'
|
||||
},
|
||||
// SQL string concatenation
|
||||
{
|
||||
regex: /(?:SELECT|INSERT|UPDATE|DELETE).*(?:\+|\$\{|`[^`]*\$\{)/i,
|
||||
severity: 'critical' as const,
|
||||
description: 'Potential SQL injection via string concatenation'
|
||||
},
|
||||
// Hardcoded credentials
|
||||
{
|
||||
regex: /(?:password|secret|token|api_key|apikey|access_token)\s*[:=]\s*["'][^"']{4,}["']/i,
|
||||
severity: 'critical' as const,
|
||||
description: 'Hardcoded credential detected'
|
||||
},
|
||||
// Insecure randomness
|
||||
{
|
||||
regex: /Math\.random\s*\(\s*\)/i,
|
||||
severity: 'medium' as const,
|
||||
description: 'Math.random() used for security-sensitive operation'
|
||||
},
|
||||
// Prototype pollution
|
||||
{
|
||||
regex: /Object\.prototype\.\w+\s*=/i,
|
||||
severity: 'high' as const,
|
||||
description: 'Prototype modification — potential prototype pollution'
|
||||
},
|
||||
// Disabled security (unsafe-inline, etc.)
|
||||
{
|
||||
regex: /unsafe-inline|unsafe-eval|'unsafe-inline'|'unsafe-eval'/i,
|
||||
severity: 'high' as const,
|
||||
description: 'Disabled security policy in configuration'
|
||||
},
|
||||
// Clipboard access without permission check
|
||||
{
|
||||
regex: /navigator\.clipboard\.writeText|document\.execCommand\s*\(\s*['"]copy['"]/i,
|
||||
severity: 'medium' as const,
|
||||
description: 'Clipboard access without explicit permission handling'
|
||||
},
|
||||
// File path traversal
|
||||
{
|
||||
regex: /(?:readFile|writeFile|createReadStream|createWriteStream)\s*\(\s*[^)]*(?:\+|\$\{|req\.|request\.)/i,
|
||||
severity: 'high' as const,
|
||||
description: 'Potential path traversal via user input'
|
||||
},
|
||||
// Missing CSRF protection
|
||||
{
|
||||
regex: /fetch\s*\(\s*[^)]*\)\s*\{[^}]*method\s*:\s*['"]POST['"]/i,
|
||||
severity: 'medium' as const,
|
||||
description: 'POST request without visible CSRF token'
|
||||
},
|
||||
// InnerHTML with user input
|
||||
{
|
||||
regex: /\.innerHTML\s*=\s*[^;]*(?:\+|\$\{|req\.|request\.)/i,
|
||||
severity: 'high' as const,
|
||||
description: 'XSS risk: innerHTML with user-controlled input'
|
||||
},
|
||||
// Regex DoS
|
||||
{
|
||||
regex: /\([^)]*\+\+?[^)]*\)[\*\+]\+?/,
|
||||
severity: 'medium' as const,
|
||||
description: 'Potentially catastrophic regex (ReDoS risk)'
|
||||
},
|
||||
// Weak crypto
|
||||
{
|
||||
regex: /md5\s*\(|sha1\s*\(|createHash\s*\(\s*['"]md5['"]/i,
|
||||
severity: 'high' as const,
|
||||
description: 'Weak cryptographic hash function'
|
||||
},
|
||||
// Process exit in error handler
|
||||
{
|
||||
regex: /process\.exit\s*\(/i,
|
||||
severity: 'medium' as const,
|
||||
description: 'Process termination in error path'
|
||||
},
|
||||
// Unhandled promise rejection pattern
|
||||
{
|
||||
regex: /new\s+Promise\s*\([^)]*\)\s*\{[^}]*\}(?!\s*catch)/i,
|
||||
severity: 'medium' as const,
|
||||
description: 'Promise without catch or error handler'
|
||||
},
|
||||
// Arbitrary command execution
|
||||
{
|
||||
regex: /exec\s*\(|execSync\s*\(|spawn\s*\(\s*[^)]*(?:\+|\$\{|req\.|request\.)/i,
|
||||
severity: 'critical' as const,
|
||||
description: 'Command execution with user input — RCE risk'
|
||||
},
|
||||
// Disabled SSL verification
|
||||
{
|
||||
regex: /rejectUnauthorized\s*:\s*false|NODE_TLS_REJECT_UNAUTHORIZED\s*=\s*['"]0['"]/i,
|
||||
severity: 'critical' as const,
|
||||
description: 'SSL certificate verification disabled'
|
||||
},
|
||||
// Open redirect
|
||||
{
|
||||
regex: /res\.redirect\s*\(\s*[^)]*(?:\+|req\.|request\.|\$\{)/i,
|
||||
severity: 'high' as const,
|
||||
description: 'Open redirect via user input'
|
||||
},
|
||||
// Information disclosure
|
||||
{
|
||||
regex: /res\.json\s*\(\s*\{[^}]*error[^}]*\}\s*\)/i,
|
||||
severity: 'medium' as const,
|
||||
description: 'Verbose error response may leak sensitive information'
|
||||
},
|
||||
// Race condition in file operations
|
||||
{
|
||||
regex: /access\s*\([^)]+\)\s*\{[^}]*readFile|writeFile/i,
|
||||
severity: 'medium' as const,
|
||||
description: 'Time-of-check time-of-use (TOCTOU) race condition'
|
||||
},
|
||||
// Missing authorization check
|
||||
{
|
||||
regex: /app\.(get|post|put|delete|patch)\s*\([^)]+\)\s*,\s*\(?\s*req\s*,\s*res\s*\)?\s*=>/i,
|
||||
severity: 'medium' as const,
|
||||
description: 'Route handler without visible authorization middleware'
|
||||
},
|
||||
// Mass assignment
|
||||
{
|
||||
regex: /Object\.assign\s*\(\s*[^,]+,\s*req\.(body|query|params)/i,
|
||||
severity: 'high' as const,
|
||||
description: 'Mass assignment from user input'
|
||||
},
|
||||
// Timing attack vulnerability
|
||||
{
|
||||
regex: /===\s*['"][^'"]+['"]/i,
|
||||
severity: 'low' as const,
|
||||
description: 'String comparison may be vulnerable to timing attacks'
|
||||
}
|
||||
];
|
||||
|
||||
export function detectPatterns(content: string): DetectionResult {
|
||||
const patterns: PatternMatch[] = [];
|
||||
const lines = content.split('\n');
|
||||
|
||||
let aiScore = 0;
|
||||
let securityScore = 0;
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
const lineNum = i + 1;
|
||||
|
||||
for (const pattern of AI_PATTERNS) {
|
||||
const regex = new RegExp(pattern.regex.source, 'i');
|
||||
if (regex.test(line)) {
|
||||
patterns.push({
|
||||
pattern: pattern.regex.source,
|
||||
severity: pattern.severity,
|
||||
description: pattern.description,
|
||||
line: lineNum
|
||||
});
|
||||
|
||||
if (['low', 'medium'].includes(pattern.severity)) {
|
||||
aiScore += 0.05;
|
||||
} else {
|
||||
securityScore += 0.15;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize scores
|
||||
aiScore = Math.min(aiScore, 1.0);
|
||||
securityScore = Math.min(securityScore, 1.0);
|
||||
|
||||
// Combined confidence: weighted toward security issues
|
||||
const confidence = (aiScore * 0.3) + (securityScore * 0.7);
|
||||
|
||||
return {
|
||||
confidence: Math.min(confidence, 1.0),
|
||||
patterns
|
||||
};
|
||||
}
|
||||
|
||||
export function getSeverityCounts(patterns: PatternMatch[]): Record<string, number> {
|
||||
const counts: Record<string, number> = { low: 0, medium: 0, high: 0, critical: 0 };
|
||||
for (const p of patterns) {
|
||||
counts[p.severity] = (counts[p.severity] || 0) + 1;
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
#!/usr/bin/env node
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { analyzeDirectory, analyzeFileContent, AnalysisSummary } from './analyzer.js';
|
||||
import { generateConsoleReport, generateJSONReport, generateMarkdownReport } from './report.js';
|
||||
|
||||
interface CliOptions {
|
||||
target: string;
|
||||
format: 'console' | 'json' | 'markdown';
|
||||
output?: string;
|
||||
include?: string[];
|
||||
exclude?: string[];
|
||||
threshold?: number;
|
||||
help: boolean;
|
||||
version: boolean;
|
||||
}
|
||||
|
||||
function parseArgs(): CliOptions {
|
||||
const args = process.argv.slice(2);
|
||||
const options: CliOptions = {
|
||||
target: '.',
|
||||
format: 'console',
|
||||
help: false,
|
||||
version: false
|
||||
};
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
|
||||
if (arg === '--help' || arg === '-h') {
|
||||
options.help = true;
|
||||
} else if (arg === '--version' || arg === '-v') {
|
||||
options.version = true;
|
||||
} else if (arg === '--format' || arg === '-f') {
|
||||
const format = args[++i];
|
||||
if (format === 'json' || format === 'markdown' || format === 'console') {
|
||||
options.format = format;
|
||||
}
|
||||
} else if (arg === '--output' || arg === '-o') {
|
||||
options.output = args[++i];
|
||||
} else if (arg === '--include') {
|
||||
options.include = args[++i].split(',').map((s: string) => s.trim().startsWith('.') ? s.trim() : '.' + s.trim());
|
||||
} else if (arg === '--exclude') {
|
||||
options.exclude = args[++i].split(',').map((s: string) => s.trim());
|
||||
} else if (arg === '--threshold') {
|
||||
options.threshold = parseInt(args[++i], 10);
|
||||
} else if (!arg.startsWith('-')) {
|
||||
options.target = arg;
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
function showHelp(): void {
|
||||
console.log(`
|
||||
ai-code-trust — Audit and assess trust in AI-generated code
|
||||
|
||||
USAGE:
|
||||
ai-code-trust [target] [options]
|
||||
|
||||
OPTIONS:
|
||||
-h, --help Show this help message
|
||||
-v, --version Show version
|
||||
-f, --format <type> Output format: console (default), json, markdown
|
||||
-o, --output <file> Write output to file instead of stdout
|
||||
--include <exts> Comma-separated extensions to scan (default: common code types)
|
||||
--exclude <dirs> Comma-separated directories to exclude
|
||||
--threshold <score> Minimum trust score threshold (0-100)
|
||||
|
||||
EXAMPLES:
|
||||
ai-code-trust ./src
|
||||
ai-code-trust . --format json --output report.json
|
||||
ai-code-trust ./src --include .ts,.js --exclude node_modules,dist
|
||||
ai-code-trust ./src --threshold 60
|
||||
|
||||
DESCRIPTION:
|
||||
Scans code for AI-generation patterns and security vulnerabilities,
|
||||
assigns trust scores (0-100), and generates actionable reports.
|
||||
`);
|
||||
}
|
||||
|
||||
function showVersion(): void {
|
||||
console.log('ai-code-trust v1.0.0');
|
||||
}
|
||||
|
||||
function main(): void {
|
||||
const options = parseArgs();
|
||||
|
||||
if (options.help) {
|
||||
showHelp();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (options.version) {
|
||||
showVersion();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const targetPath = path.resolve(options.target);
|
||||
|
||||
if (!fs.existsSync(targetPath)) {
|
||||
console.error(`Error: Target path does not exist: ${targetPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let summary: AnalysisSummary;
|
||||
|
||||
if (fs.statSync(targetPath).isFile()) {
|
||||
const fileResult = analyzeFileContent(targetPath);
|
||||
if (!fileResult) {
|
||||
console.error('Error: Could not analyze file');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
summary = {
|
||||
totalFiles: 1,
|
||||
scannedFiles: 1,
|
||||
totalLines: fileResult.lines,
|
||||
issuesFound: fileResult.detection.patterns.length,
|
||||
criticalIssues: fileResult.detection.patterns.filter(p => p.severity === 'critical').length,
|
||||
highIssues: fileResult.detection.patterns.filter(p => p.severity === 'high').length,
|
||||
mediumIssues: fileResult.detection.patterns.filter(p => p.severity === 'medium').length,
|
||||
lowIssues: fileResult.detection.patterns.filter(p => p.severity === 'low').length,
|
||||
averageTrustScore: fileResult.trustScore,
|
||||
filesWithIssues: fileResult.detection.patterns.length > 0 ? 1 : 0,
|
||||
cleanFiles: fileResult.detection.patterns.length > 0 ? 0 : 1,
|
||||
riskLevel: fileResult.trustScore < 40 ? 'critical' : fileResult.trustScore < 60 ? 'high' : fileResult.trustScore < 80 ? 'medium' : 'low',
|
||||
fileResults: [fileResult]
|
||||
};
|
||||
} else {
|
||||
const config: { include?: string[]; exclude?: string[]; minScore?: number } = {};
|
||||
if (options.include) config.include = options.include;
|
||||
if (options.exclude) config.exclude = options.exclude;
|
||||
if (options.threshold !== undefined) config.minScore = options.threshold;
|
||||
|
||||
summary = analyzeDirectory(targetPath, config);
|
||||
}
|
||||
|
||||
let output: string;
|
||||
|
||||
switch (options.format) {
|
||||
case 'json':
|
||||
output = generateJSONReport(summary);
|
||||
break;
|
||||
case 'markdown':
|
||||
output = generateMarkdownReport(summary);
|
||||
break;
|
||||
default:
|
||||
output = generateConsoleReport(summary);
|
||||
}
|
||||
|
||||
if (options.output) {
|
||||
fs.writeFileSync(path.resolve(options.output), output, 'utf-8');
|
||||
console.log(`Report written to: ${options.output}`);
|
||||
} else {
|
||||
console.log(output);
|
||||
}
|
||||
|
||||
// Exit with non-zero if critical issues found
|
||||
if (summary.criticalIssues > 0) {
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
main();
|
||||
+233
@@ -0,0 +1,233 @@
|
||||
import { AnalysisSummary, FileResult } from './analyzer.js';
|
||||
import { PatternMatch } from './detectors.js';
|
||||
|
||||
export function generateConsoleReport(summary: AnalysisSummary): string {
|
||||
const lines: string[] = [];
|
||||
|
||||
lines.push('═'.repeat(60));
|
||||
lines.push(' AI CODE TRUST AUDIT REPORT');
|
||||
lines.push('═'.repeat(60));
|
||||
lines.push('');
|
||||
|
||||
// Risk level with color indicator
|
||||
const riskEmoji = {
|
||||
low: '✅',
|
||||
medium: '⚠️ ',
|
||||
high: '🔴',
|
||||
critical: '☠️ '
|
||||
};
|
||||
|
||||
lines.push(`OVERALL RISK LEVEL: ${riskEmoji[summary.riskLevel]} ${summary.riskLevel.toUpperCase()}`);
|
||||
lines.push(`Average Trust Score: ${summary.averageTrustScore}/100`);
|
||||
lines.push('');
|
||||
lines.push('─'.repeat(60));
|
||||
lines.push('SUMMARY');
|
||||
lines.push('─'.repeat(60));
|
||||
lines.push(` Files scanned: ${summary.scannedFiles} / ${summary.totalFiles}`);
|
||||
lines.push(` Total lines: ${summary.totalLines.toLocaleString()}`);
|
||||
lines.push(` Files with issues: ${summary.filesWithIssues}`);
|
||||
lines.push(` Clean files: ${summary.cleanFiles}`);
|
||||
lines.push(` Issues found: ${summary.issuesFound}`);
|
||||
lines.push('');
|
||||
lines.push('─'.repeat(60));
|
||||
lines.push('ISSUE BREAKDOWN');
|
||||
lines.push('─'.repeat(60));
|
||||
lines.push(` 🔴 Critical: ${summary.criticalIssues}`);
|
||||
lines.push(` 🔴 High: ${summary.highIssues}`);
|
||||
lines.push(` ⚠️ Medium: ${summary.mediumIssues}`);
|
||||
lines.push(` ℹ️ Low: ${summary.lowIssues}`);
|
||||
lines.push('');
|
||||
|
||||
// Top files by risk
|
||||
if (summary.fileResults.length > 0) {
|
||||
lines.push('─'.repeat(60));
|
||||
lines.push('FILES BY TRUST SCORE (lowest first)');
|
||||
lines.push('─'.repeat(60));
|
||||
|
||||
const topFiles = summary.fileResults.slice(0, 20); // show top 20
|
||||
for (const file of topFiles) {
|
||||
const scoreEmoji = file.trustScore >= 80 ? '✅' : file.trustScore >= 60 ? '⚠️ ' : file.trustScore >= 40 ? '🔴' : '☠️ ';
|
||||
lines.push(` ${scoreEmoji} ${file.trustScore.toString().padStart(3)} ${file.file} (${file.lines} lines, ${file.detection.patterns.length} issues)`);
|
||||
}
|
||||
|
||||
if (summary.fileResults.length > 20) {
|
||||
lines.push(` ... and ${summary.fileResults.length - 20} more files`);
|
||||
}
|
||||
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
// Detailed issue report for critical and high
|
||||
const criticalHighFiles = summary.fileResults.filter(
|
||||
f => f.detection.patterns.some(p => p.severity === 'critical' || p.severity === 'high')
|
||||
);
|
||||
|
||||
if (criticalHighFiles.length > 0) {
|
||||
lines.push('─'.repeat(60));
|
||||
lines.push('CRITICAL & HIGH SEVERITY DETAILS');
|
||||
lines.push('─'.repeat(60));
|
||||
|
||||
for (const file of criticalHighFiles) {
|
||||
const criticalIssues = file.detection.patterns.filter(p => p.severity === 'critical' || p.severity === 'high');
|
||||
if (criticalIssues.length > 0) {
|
||||
lines.push(`\n ${file.file} (trust score: ${file.trustScore})`);
|
||||
for (const issue of criticalIssues) {
|
||||
const severityEmoji = issue.severity === 'critical' ? '☠️ ' : '🔴';
|
||||
lines.push(` ${severityEmoji} [${issue.severity.toUpperCase()}] Line ${issue.line}: ${issue.description}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
// Recommendations
|
||||
lines.push('─'.repeat(60));
|
||||
lines.push('RECOMMENDATIONS');
|
||||
lines.push('─'.repeat(60));
|
||||
|
||||
if (summary.criticalIssues > 0) {
|
||||
lines.push(' ☠️ CRITICAL: Fix all critical issues immediately before deploying.');
|
||||
}
|
||||
if (summary.highIssues > 0) {
|
||||
lines.push(' 🔴 HIGH: Review and fix high-severity issues before code review.');
|
||||
}
|
||||
if (summary.mediumIssues > 0) {
|
||||
lines.push(' ⚠️ MEDIUM: Address medium issues during your next refactor cycle.');
|
||||
}
|
||||
if (summary.averageTrustScore < 50) {
|
||||
lines.push(' 📋 Consider a full manual code review of this codebase.');
|
||||
} else if (summary.averageTrustScore < 70) {
|
||||
lines.push(' 📋 This codebase shows moderate AI-generation signals. Review recommended.');
|
||||
} else {
|
||||
lines.push(' ✅ This codebase scores well. Continue monitoring for new issues.');
|
||||
}
|
||||
|
||||
lines.push('');
|
||||
lines.push('═'.repeat(60));
|
||||
lines.push('Generated by ai-code-trust v1.0.0');
|
||||
lines.push('═'.repeat(60));
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
export function generateJSONReport(summary: AnalysisSummary): string {
|
||||
const report = {
|
||||
version: '1.0.0',
|
||||
generatedAt: new Date().toISOString(),
|
||||
summary: {
|
||||
riskLevel: summary.riskLevel,
|
||||
averageTrustScore: summary.averageTrustScore,
|
||||
totalFiles: summary.totalFiles,
|
||||
scannedFiles: summary.scannedFiles,
|
||||
totalLines: summary.totalLines,
|
||||
issuesFound: summary.issuesFound,
|
||||
issueBreakdown: {
|
||||
critical: summary.criticalIssues,
|
||||
high: summary.highIssues,
|
||||
medium: summary.mediumIssues,
|
||||
low: summary.lowIssues
|
||||
},
|
||||
filesWithIssues: summary.filesWithIssues,
|
||||
cleanFiles: summary.cleanFiles
|
||||
},
|
||||
files: summary.fileResults.map(f => ({
|
||||
file: f.file,
|
||||
lines: f.lines,
|
||||
trustScore: f.trustScore,
|
||||
issueCount: f.detection.patterns.length,
|
||||
issues: f.detection.patterns.map(p => ({
|
||||
line: p.line,
|
||||
severity: p.severity,
|
||||
description: p.description
|
||||
}))
|
||||
}))
|
||||
};
|
||||
|
||||
return JSON.stringify(report, null, 2);
|
||||
}
|
||||
|
||||
export function generateMarkdownReport(summary: AnalysisSummary): string {
|
||||
const lines: string[] = [];
|
||||
|
||||
lines.push('# AI Code Trust Audit Report');
|
||||
lines.push('');
|
||||
lines.push(`**Generated:** ${new Date().toISOString()}`);
|
||||
lines.push(`**Risk Level:** ${summary.riskLevel.toUpperCase()}`);
|
||||
lines.push(`**Average Trust Score:** ${summary.averageTrustScore}/100`);
|
||||
lines.push('');
|
||||
|
||||
lines.push('## Summary');
|
||||
lines.push('');
|
||||
lines.push(`| Metric | Value |`);
|
||||
lines.push(`|--------|-------|`);
|
||||
lines.push(`| Files Scanned | ${summary.scannedFiles} / ${summary.totalFiles} |`);
|
||||
lines.push(`| Total Lines | ${summary.totalLines.toLocaleString()} |`);
|
||||
lines.push(`| Issues Found | ${summary.issuesFound} |`);
|
||||
lines.push(`| Critical | ${summary.criticalIssues} |`);
|
||||
lines.push(`| High | ${summary.highIssues} |`);
|
||||
lines.push(`| Medium | ${summary.mediumIssues} |`);
|
||||
lines.push(`| Low | ${summary.lowIssues} |`);
|
||||
lines.push(`| Clean Files | ${summary.cleanFiles} |`);
|
||||
lines.push('');
|
||||
|
||||
if (summary.fileResults.length > 0) {
|
||||
lines.push('## Files by Trust Score');
|
||||
lines.push('');
|
||||
lines.push(`| Score | File | Lines | Issues |`);
|
||||
lines.push(`|-------|------|-------|--------|`);
|
||||
|
||||
for (const file of summary.fileResults) {
|
||||
lines.push(`| ${file.trustScore} | ${file.file} | ${file.lines} | ${file.detection.patterns.length} |`);
|
||||
}
|
||||
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
const criticalHighFiles = summary.fileResults.filter(
|
||||
f => f.detection.patterns.some(p => p.severity === 'critical' || p.severity === 'high')
|
||||
);
|
||||
|
||||
if (criticalHighFiles.length > 0) {
|
||||
lines.push('## Critical & High Severity Issues');
|
||||
lines.push('');
|
||||
|
||||
for (const file of criticalHighFiles) {
|
||||
const issues = file.detection.patterns.filter(p => p.severity === 'critical' || p.severity === 'high');
|
||||
if (issues.length > 0) {
|
||||
lines.push(`### ${file.file}`);
|
||||
lines.push('');
|
||||
for (const issue of issues) {
|
||||
lines.push(`- **${issue.severity.toUpperCase()}** (Line ${issue.line}): ${issue.description}`);
|
||||
}
|
||||
lines.push('');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lines.push('## Recommendations');
|
||||
lines.push('');
|
||||
|
||||
if (summary.criticalIssues > 0) {
|
||||
lines.push('- **CRITICAL**: Fix all critical issues immediately before deploying.');
|
||||
}
|
||||
if (summary.highIssues > 0) {
|
||||
lines.push('- **HIGH**: Review and fix high-severity issues before code review.');
|
||||
}
|
||||
if (summary.mediumIssues > 0) {
|
||||
lines.push('- **MEDIUM**: Address medium issues during your next refactor cycle.');
|
||||
}
|
||||
if (summary.averageTrustScore < 50) {
|
||||
lines.push('- Consider a full manual code review of this codebase.');
|
||||
} else if (summary.averageTrustScore < 70) {
|
||||
lines.push('- This codebase shows moderate AI-generation signals. Review recommended.');
|
||||
} else {
|
||||
lines.push('- This codebase scores well. Continue monitoring for new issues.');
|
||||
}
|
||||
|
||||
lines.push('');
|
||||
lines.push('---');
|
||||
lines.push('*Generated by ai-code-trust v1.0.0*');
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "Node16",
|
||||
"moduleResolution": "Node16",
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user