next-api-analyzer
Version:
Next.js API routes analyzer for security, performance, and maintainability
1,285 lines (1,219 loc) • 161 kB
JavaScript
#!/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)
- **