UNPKG

next-api-analyzer

Version:

Next.js API routes analyzer for security, performance, and maintainability

1,285 lines (1,219 loc) 161 kB
#!/usr/bin/env node "use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/bin/api-analyzer.ts var api_analyzer_exports = {}; __export(api_analyzer_exports, { program: () => program }); module.exports = __toCommonJS(api_analyzer_exports); var import_commander = require("commander"); // src/lib/api-analyzer.ts var import_fs = __toESM(require("fs")); var import_path2 = __toESM(require("path")); var import_typescript2 = __toESM(require("typescript")); // src/config/default-config.ts var AUTH_PATTERNS = [ { name: "NextAuth.js", pattern: /getServerSession|getToken|next-auth/i, type: "NextAuth.js", confidence: 0.9 }, { name: "JWT", pattern: /jwt\.verify|jsonwebtoken|jose/i, type: "JWT", confidence: 0.8 }, { name: "Bearer Token", pattern: /bearer\s+token|authorization.*bearer/i, type: "Bearer Token", confidence: 0.7 }, { name: "API Key", pattern: /api[_-]?key|x-api-key/i, type: "API Key", confidence: 0.6 }, { name: "Session", pattern: /req\.session|express-session/i, type: "Session", confidence: 0.7 }, { name: "Firebase Auth", pattern: /firebase.*auth|getAuth/i, type: "Firebase Auth", confidence: 0.8 }, { name: "Supabase Auth", pattern: /supabase.*auth|createClient.*auth/i, type: "Supabase Auth", confidence: 0.8 }, { name: "Auth0", pattern: /auth0|@auth0/i, type: "Auth0", confidence: 0.9 }, { name: "Passport", pattern: /passport\.|require.*passport/i, type: "Passport", confidence: 0.8 } ]; var MIDDLEWARE_PATTERNS = [ { name: "CORS", pattern: /cors\(|Access-Control-Allow/i, category: "security" }, { name: "Helmet", pattern: /helmet\(|security headers/i, category: "security" }, { name: "Rate Limiting", pattern: /rateLimit|express-rate-limit|slowDown/i, category: "security" }, { name: "Body Parser", pattern: /bodyParser|express\.json/i, category: "parsing" }, { name: "Multer", pattern: /multer\(|file upload/i, category: "file-handling" }, { name: "Validation", pattern: /joi\.|yup\.|zod\.|express-validator/i, category: "validation" }, { name: "Logging", pattern: /morgan\(|winston|pino/i, category: "logging" }, { name: "Compression", pattern: /compression\(/i, category: "performance" }, { name: "Cookie Parser", pattern: /cookieParser|cookie-parser/i, category: "parsing" }, { name: "CSRF", pattern: /csrf|csurf/i, category: "security" } ]; var DEFAULT_CONFIG = { apiDir: "src/app/api", outputDir: "./api-analysis", includePatterns: ["**/*.ts", "**/*.js", "**/*.tsx"], excludePatterns: ["**/node_modules/**", "**/*.test.*", "**/*.spec.*", "**/*.d.ts", "**/dist/**", "**/build/**"], authPatterns: AUTH_PATTERNS, middlewarePatterns: MIDDLEWARE_PATTERNS, enableTrends: false, enablePerformanceAnalysis: true, enableSecurityAnalysis: true, thresholds: { security: 80, performance: 70, maintainability: 75, testCoverage: 80, complexity: 10 }, cache: { enabled: true, ttl: 36e5, directory: ".cache/api-analyzer" }, parallel: true, maxConcurrency: 4 }; function validateConfig(config) { const errors = []; if (!config.apiDir) { errors.push("apiDir is required"); } if (config.thresholds.security < 0 || config.thresholds.security > 100) { errors.push("Security threshold must be between 0 and 100"); } if (config.thresholds.performance < 0 || config.thresholds.performance > 100) { errors.push("Performance threshold must be between 0 and 100"); } return errors; } // src/utils/file-utils.ts var import_promises = __toESM(require("fs/promises")); var import_path = __toESM(require("path")); var import_glob = require("glob"); var import_crypto = __toESM(require("crypto")); // src/utils/logger.ts var Logger = class _Logger { constructor() { this.config = { level: 1 /* INFO */, colors: true }; } static getInstance() { if (!_Logger.instance) { _Logger.instance = new _Logger(); } return _Logger.instance; } configure(config) { this.config = { ...this.config, ...config }; } shouldLog(level) { return level >= this.config.level; } formatMessage(level, message, emoji) { const prefix = this.config.prefix ? `[${this.config.prefix}] ` : ""; return this.config.colors ? `${prefix}${emoji} ${message}` : `${prefix}${level}: ${message}`; } debug(message, ...args) { if (this.shouldLog(0 /* DEBUG */)) { console.log(this.formatMessage("DEBUG", message, "\u{1F41B}"), ...args); } } info(message, ...args) { if (this.shouldLog(1 /* INFO */)) { console.log(this.formatMessage("INFO", message, "\u2139\uFE0F"), ...args); } } success(message, ...args) { if (this.shouldLog(1 /* INFO */)) { console.log(this.formatMessage("SUCCESS", message, "\u2705"), ...args); } } warn(message, ...args) { if (this.shouldLog(2 /* WARN */)) { console.warn(this.formatMessage("WARN", message, "\u26A0\uFE0F"), ...args); } } error(message, ...args) { if (this.shouldLog(3 /* ERROR */)) { console.error(this.formatMessage("ERROR", message, "\u274C"), ...args); } } progress(message) { if (this.shouldLog(1 /* INFO */)) { process.stdout.write(`\u23F3 ${message}...\r`); } } clearProgress() { process.stdout.write("\r\x1B[K"); } separator() { if (this.shouldLog(1 /* INFO */)) { console.log("\u2500".repeat(80)); } } }; var logger = Logger.getInstance(); // src/utils/file-utils.ts var FileUtils = class { static async findApiFiles(config) { try { const patterns = config.includePatterns.map((pattern) => import_path.default.join(config.apiDir, pattern).replace(/\\/g, "/")); const allFiles = []; for (const pattern of patterns) { const matches = await (0, import_glob.glob)(pattern, { ignore: config.excludePatterns, absolute: true, nodir: true }); allFiles.push(...matches); } const uniqueFiles = [...new Set(allFiles)]; return uniqueFiles.filter((file) => this.isApiFile(file)); } catch (error) { logger.error("Error finding API files:", error); throw new Error(`Failed to find API files: ${error}`); } } static isApiFile(filename) { const basename = import_path.default.basename(filename); const isTypeScriptOrJavaScript = /\.(js|ts|tsx)$/.test(basename); const isRouteFile = basename === "route.js" || basename === "route.ts"; const isInApiDirectory = filename.includes("/api/") || filename.includes("\\api\\"); return isTypeScriptOrJavaScript && (isRouteFile || isInApiDirectory); } static async getFileStats(filePath) { try { const [stats, content] = await Promise.all([import_promises.default.stat(filePath), import_promises.default.readFile(filePath, "utf-8")]); const linesOfCode = content.split("\n").filter((line) => { const trimmed = line.trim(); return trimmed && !trimmed.startsWith("//") && !trimmed.startsWith("/*"); }).length; return { size: stats.size, lastModified: stats.mtime, linesOfCode }; } catch (error) { logger.error(`Error getting file stats for ${filePath}:`, error); throw error; } } static async ensureDirectoryExists(dirPath) { try { await import_promises.default.mkdir(dirPath, { recursive: true }); } catch (error) { if (error.code !== "EEXIST") { throw error; } } } static async writeJsonFile(filePath, data) { await this.ensureDirectoryExists(import_path.default.dirname(filePath)); await import_promises.default.writeFile(filePath, JSON.stringify(data, null, 2), "utf-8"); } static async readJsonFile(filePath) { try { const content = await import_promises.default.readFile(filePath, "utf-8"); return JSON.parse(content); } catch (error) { if (error.code === "ENOENT") { return null; } throw error; } } static async writeFile(filePath, content) { await this.ensureDirectoryExists(import_path.default.dirname(filePath)); await import_promises.default.writeFile(filePath, content, "utf-8"); } static generateHash(content) { return import_crypto.default.createHash("md5").update(content).digest("hex"); } static async fileExists(filePath) { try { await import_promises.default.access(filePath); return true; } catch { return false; } } }; // src/utils/common.ts var createRecommendation = (id, type, severity, title, description, route, solution, impact, effort, category, tags = [], codeExample, fixExample) => ({ id: `${id}_${route.replace(/[^a-zA-Z0-9]/g, "_")}`, type, severity, title, description, route, solution, impact, effort, category, tags, codeExample, fixExample }); var calculateRiskLevel = (score) => { if (score >= 50) return "CRITICAL"; if (score >= 30) return "HIGH"; if (score >= 15) return "MEDIUM"; return "LOW"; }; var getScoreColor = (score) => { if (score >= 90) return "\u{1F7E2}"; if (score >= 70) return "\u{1F7E1}"; if (score >= 50) return "\u{1F7E0}"; return "\u{1F534}"; }; var getRiskEmoji = (risk) => { const emojiMap = { LOW: "\u{1F7E2}", MEDIUM: "\u{1F7E1}", HIGH: "\u{1F7E0}", CRITICAL: "\u{1F534}" }; return emojiMap[risk]; }; var getSeverityWeight = (severity) => { const weights = { LOW: 1, MEDIUM: 2, HIGH: 3, CRITICAL: 4 }; return weights[severity]; }; var SECURITY_PATTERNS = { SQL_INJECTION: [/query\s*\+\s*['"`]/, /\$\{[^}]*query[^}]*\}/, /SELECT\s+.*\+.*FROM/i], XSS: [/innerHTML\s*=\s*.*req\./, /dangerouslySetInnerHTML/, /eval\s*\(.*req\./], HARDCODED_SECRETS: [ /password\s*[:=]\s*['"`][^'"`]{8,}/, /api[_-]?key\s*[:=]\s*['"`][A-Za-z0-9]{16,}/, /secret\s*[:=]\s*['"`][A-Za-z0-9]{16,}/ ] }; var PERFORMANCE_PATTERNS = { BLOCKING_OPERATIONS: [/fs\.readFileSync/, /fs\.writeFileSync/, /child_process\.execSync/], INEFFICIENT_QUERIES: [/SELECT \*/i, /\.find$$$$/, /N\+1/i], MEMORY_LEAKS: [/setInterval\s*\(/, /new Array\s*\(\s*\d{6,}/, /global\./] }; // src/analyzers/base-analyzer.ts var import_typescript = __toESM(require("typescript")); var BaseAnalyzer = class { static extractMethods(content, sourceFile, isAppRouter) { const methods = /* @__PURE__ */ new Set(); if (isAppRouter) { const httpMethods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]; import_typescript.default.forEachChild(sourceFile, (node) => { if (import_typescript.default.isFunctionDeclaration(node) && node.name && httpMethods.includes(node.name.text)) { methods.add(node.name.text); } }); } else { const methodRegex = /req\.method\s*===?\s*['"`](\w+)['"`]/g; let match; while ((match = methodRegex.exec(content)) !== null) { methods.add(match[1].toUpperCase()); } } return methods.size === 0 ? ["GET"] : Array.from(methods); } static extractParams(content, pattern) { const params = /* @__PURE__ */ new Set(); let match; while ((match = pattern.exec(content)) !== null) { params.add(match[1]); } return Array.from(params); } static calculateComplexity(sourceFile) { let complexity = 1; const visit = (node) => { switch (node.kind) { case import_typescript.default.SyntaxKind.IfStatement: case import_typescript.default.SyntaxKind.WhileStatement: case import_typescript.default.SyntaxKind.ForStatement: case import_typescript.default.SyntaxKind.SwitchStatement: case import_typescript.default.SyntaxKind.ConditionalExpression: complexity++; break; } import_typescript.default.forEachChild(node, visit); }; visit(sourceFile); return complexity; } }; BaseAnalyzer.createRecommendation = createRecommendation; // src/analyzers/security-analyzer.ts var SecurityAnalyzer = class extends BaseAnalyzer { static analyzeRoute(route, content) { const recommendations = []; const vulnerabilities = []; let riskScore = 0; try { if (!route.hasAuth && this.isSensitiveRoute(route)) { riskScore += 30; recommendations.push( this.createRecommendation( "MISSING_AUTHENTICATION", "SECURITY", "HIGH", "Missing Authentication", `Route ${route.path} allows ${route.methods.join(", ")} operations without authentication`, route.path, "Add authentication middleware", "Unauthorized access to sensitive operations", "MEDIUM", "authentication", ["security", "auth"] ) ); } for (const vulnerability of this.VULNERABILITIES) { for (const pattern of vulnerability.patterns) { if (pattern.test(content)) { riskScore += vulnerability.score; vulnerabilities.push(vulnerability.name); recommendations.push( this.createRecommendation( vulnerability.name, "SECURITY", vulnerability.severity, `Potential ${vulnerability.name.replace("_", " ")}`, `Detected pattern that may indicate ${vulnerability.name.toLowerCase().replace("_", " ")} vulnerability`, route.path, this.getVulnerabilitySolution(vulnerability.name), this.getVulnerabilityImpact(vulnerability.name), "MEDIUM", vulnerability.category, ["security", vulnerability.category] ) ); break; } } } Object.entries(SECURITY_PATTERNS).forEach(([vulnName, patterns]) => { if (patterns.some((pattern) => pattern.test(content))) { const severity = vulnName === "SQL_INJECTION" ? "CRITICAL" : "HIGH"; const score = severity === "CRITICAL" ? 40 : 25; riskScore += score; recommendations.push( this.createRecommendation( vulnName, "SECURITY", severity, `Potential ${vulnName.replace("_", " ")}`, `Detected pattern that may indicate ${vulnName.toLowerCase().replace("_", " ")} vulnerability`, route.path, this.getVulnerabilitySolution(vulnName), this.getVulnerabilityImpact(vulnName), "MEDIUM", "security", ["security", vulnName.toLowerCase()] ) ); } }); const riskLevel = calculateRiskLevel(riskScore); const securityScore = Math.max(0, 100 - riskScore); logger.debug(`Security analysis for ${route.path}: score=${securityScore}, risk=${riskLevel}`); return { riskLevel, recommendations, securityScore, vulnerabilities }; } catch (error) { logger.error(`Error in security analysis for ${route.path}:`, error); return { riskLevel: "MEDIUM", recommendations: [], securityScore: 50, vulnerabilities: [] }; } } static isSensitiveRoute(route) { const sensitiveMethods = ["POST", "PUT", "DELETE", "PATCH"]; const sensitivePathPatterns = [/\/admin\//i, /\/user[s]?\//i, /\/auth\//i, /\/config\//i, /\/settings\//i, /\/payment[s]\//i, /\/checkout\//i, /\/order[s]?\//i]; return route.methods.some((method) => sensitiveMethods.includes(method)) || sensitivePathPatterns.some((pattern) => pattern.test(route.path)); } static getVulnerabilitySolution(vulnerability) { const solutions = { SQL_INJECTION: "Use parameterized queries or ORM with proper escaping", XSS: "Sanitize user input and use Content Security Policy", HARDCODED_SECRETS: "Move secrets to environment variables" }; return solutions[vulnerability] || "Review and fix the security issue"; } static getVulnerabilityImpact(vulnerability) { const impacts = { SQL_INJECTION: "Database compromise and data theft", XSS: "Client-side code execution and session hijacking", HARDCODED_SECRETS: "Credential exposure and unauthorized access" }; return impacts[vulnerability] || "Security vulnerability"; } }; SecurityAnalyzer.VULNERABILITIES = [ { name: "SQL_INJECTION", patterns: [ /query\s*\+\s*['"`]/, /\$\{[^}]*query[^}]*\}/, /execute\s*\(\s*['"`].*\$\{/, /SELECT\s+.*\+.*FROM/i, /INSERT\s+.*\+.*VALUES/i, /UPDATE\s+.*SET\s+.*\+/i, /DELETE\s+.*WHERE\s+.*\+/i ], severity: "CRITICAL", score: 40, category: "injection", cwe: "CWE-89" }, { name: "XSS", patterns: [ /innerHTML\s*=\s*.*req\./, /dangerouslySetInnerHTML/, /document\.write\s*\(.*req\./, /eval\s*\(.*req\./, /Function\s*\(.*req\./ ], severity: "HIGH", score: 30, category: "injection", cwe: "CWE-79" }, { name: "HARDCODED_SECRETS", patterns: [ /password\s*[:=]\s*['"`][^'"`]{8,}/, /api[_-]?key\s*[:=]\s*['"`][A-Za-z0-9]{16,}/, /secret\s*[:=]\s*['"`][A-Za-z0-9]{16,}/, /token\s*[:=]\s*['"`][A-Za-z0-9]{20,}/, /private[_-]?key\s*[:=]\s*['"`][^'"`]{20,}/ ], severity: "CRITICAL", score: 35, category: "secrets", cwe: "CWE-798" }, { name: "WEAK_CRYPTO", patterns: [ /md5\s*\(/, /sha1\s*\(/, /Math\.random\s*$$\s*$$/, /crypto\.pseudoRandomBytes/, /des\s*\(/i, /rc4\s*\(/i ], severity: "MEDIUM", score: 15, category: "cryptography", cwe: "CWE-327" }, { name: "CORS_MISCONFIGURATION", patterns: [ /Access-Control-Allow-Origin.*\*/, /cors\s*\(\s*\{\s*origin\s*:\s*true/, /Access-Control-Allow-Credentials.*true.*Access-Control-Allow-Origin.*\*/ ], severity: "MEDIUM", score: 20, category: "configuration", cwe: "CWE-942" }, { name: "PATH_TRAVERSAL", patterns: [/\.\.[/\\]/, /path\.join\s*\([^)]*req\./, /fs\.readFile\s*\([^)]*req\./, /require\s*\([^)]*req\./], severity: "HIGH", score: 25, category: "path-traversal", cwe: "CWE-22" }, { name: "COMMAND_INJECTION", patterns: [/exec\s*\([^)]*req\./, /spawn\s*\([^)]*req\./, /system\s*\([^)]*req\./, /shell\s*\([^)]*req\./], severity: "CRITICAL", score: 45, category: "injection", cwe: "CWE-78" } ]; // src/analyzers/performance-analyzer.ts var PerformanceAnalyzer = class extends BaseAnalyzer { static analyzeRoute(route, content, sourceFile) { const recommendations = []; let performanceScore = 100; const complexity = this.calculateComplexity(sourceFile); Object.entries(PERFORMANCE_PATTERNS).forEach(([issueName, patterns]) => { if (patterns.some((pattern) => pattern.test(content))) { const score = this.getIssueScore(issueName); performanceScore -= score; recommendations.push( this.createRecommendation( issueName, "PERFORMANCE", this.getIssueSeverity(issueName), this.getIssueTitle(issueName), this.getIssueDescription(issueName), route.path, this.getIssueSolution(issueName), this.getIssueImpact(issueName), "MEDIUM", "performance", ["performance", issueName.toLowerCase()] ) ); } }); if (complexity > 10) { const complexityPenalty = Math.min(20, (complexity - 10) * 2); performanceScore -= complexityPenalty; recommendations.push( this.createRecommendation( "HIGH_COMPLEXITY", "PERFORMANCE", complexity > 20 ? "HIGH" : "MEDIUM", "High Cyclomatic Complexity", `Route has high cyclomatic complexity (${complexity})`, route.path, "Refactor into smaller functions and reduce nesting", "Harder to maintain and potentially slower execution", "HIGH", "complexity", ["complexity", "maintainability"] ) ); } return { performanceScore: Math.max(0, performanceScore), recommendations, complexity }; } static getIssueScore(issueName) { const scores = { BLOCKING_OPERATIONS: 20, INEFFICIENT_QUERIES: 15, MEMORY_LEAKS: 25 }; return scores[issueName] || 10; } static getIssueSeverity(issueName) { const severities = { BLOCKING_OPERATIONS: "HIGH", INEFFICIENT_QUERIES: "MEDIUM", MEMORY_LEAKS: "HIGH" }; return severities[issueName] || "MEDIUM"; } static getIssueTitle(issueName) { const titles = { BLOCKING_OPERATIONS: "Blocking Operations", INEFFICIENT_QUERIES: "Inefficient Database Queries", MEMORY_LEAKS: "Potential Memory Leaks" }; return titles[issueName] || "Performance Issue"; } static getIssueDescription(issueName) { const descriptions = { BLOCKING_OPERATIONS: "Synchronous operations that can block the event loop", INEFFICIENT_QUERIES: "Database queries that may be inefficient", MEMORY_LEAKS: "Code patterns that may cause memory leaks" }; return descriptions[issueName] || "Performance issue detected"; } static getIssueSolution(issueName) { const solutions = { BLOCKING_OPERATIONS: "Use asynchronous alternatives", INEFFICIENT_QUERIES: "Optimize queries and use proper indexing", MEMORY_LEAKS: "Ensure proper cleanup of resources" }; return solutions[issueName] || "Optimize the code"; } static getIssueImpact(issueName) { const impacts = { BLOCKING_OPERATIONS: "Reduced server performance and responsiveness", INEFFICIENT_QUERIES: "Slower response times and increased database load", MEMORY_LEAKS: "Memory consumption growth over time" }; return impacts[issueName] || "Performance degradation"; } }; // src/lib/api-analyzer.ts var NextApiAnalyzer = class { constructor(config = {}) { this.routes = []; this.startTime = 0; this.config = { ...DEFAULT_CONFIG, ...config }; } async analyzeRoutes() { this.startTime = Date.now(); this.routes = []; logger.info("Starting API routes analysis..."); const files = await FileUtils.findApiFiles(this.config); logger.info(`Found ${files.length} API files`); for (const file of files) { await this.analyzeFile(file); } logger.success(`Analyzed ${this.routes.length} API routes`); return this.generateAnalysisResult(); } async analyzeFile(filePath) { try { const content = import_fs.default.readFileSync(filePath, "utf-8"); const fileStats = await FileUtils.getFileStats(filePath); const routeInfo = await this.parseRouteInfo(filePath, content, fileStats); this.routes.push(routeInfo); } catch (error) { logger.error(`Error analyzing file ${filePath}:`, error); } } async parseRouteInfo(filePath, content, fileStats) { const routePath = this.getRoutePath(filePath); const isAppRouter = this.isAppRouterFile(filePath); const sourceFile = import_typescript2.default.createSourceFile(filePath, content, import_typescript2.default.ScriptTarget.Latest, true); const baseRoute = { path: routePath, methods: this.extractMethods(content, sourceFile, isAppRouter), hasAuth: this.detectAuth(content), authTypes: this.extractAuthTypes(content), queryParams: this.extractQueryParams(content, isAppRouter), pathParams: this.extractPathParams(routePath), bodyParams: this.extractBodyParams(content, isAppRouter), headers: this.extractHeaders(content), responseStatuses: this.extractResponseStatuses(content, isAppRouter), middlewares: this.extractMiddlewares(content), description: this.extractDescription(content), hasRateLimit: this.detectRateLimit(content), hasCors: this.detectCors(content), hasInputValidation: this.detectInputValidation(content), dependencies: this.extractDependencies(sourceFile), fileSize: fileStats.size, linesOfCode: fileStats.linesOfCode, lastModified: fileStats.lastModified }; const securityAnalysis = SecurityAnalyzer.analyzeRoute(baseRoute, content); const performanceAnalysis = PerformanceAnalyzer.analyzeRoute(baseRoute, content, sourceFile); return { ...baseRoute, riskLevel: securityAnalysis.riskLevel, complexity: performanceAnalysis.complexity, performanceScore: performanceAnalysis.performanceScore }; } generateAnalysisResult() { const duration = Date.now() - this.startTime; const recommendations = this.generateRecommendations(); const summary = { totalRoutes: this.routes.length, secureRoutes: this.routes.filter((r) => r.hasAuth).length, publicRoutes: this.routes.filter((r) => !r.hasAuth).length, methodsBreakdown: this.calculateMethodsBreakdown(), statusCodeDistribution: this.calculateStatusCodeDistribution(), riskDistribution: this.calculateRiskDistribution(), securityScore: this.calculateSecurityScore(), performanceScore: this.calculatePerformanceScore(), maintainabilityScore: this.calculateMaintainabilityScore(), testCoverageScore: 75 }; return { routes: this.routes, summary, metadata: { analyzedAt: /* @__PURE__ */ new Date(), version: "3.1.0", duration, totalFiles: this.routes.length, totalLinesOfCode: this.routes.reduce((sum, route) => sum + (route.linesOfCode || 0), 0) }, recommendations }; } generateReport(analysis) { let report = "# \u{1F50D} API Routes Analysis Report\n\n"; report += `**Generated:** ${analysis.metadata.analyzedAt.toLocaleString()} `; report += `**Duration:** ${analysis.metadata.duration}ms `; report += `**Files:** ${analysis.metadata.totalFiles} `; report += "## \u{1F4CA} Summary\n\n"; report += `| Metric | Value | Status | `; report += `|--------|-------|--------| `; report += `| Total Routes | ${analysis.summary.totalRoutes} | \u2139\uFE0F | `; report += `| Security Score | ${analysis.summary.securityScore.toFixed(1)}% | ${getScoreColor(analysis.summary.securityScore)} | `; report += `| Performance Score | ${analysis.summary.performanceScore.toFixed(1)}% | ${getScoreColor(analysis.summary.performanceScore)} | `; if (analysis.recommendations.length > 0) { report += "## \u{1F4A1} Recommendations\n\n"; analysis.recommendations.sort((a, b) => getSeverityWeight(b.severity) - getSeverityWeight(a.severity)).slice(0, 10).forEach((rec, index) => { report += `### ${index + 1}. ${rec.title} `; report += `**Severity:** ${rec.severity} | **Type:** ${rec.type} `; report += `${rec.description} `; report += `**Solution:** ${rec.solution} `; }); } return report; } isAppRouterFile(filePath) { return filePath.includes("/route.") || filePath.endsWith("route.js") || filePath.endsWith("route.ts"); } getRoutePath(filePath) { const relativePath = import_path2.default.relative(this.config.apiDir, filePath); return "/" + relativePath.replace(/\\/g, "/").replace(/\.(js|ts|tsx)$/, "").replace(/\/index$/, "").replace(/\/route$/, "").replace(/\[([^\]]+)\]/g, ":$1"); } extractMethods(content, sourceFile, isAppRouter) { const methods = /* @__PURE__ */ new Set(); if (isAppRouter) { const httpMethods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]; import_typescript2.default.forEachChild(sourceFile, (node) => { if (import_typescript2.default.isFunctionDeclaration(node) && node.name && httpMethods.includes(node.name.text)) { methods.add(node.name.text); } }); } else { const methodRegex = /req\.method\s*===?\s*['"`](\w+)['"`]/g; let match; while ((match = methodRegex.exec(content)) !== null) { methods.add(match[1].toUpperCase()); } } return methods.size === 0 ? ["GET"] : Array.from(methods); } detectAuth(content) { return this.config.authPatterns.some((pattern) => pattern.pattern.test(content)); } extractAuthTypes(content) { return this.config.authPatterns.filter((pattern) => pattern.pattern.test(content)).map((pattern) => pattern.type); } extractQueryParams(content, isAppRouter) { const params = /* @__PURE__ */ new Set(); const regex = isAppRouter ? /searchParams\.get$$['"`]([^'"`]+)['"`]$$/g : /req\.query\.(\w+)/g; let match; while ((match = regex.exec(content)) !== null) { params.add(match[1]); } return Array.from(params).map((name) => ({ name, type: "string", required: true })); } extractPathParams(routePath) { const params = /* @__PURE__ */ new Set(); const regex = /\[([^\]]+)\]/g; let match; while ((match = regex.exec(routePath)) !== null) { params.add(match[1]); } return Array.from(params).map((name) => ({ name, type: "string", required: true })); } extractBodyParams(content, isAppRouter) { const params = /* @__PURE__ */ new Set(); const regex = isAppRouter ? /const\s*\{\s*([^}]+)\s*\}\s*=\s*await\s+request\.json$$$$/g : /req\.body\.(\w+)/g; let match; while ((match = regex.exec(content)) !== null) { if (isAppRouter) { const paramNames = match[1].split(",").map((p) => p.trim()); paramNames.forEach((param) => params.add(param)); } else { params.add(match[1]); } } return Array.from(params).map((name) => ({ name, type: "string", required: true })); } extractHeaders(content) { const headers = /* @__PURE__ */ new Set(); const patterns = [/headers\.get$$['"`]([^'"`]+)['"`]$$/g, /req\.headers\[['"`]([^'"`]+)['"`]\]/g]; patterns.forEach((pattern) => { let match; while ((match = pattern.exec(content)) !== null) { headers.add(match[1]); } }); return Array.from(headers); } extractResponseStatuses(content, isAppRouter) { const statuses = /* @__PURE__ */ new Set(); if (isAppRouter) { const regex = /Response\.json\([^,]*,\s*\{\s*status:\s*(\d+)/g; let match; while ((match = regex.exec(content)) !== null) { statuses.add(Number.parseInt(match[1])); } } else { const regex = /res\.status$$(\d+)$$/g; let match; while ((match = regex.exec(content)) !== null) { statuses.add(Number.parseInt(match[1])); } } return statuses.size === 0 ? [200] : Array.from(statuses).sort(); } extractMiddlewares(content) { return this.config.middlewarePatterns.filter((pattern) => pattern.pattern.test(content)).map((pattern) => pattern.name); } extractDescription(content) { const jsDocRegex = /\/\*\*\s*\n\s*\*\s*(.+?)\s*\n/; const match = content.match(jsDocRegex); return match ? match[1].trim() : void 0; } detectRateLimit(content) { return /rate[_-]?limit|throttle|slowDown/i.test(content); } detectCors(content) { return /cors|Access-Control-Allow/i.test(content); } detectInputValidation(content) { return /joi\.|yup\.|zod\.|validate\(|schema\./i.test(content); } extractDependencies(sourceFile) { const dependencies = /* @__PURE__ */ new Set(); import_typescript2.default.forEachChild(sourceFile, (node) => { if (import_typescript2.default.isImportDeclaration(node)) { const moduleSpecifier = node.moduleSpecifier.getText().replace(/['"]/g, ""); if (!moduleSpecifier.startsWith(".") && !moduleSpecifier.startsWith("/")) { dependencies.add(moduleSpecifier); } } }); return Array.from(dependencies); } generateRecommendations() { return []; } calculateMethodsBreakdown() { const breakdown = {}; this.routes.forEach((route) => { route.methods.forEach((method) => { breakdown[method] = (breakdown[method] || 0) + 1; }); }); return breakdown; } calculateStatusCodeDistribution() { const distribution = {}; this.routes.forEach((route) => { route.responseStatuses.forEach((status) => { const key = status.toString(); distribution[key] = (distribution[key] || 0) + 1; }); }); return distribution; } calculateRiskDistribution() { const distribution = { LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0 }; this.routes.forEach((route) => { distribution[route.riskLevel]++; }); return distribution; } calculateSecurityScore() { if (this.routes.length === 0) return 100; const secureRoutes = this.routes.filter((r) => r.hasAuth).length; return secureRoutes / this.routes.length * 100; } calculatePerformanceScore() { if (this.routes.length === 0) return 100; const totalScore = this.routes.reduce((sum, route) => sum + (route.performanceScore || 100), 0); return totalScore / this.routes.length; } calculateMaintainabilityScore() { if (this.routes.length === 0) return 100; const avgComplexity = this.routes.reduce((sum, route) => sum + (route.complexity || 1), 0) / this.routes.length; return Math.max(0, 100 - (avgComplexity > 10 ? (avgComplexity - 10) * 5 : 0)); } }; // src/lib/plugin-system.ts var PluginManager = class { constructor() { this.plugins = /* @__PURE__ */ new Map(); } async loadPlugin(plugin) { this.plugins.set(plugin.name, plugin); logger.info(`Loaded plugin: ${plugin.name} v${plugin.version}`); } async runPlugins(route, content, context) { const results = []; for (const [name, plugin] of this.plugins) { try { const result = await plugin.analyze(route, content, context); results.push(result); } catch (error) { logger.error(`Plugin ${name} failed for route ${route.path}:`, error); } } return results; } getLoadedPlugins() { return Array.from(this.plugins.keys()); } }; var OpenApiPlugin = class { constructor() { this.name = "openapi-generator"; this.version = "1.0.0"; } async analyze(route, content) { const recommendations = []; if (!this.hasOpenApiDocs(content)) { recommendations.push( createRecommendation( "openapi_missing", "DOCUMENTATION", "LOW", "Missing OpenAPI Documentation", "Route lacks OpenAPI/Swagger documentation", route.path, "Add JSDoc comments with OpenAPI annotations", "Reduced API discoverability", "LOW", "documentation", ["openapi", "documentation"] ) ); } return { recommendations, metrics: {}, metadata: {} }; } hasOpenApiDocs(content) { return /\/\*\*[\s\S]*@swagger[\s\S]*\*\//.test(content); } }; var TestCoveragePlugin = class { constructor() { this.name = "test-coverage"; this.version = "1.0.0"; } async analyze(route) { const recommendations = []; const hasTests = false; if (!hasTests) { recommendations.push( createRecommendation( "test_missing", "TESTING", "MEDIUM", "Missing Test Coverage", "Route appears to lack test coverage", route.path, "Add unit and integration tests", "Reduced code quality and increased bug risk", "MEDIUM", "testing", ["testing", "quality"] ) ); } return { recommendations, metrics: { testCoverage: hasTests ? 80 : 0 }, metadata: {} }; } }; // src/bin/api-analyzer.ts var import_path4 = __toESM(require("path")); var import_perf_hooks = require("perf_hooks"); var import_chalk = __toESM(require("chalk")); // src/lib/report-generator.ts var import_path3 = __toESM(require("path")); var UnifiedReportGenerator = class { constructor(config, outputDir) { this.config = config; this.outputDir = outputDir; } async generateUnifiedReport(command, analysis, additionalData) { logger.info(`\u{1F4CA} Generating unified report for ${command} command...`); const reportData = { metadata: { generatedAt: /* @__PURE__ */ new Date(), version: "3.1.0", command, duration: analysis.metadata.duration, configHash: this.generateConfigHash() }, analysis, ...additionalData, insights: await this.generateInsights(analysis), recommendations: await this.enhanceRecommendations(analysis.recommendations) }; await this.generateMarkdownReport(reportData); await this.generateJsonReport(reportData); await this.generateHtmlReport(reportData); await this.generateCsvReport(reportData); await this.generateExecutiveSummary(reportData); logger.success(`\u2705 Unified report generated successfully in ${this.outputDir}`); return reportData; } async generateMarkdownReport(data) { let report = this.generateMarkdownHeader(data); report += this.generateExecutiveSummarySection(data); report += this.generateAnalysisOverviewSection(data); if (data.security) { report += this.generateSecuritySection(data.security); } if (data.performance) { report += this.generatePerformanceSection(data.performance); } if (data.trends) { report += this.generateTrendsSection(data.trends); } if (data.comparison) { report += this.generateComparisonSection(data.comparison); } report += this.generateInsightsSection(data.insights); report += this.generateRecommendationsSection(data.recommendations); report += this.generateDetailedRoutesSection(data.analysis.routes); report += this.generateAppendixSection(data); await FileUtils.writeFile(import_path3.default.join(this.outputDir, "unified-report.md"), report); } generateMarkdownHeader(data) { return `# \u{1F50D} Comprehensive API Analysis Report **Command:** \`${data.metadata.command}\` **Generated:** ${data.metadata.generatedAt.toLocaleString()} **Version:** ${data.metadata.version} **Duration:** ${data.metadata.duration.toFixed(2)}ms **Total Routes:** ${data.analysis.summary.totalRoutes} **Total Files:** ${data.analysis.metadata.totalFiles} **Lines of Code:** ${data.analysis.metadata.totalLinesOfCode.toLocaleString()} --- `; } generateExecutiveSummarySection(data) { const summary = data.analysis.summary; const insights = data.insights; return `## \u{1F4CB} Executive Summary ### Overall Health Score | Metric | Score | Status | Trend | |--------|-------|--------|-------| | Security | ${summary.securityScore.toFixed(1)}% | ${getScoreColor(summary.securityScore)} | ${this.getTrendIndicator(data.trends?.trends.security)} | | Performance | ${summary.performanceScore.toFixed(1)}% | ${getScoreColor(summary.performanceScore)} | ${this.getTrendIndicator(data.trends?.trends.performance)} | | Maintainability | ${summary.maintainabilityScore.toFixed(1)}% | ${getScoreColor(summary.maintainabilityScore)} | ${this.getTrendIndicator(data.trends?.trends.maintainability)} | | Test Coverage | ${summary.testCoverageScore.toFixed(1)}% | ${getScoreColor(summary.testCoverageScore)} | - | ### Key Findings ${insights.keyFindings.map((finding) => `- ${finding}`).join("\n")} ### Critical Issues Requiring Immediate Attention ${insights.criticalIssues.length > 0 ? insights.criticalIssues.map((issue) => `- \u26A0\uFE0F ${issue}`).join("\n") : "- \u2705 No critical issues identified"} ### Quick Wins (Low Effort, High Impact) ${insights.quickWins.map((win) => `- \u{1F3AF} ${win}`).join("\n")} --- `; } generateAnalysisOverviewSection(data) { const summary = data.analysis.summary; return `## \u{1F4CA} Analysis Overview ### Route Distribution - **Total Routes:** ${summary.totalRoutes} - **Secured Routes:** ${summary.secureRoutes} (${(summary.secureRoutes / summary.totalRoutes * 100).toFixed(1)}%) - **Public Routes:** ${summary.publicRoutes} (${(summary.publicRoutes / summary.totalRoutes * 100).toFixed(1)}%) ### HTTP Methods Breakdown ${Object.entries(summary.methodsBreakdown).filter(([_, count]) => count > 0).map(([method, count]) => `- **${method}:** ${count} routes`).join("\n")} ### Risk Distribution ${Object.entries(summary.riskDistribution).map(([risk, count]) => `- ${getRiskEmoji(risk)} **${risk}:** ${count} routes`).join("\n")} ### Response Status Codes ${Object.entries(summary.statusCodeDistribution).map(([status, count]) => `- **${status}:** ${count} occurrences`).join("\n")} --- `; } generateSecuritySection(security) { return `## \u{1F510} Security Analysis ### Vulnerability Assessment | Severity | Count | Routes Affected | |----------|-------|-----------------| | Critical | ${security.vulnerabilities.critical.length} | ${this.getAffectedRoutes(security.vulnerabilities.critical)} | | High | ${security.vulnerabilities.high.length} | ${this.getAffectedRoutes(security.vulnerabilities.high)} | | Medium | ${security.vulnerabilities.medium.length} | ${this.getAffectedRoutes(security.vulnerabilities.medium)} | | Low | ${security.vulnerabilities.low.length} | ${this.getAffectedRoutes(security.vulnerabilities.low)} | ### Compliance Assessment #### OWASP Top 10 Compliance **Score:** ${security.compliance.owasp.score.toFixed(1)}% ${Object.entries(security.compliance.owasp.coverage).map(([item, compliant]) => `- ${compliant ? "\u2705" : "\u274C"} ${item}`).join("\n")} **Identified Issues:** ${security.compliance.owasp.issues.map((issue) => `- \u26A0\uFE0F ${issue}`).join("\n")} #### PCI DSS Compliance **Score:** ${security.compliance.pciDss.score.toFixed(1)}% ${Object.entries(security.compliance.pciDss.requirements).map(([req, compliant]) => `- ${compliant ? "\u2705" : "\u274C"} ${req}`).join("\n")} ### Risk Assessment **Overall Risk Level:** ${getRiskEmoji(security.riskAssessment.overallRisk)} ${security.riskAssessment.overallRisk} **Risk Factors:** ${security.riskAssessment.riskFactors.map((factor) => `- ${factor}`).join("\n")} **Mitigation Priority:** ${security.riskAssessment.mitigationPriority.map((item, index) => `${index + 1}. ${item}`).join("\n")} ### Security Metrics - **Authentication Coverage:** ${security.securityMetrics.authenticationCoverage.toFixed(1)}% - **Input Validation Coverage:** ${security.securityMetrics.inputValidationCoverage.toFixed(1)}% - **Encryption Usage:** ${security.securityMetrics.encryptionUsage.toFixed(1)}% - **Rate Limiting Coverage:** ${security.securityMetrics.rateLimitingCoverage.toFixed(1)}% --- `; } generatePerformanceSection(performance2) { return `## \u26A1 Performance Analysis ### Performance Metrics - **Average Complexity:** ${performance2.metrics.averageComplexity.toFixed(1)} - **Average Lines of Code:** ${Math.round(performance2.metrics.averageLinesOfCode)} - **Total Dependencies:** ${performance2.metrics.totalDependencies} - **Blocking Operations:** ${performance2.metrics.blockingOperations} - **Async Operations:** ${performance2.metrics.asyncOperations} ### Performance Bottlenecks #### High Complexity Routes ${performance2.bottlenecks.highComplexityRoutes.length > 0 ? performance2.bottlenecks.highComplexityRoutes.map( (route) => `- **${route.path}** (Complexity: ${route.complexity}) ${route.recommendations.map((rec) => ` - ${rec}`).join("\n ")}` ).join("\n") : "- \u2705 No high complexity routes identified"} #### Large Routes (High LOC) ${performance2.bottlenecks.largeRoutes.length > 0 ? performance2.bottlenecks.largeRoutes.map( (route) => `- **${route.path}** (${route.linesOfCode} LOC) ${route.recommendations.map((rec) => ` - ${rec}`).join("\n ")}` ).join("\n") : "- \u2705 No overly large routes identified"} #### Dependency-Heavy Routes ${performance2.bottlenecks.dependencyHeavyRoutes.length > 0 ? performance2.bottlenecks.dependencyHeavyRoutes.map( (route) => `- **${route.path}** (${route.dependencies.length} dependencies) Dependencies: ${route.dependencies.join(", ")} ${route.recommendations.map((rec) => ` - ${rec}`).join("\n ")}` ).join("\n") : "- \u2705 No dependency-heavy routes identified"} ### Optimization Opportunities #### Caching Opportunities ${performance2.optimizationOpportunities.caching.map((opp) => `- ${opp}`).join("\n")} #### Async Optimization ${performance2.optimizationOpportunities.asyncOptimization.map((opp) => `- ${opp}`).join("\n")} #### Code Reduction ${performance2.optimizationOpportunities.codeReduction.map((opp) => `- ${opp}`).join("\n")} #### Dependency Optimization ${performance2.optimizationOpportunities.dependencyOptimization.map((opp) => `- ${opp}`).join("\n")} ### Performance Benchmarks - **Scalability Score:** ${performance2.benchmarks.scalabilityScore.toFixed(1)}% #### Estimated Response Times ${Object.entries(performance2.benchmarks.estimatedResponseTimes).map(([route, time]) => `- **${route}:** ${time}ms`).join("\n")} #### Memory Usage Estimates ${Object.entries(performance2.benchmarks.memoryUsageEstimates).map(([route, memory]) => `- **${route}:** ${(memory / 1024).toFixed(1)}KB`).join("\n")} --- `; } generateTrendsSection(trends) { return `## \u{1F4C8} Trends Analysis ### Time Range **Period:** ${trends.timeRange.start.toLocaleDateString()} - ${trends.timeRange.end.toLocaleDateString()} (${trends.timeRange.days} days) ### Trend Summary | Metric | Current | Previous | Change | Trend | |--------|---------|----------|--------|-------| | Routes | ${trends.trends.routes.current} | ${trends.trends.routes.previous} | ${trends.trends.routes.change >= 0 ? "+" : ""}${trends.trends.routes.change} | ${this.getTrendEmoji(trends.trends.routes.trend)} ${trends.trends.routes.trend} | | Security Score | ${trends.trends.security.current.toFixed(1)}% | ${trends.trends.security.previous.toFixed(1)}% | ${trends.trends.security.change >= 0 ? "+" : ""}${trends.trends.security.change.toFixed(1)}% | ${this.getTrendEmoji(trends.trends.security.trend)} ${trends.trends.security.trend} | | Performance Score | ${trends.trends.performance.current.toFixed(1)}% | ${trends.trends.performance.previous.toFixed(1)}% | ${trends.trends.performance.change >= 0 ? "+" : ""}${trends.trends.performance.change.toFixed(1)}% | ${this.getTrendEmoji(trends.trends.performance.trend)} ${trends.trends.performance.trend} | | Maintainability Score | ${trends.trends.maintainability.current.toFixed(1)}% | ${trends.trends.maintainability.previous.toFixed(1)}% | ${trends.trends.maintainability.change >= 0 ? "+" : ""}${trends.trends.maintainability.change.toFixed(1)}% | ${this.getTrendEmoji(trends.trends.maintainability.trend)} ${trends.trends.maintainability.trend} | ### Historical Data Points ${trends.historicalData.slice(-10).map( (data) => `- **${data.date.toLocaleDateString()}:** Routes: ${data.totalRoutes}, Security: ${data.securityScore.toFixed(1)}%, Performance: ${data.performanceScore.toFixed(1)}%, Maintainability: ${data.maintainabilityScore.toFixed(1)}%` ).join("\n")} ### Predictions (Next Month) - **