From 75e045cd04aa6395aeb7424c2374f85e55a7e8cf Mon Sep 17 00:00:00 2001 From: BunBun Labs Date: Thu, 18 Jun 2026 00:23:00 +0000 Subject: [PATCH] Initial commit: ai-code-trust CLI tool v1.0.0 --- .gitignore | 5 + package-lock.json | 51 +++++++++ package.json | 29 +++++ src/analyzer.test.ts | 102 +++++++++++++++++ src/analyzer.ts | 208 ++++++++++++++++++++++++++++++++++ src/detectors.ts | 261 +++++++++++++++++++++++++++++++++++++++++++ src/index.ts | 168 ++++++++++++++++++++++++++++ src/report.ts | 233 ++++++++++++++++++++++++++++++++++++++ tsconfig.json | 19 ++++ 9 files changed, 1076 insertions(+) create mode 100644 .gitignore create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/analyzer.test.ts create mode 100644 src/analyzer.ts create mode 100644 src/detectors.ts create mode 100644 src/index.ts create mode 100644 src/report.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c6208b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +.venv/ +*.log +.verdict diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..cc1c4b4 --- /dev/null +++ b/package-lock.json @@ -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" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..5ec8996 --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/src/analyzer.test.ts b/src/analyzer.test.ts new file mode 100644 index 0000000..affc5e7 --- /dev/null +++ b/src/analyzer.test.ts @@ -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')); +}); diff --git a/src/analyzer.ts b/src/analyzer.ts new file mode 100644 index 0000000..f0040ef --- /dev/null +++ b/src/analyzer.ts @@ -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 = {}): 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; + } +} diff --git a/src/detectors.ts b/src/detectors.ts new file mode 100644 index 0000000..a331d2d --- /dev/null +++ b/src/detectors.ts @@ -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 { + const counts: Record = { low: 0, medium: 0, high: 0, critical: 0 }; + for (const p of patterns) { + counts[p.severity] = (counts[p.severity] || 0) + 1; + } + return counts; +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..a16b429 --- /dev/null +++ b/src/index.ts @@ -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 Output format: console (default), json, markdown + -o, --output Write output to file instead of stdout + --include Comma-separated extensions to scan (default: common code types) + --exclude Comma-separated directories to exclude + --threshold 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(); diff --git a/src/report.ts b/src/report.ts new file mode 100644 index 0000000..dbe4d10 --- /dev/null +++ b/src/report.ts @@ -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'); +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..d4cb1a8 --- /dev/null +++ b/tsconfig.json @@ -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"] +}