209 lines
5.7 KiB
TypeScript
209 lines
5.7 KiB
TypeScript
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;
|
|
}
|
|
}
|