UNPKG

sicua

Version:

A tool for analyzing project structure and dependencies

1,027 lines (1,026 loc) 37.2 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.scanDirectory = scanDirectory; exports.getFilePaths = getFilePaths; const fast_glob_1 = __importDefault(require("fast-glob")); const fs = __importStar(require("fs")); const path = __importStar(require("path")); const typescript_1 = __importDefault(require("typescript")); /** * Detect project type and structure by analyzing package.json */ async function detectProjectStructure(projectPath) { const packageJsonPath = path.join(projectPath, "package.json"); let projectInfo = { projectType: "react", hasSourceDirectory: false, sourceDirectory: projectPath, detectedDirectories: [], }; try { const packageContent = fs.readFileSync(packageJsonPath, "utf-8"); const packageJson = JSON.parse(packageContent); // Check for Next.js dependency const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies, }; const nextVersion = dependencies.next; if (nextVersion) { projectInfo.projectType = "nextjs"; projectInfo.nextjsVersion = nextVersion; // Parse version to determine router type const versionMatch = nextVersion.match(/(\d+)\.(\d+)/); if (versionMatch) { const major = parseInt(versionMatch[1]); const minor = parseInt(versionMatch[2]); // Next.js 13.4+ introduced stable app router if (major > 13 || (major === 13 && minor >= 4)) { projectInfo.routerType = "app"; } else { projectInfo.routerType = "pages"; } } else { // Default to pages for unparseable versions projectInfo.routerType = "pages"; } } } catch (error) { console.warn("Could not read package.json, assuming React project"); } // Detect actual directory structure const possibleDirectories = [ "src", "app", "pages", "components", "lib", "utils", ]; const detectedDirs = []; for (const dir of possibleDirectories) { const dirPath = path.join(projectPath, dir); try { const stat = fs.statSync(dirPath); if (stat.isDirectory()) { detectedDirs.push(dir); } } catch { // Directory doesn't exist, continue } } projectInfo.detectedDirectories = detectedDirs; // Determine source directory priority if (detectedDirs.includes("src")) { projectInfo.hasSourceDirectory = true; projectInfo.sourceDirectory = path.join(projectPath, "src"); } else if (projectInfo.projectType === "nextjs") { // For Next.js without src, use project root projectInfo.sourceDirectory = projectPath; } else { // For React projects without src, look for common patterns if (detectedDirs.includes("components")) { projectInfo.sourceDirectory = projectPath; } else { projectInfo.sourceDirectory = projectPath; } } return projectInfo; } /** * Generate scanning patterns based on detected project structure */ function generateScanningPatterns(projectInfo) { const rootConfigPatterns = [ "next.config.ts", "next.config.js", "next.config.mjs", "tailwind.config.ts", "tailwind.config.js", "tailwind.config.mjs", "package.json", ".env", ".env.local", ".env.development", ".env.production", "tsconfig.json", "eslint.config.ts", "eslint.config.js", ".eslintrc.js", ".eslintrc.json", "middleware.ts", "middleware.js", "instrumentation.ts", "instrumentation.js", ]; let sourcePatterns = []; let securityPatterns = []; if (projectInfo.projectType === "nextjs") { if (projectInfo.hasSourceDirectory) { // Next.js with src directory sourcePatterns = [ "src/**/*.{ts,tsx,js,jsx}", "src/app/**/*.{ts,tsx,js,jsx}", "src/pages/**/*.{ts,tsx,js,jsx}", "src/components/**/*.{ts,tsx,js,jsx}", "src/lib/**/*.{ts,tsx,js,jsx}", "src/utils/**/*.{ts,tsx,js,jsx}", "src/hooks/**/*.{ts,tsx,js,jsx}", "src/context/**/*.{ts,tsx,js,jsx}", "src/store/**/*.{ts,tsx,js,jsx}", ]; securityPatterns = [ "src/app/**/api/**/*.{ts,js}", "src/pages/api/**/*.{ts,js}", "src/app/**/auth/**/*.{ts,tsx,js,jsx}", "src/auth/**/*.{ts,tsx,js,jsx}", "src/config/**/*.{ts,js}", "src/security/**/*.{ts,tsx,js,jsx}", "src/**/*auth*.{ts,tsx,js,jsx}", "src/**/*middleware*.{ts,js}", "src/**/*config*.{ts,js}", ]; } else { // Next.js without src directory if (projectInfo.routerType === "app") { sourcePatterns = [ "app/**/*.{ts,tsx,js,jsx}", "components/**/*.{ts,tsx,js,jsx}", "lib/**/*.{ts,tsx,js,jsx}", "utils/**/*.{ts,tsx,js,jsx}", "hooks/**/*.{ts,tsx,js,jsx}", "context/**/*.{ts,tsx,js,jsx}", "store/**/*.{ts,tsx,js,jsx}", "types/**/*.{ts,tsx,js,jsx}", ]; securityPatterns = [ "app/**/api/**/*.{ts,js}", "app/**/auth/**/*.{ts,tsx,js,jsx}", "auth/**/*.{ts,tsx,js,jsx}", "config/**/*.{ts,js}", "security/**/*.{ts,tsx,js,jsx}", "**/*auth*.{ts,tsx,js,jsx}", "**/*middleware*.{ts,js}", "**/*config*.{ts,js}", ]; } else { // Pages router sourcePatterns = [ "pages/**/*.{ts,tsx,js,jsx}", "components/**/*.{ts,tsx,js,jsx}", "lib/**/*.{ts,tsx,js,jsx}", "utils/**/*.{ts,tsx,js,jsx}", "hooks/**/*.{ts,tsx,js,jsx}", "context/**/*.{ts,tsx,js,jsx}", "store/**/*.{ts,tsx,js,jsx}", "types/**/*.{ts,tsx,js,jsx}", ]; securityPatterns = [ "pages/api/**/*.{ts,js}", "pages/**/auth/**/*.{ts,tsx,js,jsx}", "auth/**/*.{ts,tsx,js,jsx}", "config/**/*.{ts,js}", "security/**/*.{ts,tsx,js,jsx}", "**/*auth*.{ts,tsx,js,jsx}", "**/*middleware*.{ts,js}", "**/*config*.{ts,js}", ]; } } } else { // Regular React project if (projectInfo.hasSourceDirectory) { sourcePatterns = [ "src/**/*.{ts,tsx,js,jsx}", "src/components/**/*.{ts,tsx,js,jsx}", "src/pages/**/*.{ts,tsx,js,jsx}", "src/views/**/*.{ts,tsx,js,jsx}", "src/lib/**/*.{ts,tsx,js,jsx}", "src/utils/**/*.{ts,tsx,js,jsx}", "src/hooks/**/*.{ts,tsx,js,jsx}", "src/context/**/*.{ts,tsx,js,jsx}", "src/store/**/*.{ts,tsx,js,jsx}", ]; } else { sourcePatterns = [ "components/**/*.{ts,tsx,js,jsx}", "pages/**/*.{ts,tsx,js,jsx}", "views/**/*.{ts,tsx,js,jsx}", "lib/**/*.{ts,tsx,js,jsx}", "utils/**/*.{ts,tsx,js,jsx}", "hooks/**/*.{ts,tsx,js,jsx}", "context/**/*.{ts,tsx,js,jsx}", "store/**/*.{ts,tsx,js,jsx}", "**/*.{ts,tsx,js,jsx}", ]; } securityPatterns = [ "**/auth/**/*.{ts,tsx,js,jsx}", "**/config/**/*.{ts,js}", "**/security/**/*.{ts,tsx,js,jsx}", "**/*auth*.{ts,tsx,js,jsx}", "**/*config*.{ts,js}", ]; } return { rootConfigPatterns, sourcePatterns, securityPatterns }; } /** * Process file metadata with security analysis */ function processFileMetadata(content, filePath) { const componentCount = countComponents(content); const securityPatterns = detectSecurityPatterns(content); return { hasReactImport: content.includes("import React") || content.includes('from "react"') || content.includes("from 'react'"), hasJSX: content.includes("<") && (filePath.endsWith(".tsx") || filePath.endsWith(".jsx")), hasTranslations: content.includes("useTranslation") || content.includes("useTranslations"), hasTypeDefinitions: content.includes("interface ") || content.includes("type ") || content.includes("enum "), isTest: filePath.includes(".test.") || filePath.includes(".spec.") || filePath.includes("__tests__"), componentCount, lastAnalyzed: Date.now(), // Security-related metadata hasSecurityPatterns: securityPatterns.hasSecurityPatterns, hasAuthenticationCode: securityPatterns.hasAuthenticationCode, hasAPIRoutes: securityPatterns.hasAPIRoutes, hasEnvironmentVariables: securityPatterns.hasEnvironmentVariables, hasCryptographicOperations: securityPatterns.hasCryptographicOperations, hasFileOperations: securityPatterns.hasFileOperations, hasDatabaseOperations: securityPatterns.hasDatabaseOperations, hasExternalAPICalls: securityPatterns.hasExternalAPICalls, securityRiskLevel: securityPatterns.securityRiskLevel, }; } /** * Detect security-relevant patterns in file content */ function detectSecurityPatterns(content) { const patterns = { hasSecurityPatterns: false, hasAuthenticationCode: false, hasAPIRoutes: false, hasEnvironmentVariables: false, hasCryptographicOperations: false, hasFileOperations: false, hasDatabaseOperations: false, hasExternalAPICalls: false, securityRiskLevel: "none", }; const securityKeywords = { auth: [ "auth", "login", "logout", "session", "token", "jwt", "oauth", "passport", "clerk", "nextauth", ], crypto: [ "crypto", "encrypt", "decrypt", "hash", "bcrypt", "scrypt", "pbkdf2", "aes", "rsa", ], database: [ "prisma", "mongoose", "sequelize", "knex", "sql", "query", "database", ], fileOps: ["fs.", "readFile", "writeFile", "path.join", "path.resolve"], apiCalls: ["fetch(", "axios", "request(", "http.", "https."], env: ["process.env", "dotenv", ".env"], dangerous: [ "eval(", "dangerouslySetInnerHTML", "innerHTML", "document.write", ], }; const lowerContent = content.toLowerCase(); // Check authentication patterns if (securityKeywords.auth.some((keyword) => lowerContent.includes(keyword))) { patterns.hasAuthenticationCode = true; patterns.hasSecurityPatterns = true; } // Check cryptographic operations if (securityKeywords.crypto.some((keyword) => lowerContent.includes(keyword))) { patterns.hasCryptographicOperations = true; patterns.hasSecurityPatterns = true; } // Check database operations if (securityKeywords.database.some((keyword) => lowerContent.includes(keyword))) { patterns.hasDatabaseOperations = true; patterns.hasSecurityPatterns = true; } // Check file operations if (securityKeywords.fileOps.some((keyword) => content.includes(keyword))) { patterns.hasFileOperations = true; patterns.hasSecurityPatterns = true; } // Check external API calls if (securityKeywords.apiCalls.some((keyword) => content.includes(keyword))) { patterns.hasExternalAPICalls = true; patterns.hasSecurityPatterns = true; } // Check environment variables if (securityKeywords.env.some((keyword) => content.includes(keyword))) { patterns.hasEnvironmentVariables = true; patterns.hasSecurityPatterns = true; } // Check API routes if (content.includes("export") && (content.includes("GET") || content.includes("POST") || content.includes("PUT") || content.includes("DELETE"))) { patterns.hasAPIRoutes = true; patterns.hasSecurityPatterns = true; } // Determine risk level const highRiskPatterns = securityKeywords.dangerous.some((keyword) => content.includes(keyword)); const mediumRiskCount = [ patterns.hasAuthenticationCode, patterns.hasCryptographicOperations, patterns.hasAPIRoutes, patterns.hasDatabaseOperations, ].filter(Boolean).length; if (highRiskPatterns) { patterns.securityRiskLevel = "high"; } else if (mediumRiskCount >= 2) { patterns.securityRiskLevel = "medium"; } else if (patterns.hasSecurityPatterns) { patterns.securityRiskLevel = "low"; } return patterns; } /** * Determine security file type based on file path and content */ function determineSecurityFileType(filePath, content) { const fileName = path.basename(filePath); const dirPath = path.dirname(filePath); // API routes if (dirPath.includes("/api/") || dirPath.includes("\\api\\")) { return "api_route"; } // Middleware if (fileName.includes("middleware") || fileName === "middleware.ts" || fileName === "middleware.js") { return "middleware"; } // Configuration files if ([ "next.config.js", "next.config.ts", "tailwind.config.js", "eslint.config.js", ].includes(fileName)) { return "config"; } // Environment files if (fileName.startsWith(".env")) { return "environment"; } // Auth-related files if (content.includes("auth") || content.includes("login") || content.includes("session")) { return "provider"; } // Utility files if (fileName.includes("util") || fileName.includes("helper") || dirPath.includes("utils") || dirPath.includes("helpers")) { return "utility"; } // Hooks if (fileName.startsWith("use") && (fileName.endsWith(".ts") || fileName.endsWith(".tsx"))) { return "hook"; } // Services if (fileName.includes("service") || dirPath.includes("services")) { return "service"; } // Constants if (fileName.includes("constant") || dirPath.includes("constants")) { return "constant"; } // Test files if (fileName.includes(".test.") || fileName.includes(".spec.") || dirPath.includes("__tests__")) { return "test"; } // Component files (default for React files) if (fileName.endsWith(".tsx") || fileName.endsWith(".jsx")) { return "component"; } // Type definitions if (fileName.endsWith(".d.ts") || fileName.includes("types")) { return "type_definition"; } return "utility"; } /** * Analyze file for security patterns */ function analyzeSecurityPatterns(content, filePath) { const patterns = []; const lines = content.split("\n"); // Common security patterns to detect const securityChecks = [ { pattern: /(?:password|secret|key|token)\s*[:=]\s*["'][\w\-+/=]{8,}["']/gi, type: "hardcoded_secret", severity: "critical", }, { pattern: /eval\s*\(/gi, type: "dangerous_function", severity: "high", }, { pattern: /dangerouslySetInnerHTML/gi, type: "unsafe_html", severity: "medium", }, { pattern: /Math\.random\(\)/gi, type: "weak_crypto", severity: "medium", }, { pattern: /localStorage\.(setItem|getItem)/gi, type: "insecure_storage", severity: "low", }, { pattern: /console\.(log|warn|error)/gi, type: "info_disclosure", severity: "low", }, ]; securityChecks.forEach((check) => { let match; while ((match = check.pattern.exec(content)) !== null) { const lineNumber = content.substring(0, match.index).split("\n").length; const lineContent = lines[lineNumber - 1] || ""; patterns.push({ patternType: check.type, pattern: match[0], lineNumber, context: lineContent.trim(), severity: check.severity, confidence: "high", }); } }); return patterns; } /** * Simple component counter for metadata */ function countComponents(content) { const componentPatterns = [ /export\s+default\s+function\s+[A-Z]/g, /export\s+function\s+[A-Z]/g, /const\s+[A-Z][a-zA-Z]*\s*=\s*\(/g, /function\s+[A-Z][a-zA-Z]*\s*\(/g, ]; let count = 0; componentPatterns.forEach((pattern) => { const matches = content.match(pattern); if (matches) count += matches.length; }); return Math.max(1, count); } /** * Main directory scan with complete security analysis and dynamic project structure detection */ async function scanDirectory(dir, config) { const scanStartTime = Date.now(); // Detect project structure first const projectInfo = await detectProjectStructure(dir); // Generate scanning patterns based on project structure const { rootConfigPatterns, sourcePatterns, securityPatterns } = generateScanningPatterns(projectInfo); // Scan for root config files const rootFiles = await (0, fast_glob_1.default)(rootConfigPatterns, { cwd: dir, absolute: true, onlyFiles: true, dot: true, followSymbolicLinks: false, }); // Scan for source and security files const sourceFiles = await (0, fast_glob_1.default)([...sourcePatterns, ...securityPatterns], { cwd: dir, absolute: true, ignore: [ "**/node_modules/**", "**/dist/**", "**/.git/**", "**/build/**", "**/.next/**", "**/coverage/**", "**/.nyc_output/**", "**/out/**", ], followSymbolicLinks: false, concurrency: 6, onlyFiles: true, }); // Combine and deduplicate const allFiles = [...new Set([...rootFiles, ...sourceFiles])]; // Update config with detected source directory if (config.srcDir !== projectInfo.sourceDirectory) { config.srcDir = projectInfo.sourceDirectory; } // Perform complete scan const scanResult = await performFullScan(allFiles, dir); // Calculate scan duration const scanDuration = Date.now() - scanStartTime; scanResult.securityScanMetadata.scanDuration = scanDuration; return scanResult; } /** * Perform complete directory scan with security analysis */ async function performFullScan(filePaths, projectDir) { const sourceFiles = new Map(); const fileContents = new Map(); const fileMetadata = new Map(); const securityFiles = []; const configFiles = []; const environmentFiles = []; const apiRoutes = []; const middlewareFiles = []; const packageInfo = []; // Separate parseable and config files const parseableExtensions = [".ts", ".tsx", ".js", ".jsx"]; const parseableFiles = filePaths.filter((filePath) => { const ext = path.extname(filePath).toLowerCase(); return parseableExtensions.includes(ext); }); const configOnlyFiles = filePaths.filter((filePath) => { const ext = path.extname(filePath).toLowerCase(); return !parseableExtensions.includes(ext); }); const batchSize = 50; // Process parseable files for AST/content parsing for (let i = 0; i < parseableFiles.length; i += batchSize) { const batch = parseableFiles.slice(i, i + batchSize); await Promise.all(batch.map(async (filePath) => { try { const content = fs.readFileSync(filePath, "utf-8"); fileContents.set(filePath, content); // Create TypeScript source file const sourceFile = typescript_1.default.createSourceFile(filePath, content, typescript_1.default.ScriptTarget.Latest, true); sourceFiles.set(filePath, sourceFile); // Extract metadata const metadata = processFileMetadata(content, filePath); fileMetadata.set(filePath, metadata); // Security analysis for parseable files if (metadata.hasSecurityPatterns) { const fileType = determineSecurityFileType(filePath, content); const patterns = analyzeSecurityPatterns(content, filePath); const securityFile = { filePath, fileType, securityRelevance: (metadata.securityRiskLevel === "none" ? "low" : metadata.securityRiskLevel), scanTimestamp: Date.now(), patterns, metadata: { hasSecrets: patterns.some((p) => p.patternType === "hardcoded_secret"), hasAuthCode: metadata.hasAuthenticationCode || false, hasValidation: content.includes("validate") || content.includes("schema"), hasCrypto: metadata.hasCryptographicOperations || false, hasFileOps: metadata.hasFileOperations || false, hasNetworkOps: metadata.hasExternalAPICalls || false, hasEval: patterns.some((p) => p.patternType === "dangerous_function"), hasDangerousHTML: patterns.some((p) => p.patternType === "unsafe_html"), packageDependencies: extractPackageDependencies(content), environmentAccess: extractEnvironmentAccess(content), externalConnections: extractExternalConnections(content), }, }; securityFiles.push(securityFile); // Categorize specific file types if (fileType === "api_route") { apiRoutes.push(analyzeAPIRoute(filePath, content)); } else if (fileType === "middleware") { middlewareFiles.push(analyzeMiddleware(filePath, content)); } } } catch (error) { console.error(`❌ Error processing file ${filePath}:`, error); } })); } // Process config files separately (no AST parsing) for (const filePath of configOnlyFiles) { try { const content = fs.readFileSync(filePath, "utf-8"); const fileName = path.basename(filePath); // Handle config files if (fileName.startsWith("next.config") || fileName === "package.json" || fileName.includes("config")) { configFiles.push(analyzeConfigFile(filePath, content)); } // Handle environment files if (fileName.startsWith(".env")) { environmentFiles.push(analyzeEnvironmentFile(filePath, content)); } } catch (error) { console.error(`❌ Error processing config file ${filePath}:`, error); } } // Analyze package.json for security information try { const packageJsonPath = path.join(projectDir, "package.json"); if (fs.existsSync(packageJsonPath)) { const packageContent = fs.readFileSync(packageJsonPath, "utf-8"); const packageJson = JSON.parse(packageContent); packageInfo.push(...analyzePackageJson(packageJson, packageJsonPath)); } } catch (error) { console.warn("Could not analyze package.json:", error); } return { filePaths: parseableFiles, // Only return parseable files for downstream processing sourceFiles, fileContents, fileMetadata, securityFiles, configFiles, environmentFiles, apiRoutes, middlewareFiles, packageInfo, securityScanMetadata: { scanTimestamp: Date.now(), scanDuration: 0, // Will be set by caller filesScanned: filePaths.length, // Total files scanned (including config) securityIssuesFound: securityFiles.reduce((total, file) => total + file.patterns.length, 0), riskLevel: calculateOverallRiskLevel(securityFiles), coveragePercentage: (securityFiles.length / filePaths.length) * 100, }, }; } /** * Helper functions for security analysis */ function extractPackageDependencies(content) { const importRegex = /from\s+['"]([^'"]+)['"]/g; const dependencies = []; let match; while ((match = importRegex.exec(content)) !== null) { const packageName = match[1]; if (!packageName.startsWith(".") && !packageName.startsWith("/")) { dependencies.push(packageName); } } return [...new Set(dependencies)]; } function extractEnvironmentAccess(content) { const envRegex = /process\.env\.(\w+)/g; const variables = []; let match; while ((match = envRegex.exec(content)) !== null) { variables.push(match[1]); } return [...new Set(variables)]; } function extractExternalConnections(content) { const urlRegex = /https?:\/\/[^\s'"]+/g; const urls = []; let match; while ((match = urlRegex.exec(content)) !== null) { urls.push(match[0]); } return [...new Set(urls)]; } function analyzeAPIRoute(filePath, content) { return { filePath, route: extractRouteFromPath(filePath), method: extractHTTPMethods(content), handlerFunctions: extractHandlerFunctions(content), middleware: [], authenticationRequired: content.includes("auth") || content.includes("token"), validationPresent: content.includes("validate") || content.includes("schema"), inputSources: extractInputSources(content), databaseAccess: content.includes("prisma") || content.includes("db.") || content.includes("query"), externalAPICalls: content.includes("fetch") || content.includes("axios"), securityHeaders: [], errorHandling: content.includes("try") && content.includes("catch") ? ["try-catch"] : [], scanTimestamp: Date.now(), }; } function analyzeMiddleware(filePath, content) { return { filePath, middlewareType: determineMiddlewareType(content), appliesTo: [], securityFunctions: extractSecurityFunctions(content), configurationOptions: {}, dependencies: extractPackageDependencies(content), scanTimestamp: Date.now(), }; } function analyzeConfigFile(filePath, content) { return { filePath, configType: determineConfigFileType(filePath), parsedConfig: {}, securitySettings: [], missingSecuritySettings: [], scanTimestamp: Date.now(), }; } function analyzeEnvironmentFile(filePath, content) { const lines = content.split("\n"); const variables = lines .filter((line) => line.includes("=") && !line.trim().startsWith("#")) .map((line) => { const [name] = line.split("="); return { name: name.trim(), isSensitive: isSensitiveVariable(name.trim()), usageLocations: [], exposureRisk: "server", }; }); return { filePath, envType: determineEnvType(filePath), variables, securityIssues: [], scanTimestamp: Date.now(), }; } function analyzePackageJson(packageJson, filePath) { const packages = []; const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies, }; for (const [name, version] of Object.entries(dependencies)) { packages.push({ name, version: version, securityCategory: categorizePackage(name), vulnerabilities: [], usageLocations: [], configurationFiles: [], securityFeatures: [], riskAssessment: { overallRisk: "low", riskFactors: [], mitigations: [], recommendedActions: [], }, }); } return packages; } /** * Helper functions for analysis */ function calculateOverallRiskLevel(securityFiles) { const hasCritical = securityFiles.some((f) => f.patterns.some((p) => p.severity === "critical")); const hasHigh = securityFiles.some((f) => f.patterns.some((p) => p.severity === "high")); const hasMedium = securityFiles.some((f) => f.patterns.some((p) => p.severity === "medium")); if (hasCritical) return "critical"; if (hasHigh) return "high"; if (hasMedium) return "medium"; return "low"; } function extractRouteFromPath(filePath) { const apiIndex = filePath.indexOf("/api/"); if (apiIndex !== -1) { return filePath.substring(apiIndex + 4).replace(/\.(ts|js|tsx|jsx)$/, ""); } return ""; } function extractHTTPMethods(content) { const methods = []; if (content.includes("export async function GET") || content.includes("export function GET")) methods.push("GET"); if (content.includes("export async function POST") || content.includes("export function POST")) methods.push("POST"); if (content.includes("export async function PUT") || content.includes("export function PUT")) methods.push("PUT"); if (content.includes("export async function DELETE") || content.includes("export function DELETE")) methods.push("DELETE"); if (content.includes("export async function PATCH") || content.includes("export function PATCH")) methods.push("PATCH"); return methods; } function extractHandlerFunctions(content) { const handlerRegex = /export\s+(?:async\s+)?function\s+(\w+)/g; const handlers = []; let match; while ((match = handlerRegex.exec(content)) !== null) { handlers.push(match[1]); } return handlers; } function extractInputSources(content) { const sources = []; if (content.includes("request.body") || content.includes("req.body")) sources.push("body"); if (content.includes("request.query") || content.includes("req.query")) sources.push("query"); if (content.includes("request.params") || content.includes("req.params")) sources.push("params"); if (content.includes("request.headers") || content.includes("req.headers")) sources.push("headers"); if (content.includes("request.cookies") || content.includes("req.cookies")) sources.push("cookies"); return sources; } function determineMiddlewareType(content) { if (content.includes("auth") || content.includes("login")) return "auth"; if (content.includes("cors")) return "cors"; if (content.includes("helmet") || content.includes("security")) return "security"; if (content.includes("log")) return "logging"; if (content.includes("validate")) return "validation"; return "custom"; } function extractSecurityFunctions(content) { const securityFunctions = []; if (content.includes("authenticate")) securityFunctions.push("authenticate"); if (content.includes("authorize")) securityFunctions.push("authorize"); if (content.includes("validate")) securityFunctions.push("validate"); if (content.includes("sanitize")) securityFunctions.push("sanitize"); if (content.includes("rateLimit")) securityFunctions.push("rateLimit"); return securityFunctions; } function determineConfigFileType(filePath) { const fileName = path.basename(filePath); if (fileName.startsWith("next.config")) return "next_config"; if (fileName === "package.json") return "package_json"; if (fileName.startsWith("tsconfig")) return "tsconfig"; if (fileName.includes("eslint")) return "eslint_config"; if (fileName.startsWith(".env")) return "env_config"; if (fileName.includes("docker")) return "docker_config"; if (fileName.includes("vercel")) return "vercel_config"; if (fileName.includes("webpack")) return "webpack_config"; if (fileName.includes("babel")) return "babel_config"; if (fileName.includes("tailwind")) return "tailwind_config"; if (fileName.includes("jest")) return "jest_config"; return "next_config"; } function determineEnvType(filePath) { const fileName = path.basename(filePath); if (fileName.includes(".dev") || fileName.includes(".development")) return "development"; if (fileName.includes(".prod") || fileName.includes(".production")) return "production"; if (fileName.includes(".test")) return "test"; return "unknown"; } function isSensitiveVariable(name) { const sensitivePatterns = [ "password", "secret", "key", "token", "api_key", "private", "auth", ]; return sensitivePatterns.some((pattern) => name.toLowerCase().includes(pattern)); } function categorizePackage(packageName) { const authPackages = [ "next-auth", "@auth0/auth0-react", "clerk", "passport", "jsonwebtoken", ]; const cryptoPackages = ["bcrypt", "crypto-js", "node-forge", "jose"]; const validationPackages = ["zod", "joi", "yup", "ajv"]; const databasePackages = ["prisma", "mongoose", "sequelize", "typeorm"]; const paymentPackages = ["stripe", "@stripe/stripe-js", "paypal"]; if (authPackages.some((pkg) => packageName.includes(pkg))) return "authentication"; if (cryptoPackages.some((pkg) => packageName.includes(pkg))) return "cryptography"; if (validationPackages.some((pkg) => packageName.includes(pkg))) return "validation"; if (databasePackages.some((pkg) => packageName.includes(pkg))) return "database"; if (paymentPackages.some((pkg) => packageName.includes(pkg))) return "payment"; return "unknown"; } /** * Get file paths only, for compatibility with existing code that needs just paths */ async function getFilePaths(dir, config) { const scanResult = await scanDirectory(dir, config); return scanResult.filePaths; }