sicua
Version:
A tool for analyzing project structure and dependencies
1,027 lines (1,026 loc) • 37.2 kB
JavaScript
;
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;
}