UNPKG

nextjs-analyzer

Version:

A modular tool that comprehensively analyzes Next.js projects. Includes component, performance, security, SEO, data fetching, code quality, and historical analysis features.

730 lines (649 loc) 29.8 kB
const fs = require('fs-extra'); const path = require('path'); const { findFiles, getRelativePath, i18n } = require('../../utils'); /** * Güvenlik Analizi Modülü */ module.exports = { name: i18n.t('modules.security.name'), description: i18n.t('modules.security.description'), /** * Analiz işlemini gerçekleştirir * @param {NextJsAnalyzer} analyzer - Analyzer instance * @param {Object} options - Analiz seçenekleri * @returns {Object} - Analiz sonuçları */ async analyze(analyzer, options) { // Server component güvenlik kontrolü const serverComponentSecurity = await this.checkServerComponentSecurity(analyzer); // API route güvenlik kontrolü const apiRouteSecurity = await this.checkApiRouteSecurity(analyzer); // Genel güvenlik kontrolü const generalSecurity = await this.checkGeneralSecurity(analyzer); return { results: { serverComponentSecurity, apiRouteSecurity, generalSecurity }, metadata: { totalIssues: serverComponentSecurity.issues.length + apiRouteSecurity.issues.length + generalSecurity.issues.length, criticalIssues: serverComponentSecurity.issues.filter(i => i.severity === 'critical').length + apiRouteSecurity.issues.filter(i => i.severity === 'critical').length + generalSecurity.issues.filter(i => i.severity === 'critical').length, highIssues: serverComponentSecurity.issues.filter(i => i.severity === 'high').length + apiRouteSecurity.issues.filter(i => i.severity === 'high').length + generalSecurity.issues.filter(i => i.severity === 'high').length, mediumIssues: serverComponentSecurity.issues.filter(i => i.severity === 'medium').length + apiRouteSecurity.issues.filter(i => i.severity === 'medium').length + generalSecurity.issues.filter(i => i.severity === 'medium').length, lowIssues: serverComponentSecurity.issues.filter(i => i.severity === 'low').length + apiRouteSecurity.issues.filter(i => i.severity === 'low').length + generalSecurity.issues.filter(i => i.severity === 'low').length } }; }, /** * Server component güvenlik kontrolü yapar * @param {NextJsAnalyzer} analyzer - Analyzer instance * @returns {Object} - Server component güvenlik sonuçları */ async checkServerComponentSecurity(analyzer) { const issues = []; // Server component'leri bul const serverComponents = Array.from(analyzer.components.entries()) .filter(([_, component]) => component.type === 'server') .map(([filePath, _]) => filePath); for (const filePath of serverComponents) { try { const content = fs.readFileSync(filePath, 'utf8'); const relativePath = getRelativePath(filePath, analyzer.projectPath); // Doğrudan process.env kullanımı if (content.includes('process.env') && !content.includes('process.env.NODE_ENV')) { const matches = content.match(/process\.env\.([A-Z0-9_]+)/g) || []; const envVars = matches.map(match => match.replace('process.env.', '')); if (envVars.some(v => v.includes('SECRET') || v.includes('KEY') || v.includes('TOKEN') || v.includes('PASSWORD'))) { issues.push({ file: relativePath, issue: i18n.t('modules.security.serverComponent.issues.sensitiveEnvVars'), severity: 'high', recommendation: i18n.t('modules.security.serverComponent.recommendations.sensitiveEnvVars') }); } } // SQL injection riski if ((content.includes('sql') || content.includes('query') || content.includes('SELECT') || content.includes('INSERT')) && (content.includes('${') || content.includes("${") || content.includes('`') || content.includes("'"))) { issues.push({ file: relativePath, issue: i18n.t('modules.security.serverComponent.issues.sqlInjection'), severity: 'critical', recommendation: i18n.t('modules.security.serverComponent.recommendations.sqlInjection') }); } // Dosya sistemi erişimi if ((content.includes('fs.') || content.includes('readFile') || content.includes('writeFile')) && (content.includes('req.') || content.includes('params.') || content.includes('query.'))) { issues.push({ file: relativePath, issue: i18n.t('modules.security.serverComponent.issues.fileSystemAccess'), severity: 'critical', recommendation: i18n.t('modules.security.serverComponent.recommendations.fileSystemAccess') }); } // eval kullanımı if (content.includes('eval(') || content.includes('new Function(')) { issues.push({ file: relativePath, issue: i18n.t('modules.security.serverComponent.issues.evalUsage'), severity: 'critical', recommendation: i18n.t('modules.security.serverComponent.recommendations.evalUsage') }); } } catch (error) { // Dosya okunamadıysa devam et continue; } } return { issues, recommendations: [ { title: i18n.t('modules.security.serverComponent.recommendations.title'), description: i18n.t('modules.security.serverComponent.recommendations.description') }, { title: i18n.t('modules.security.serverComponent.recommendations.envVars.title'), description: i18n.t('modules.security.serverComponent.recommendations.envVars.description') }, { title: i18n.t('modules.security.serverComponent.recommendations.dataValidation.title'), description: i18n.t('modules.security.serverComponent.recommendations.dataValidation.description') } ] }; }, /** * API route güvenlik kontrolü yapar * @param {NextJsAnalyzer} analyzer - Analyzer instance * @returns {Object} - API route güvenlik sonuçları */ async checkApiRouteSecurity(analyzer) { const issues = []; // API route'ları bul const apiRoutes = []; // Pages Router API routes if (analyzer.pagesDir) { const apiDir = path.join(analyzer.pagesDir, 'api'); if (fs.existsSync(apiDir)) { apiRoutes.push(...findFiles(apiDir)); } } // App Router API routes (route.js files) if (analyzer.appDir) { const appFiles = findFiles(analyzer.appDir); apiRoutes.push(...appFiles.filter(file => path.basename(file) === 'route.js' || path.basename(file) === 'route.ts')); } for (const filePath of apiRoutes) { try { const content = fs.readFileSync(filePath, 'utf8'); const relativePath = getRelativePath(filePath, analyzer.projectPath); // CORS kontrolü if (!content.includes('Access-Control-Allow-Origin') && !content.includes('cors') && !content.includes('next-cors')) { issues.push({ file: relativePath, issue: i18n.t('modules.security.apiRoute.issues.corsConfig'), severity: 'medium', recommendation: i18n.t('modules.security.apiRoute.recommendations.corsConfig') }); } else if (content.includes('Access-Control-Allow-Origin: *') || content.includes("Access-Control-Allow-Origin', '*'") || content.includes('origin: "*"') || content.includes("origin: '*'")) { issues.push({ file: relativePath, issue: i18n.t('modules.security.apiRoute.issues.corsWildcard'), severity: 'medium', recommendation: i18n.t('modules.security.apiRoute.recommendations.corsWildcard') }); } // Rate limiting kontrolü if (!content.includes('rate-limit') && !content.includes('rateLimit') && !content.includes('throttle') && !content.includes('limiter')) { issues.push({ file: relativePath, issue: i18n.t('modules.security.apiRoute.issues.rateLimiting'), severity: 'medium', recommendation: i18n.t('modules.security.apiRoute.recommendations.rateLimiting') }); } // Authentication kontrolü if ((content.includes('delete') || content.includes('update') || content.includes('create') || content.includes('DELETE') || content.includes('PUT') || content.includes('POST')) && (!content.includes('auth') && !content.includes('session') && !content.includes('token') && !content.includes('jwt') && !content.includes('cookie'))) { issues.push({ file: relativePath, issue: i18n.t('modules.security.apiRoute.issues.authentication'), severity: 'high', recommendation: i18n.t('modules.security.apiRoute.recommendations.authentication') }); } // Input validation kontrolü if ((content.includes('req.body') || content.includes('req.query') || content.includes('req.params')) && (!content.includes('validate') && !content.includes('schema') && !content.includes('joi') && !content.includes('yup') && !content.includes('zod'))) { issues.push({ file: relativePath, issue: i18n.t('modules.security.apiRoute.issues.inputValidation'), severity: 'high', recommendation: i18n.t('modules.security.apiRoute.recommendations.inputValidation') }); } // HTTP method kontrolü if (!content.includes('req.method') && !content.includes('request.method')) { issues.push({ file: relativePath, issue: i18n.t('modules.security.apiRoute.issues.httpMethod'), severity: 'medium', recommendation: i18n.t('modules.security.apiRoute.recommendations.httpMethod') }); } } catch (error) { // Dosya okunamadıysa devam et continue; } } return { issues, recommendations: [ { title: i18n.t('modules.security.apiRoute.recommendations.title'), description: i18n.t('modules.security.apiRoute.recommendations.description') }, { title: i18n.t('modules.security.apiRoute.recommendations.cors.title'), description: i18n.t('modules.security.apiRoute.recommendations.cors.description') }, { title: i18n.t('modules.security.apiRoute.recommendations.rateLimiting.title'), description: i18n.t('modules.security.apiRoute.recommendations.rateLimiting.description') }, { title: i18n.t('modules.security.apiRoute.recommendations.auth.title'), description: i18n.t('modules.security.apiRoute.recommendations.auth.description') }, { title: i18n.t('modules.security.apiRoute.recommendations.inputValidation.title'), description: i18n.t('modules.security.apiRoute.recommendations.inputValidation.description') } ] }; }, /** * Genel güvenlik kontrolü yapar * @param {NextJsAnalyzer} analyzer - Analyzer instance * @returns {Object} - Genel güvenlik sonuçları */ async checkGeneralSecurity(analyzer) { const issues = []; // package.json kontrolü const packageJsonPath = path.join(analyzer.projectPath, 'package.json'); if (fs.existsSync(packageJsonPath)) { try { const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); // Bağımlılıkları kontrol et const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies }; // Eski Next.js sürümü if (dependencies.next) { const nextVersion = dependencies.next.replace('^', '').replace('~', ''); const majorVersion = parseInt(nextVersion.split('.')[0]); if (majorVersion < 12) { issues.push({ file: 'package.json', issue: i18n.t('modules.security.general.issues.oldNextVersion', { version: dependencies.next }), severity: 'high', recommendation: i18n.t('modules.security.general.recommendations.oldNextVersion') }); } } // Güvensiz paketler const insecurePackages = { 'serialize-javascript': { maxVersion: '3.1.0', severity: 'high' }, 'lodash': { maxVersion: '4.17.19', severity: 'medium' }, 'axios': { maxVersion: '0.21.1', severity: 'medium' }, 'node-fetch': { maxVersion: '2.6.1', severity: 'medium' }, 'minimist': { maxVersion: '1.2.5', severity: 'medium' } }; for (const [pkg, info] of Object.entries(insecurePackages)) { if (dependencies[pkg]) { const version = dependencies[pkg].replace('^', '').replace('~', ''); const versionParts = version.split('.').map(Number); const maxVersionParts = info.maxVersion.split('.').map(Number); let isVulnerable = false; for (let i = 0; i < versionParts.length; i++) { if (versionParts[i] < maxVersionParts[i]) { isVulnerable = true; break; } else if (versionParts[i] > maxVersionParts[i]) { break; } } if (isVulnerable) { issues.push({ file: 'package.json', issue: i18n.t('modules.security.general.issues.insecurePackage', { package: pkg, version: version }), severity: info.severity, recommendation: i18n.t('modules.security.general.recommendations.insecurePackage', { package: pkg, minVersion: info.maxVersion }) }); } } } } catch (error) { // package.json okunamadıysa devam et } } // .env dosyalarını kontrol et const envFiles = ['.env', '.env.local', '.env.development', '.env.production']; for (const envFile of envFiles) { const envPath = path.join(analyzer.projectPath, envFile); if (fs.existsSync(envPath)) { try { const content = fs.readFileSync(envPath, 'utf8'); // .env dosyası .gitignore'da mı? const gitignorePath = path.join(analyzer.projectPath, '.gitignore'); let isIgnored = false; if (fs.existsSync(gitignorePath)) { const gitignore = fs.readFileSync(gitignorePath, 'utf8'); isIgnored = gitignore.split('\n').some(line => line.trim() === envFile || line.trim() === '*.env' || line.trim() === '.env*' ); } if (!isIgnored && envFile !== '.env.example' && envFile !== '.env.sample') { issues.push({ file: envFile, issue: i18n.t('modules.security.general.issues.envNotIgnored', { file: envFile }), severity: 'critical', recommendation: i18n.t('modules.security.general.recommendations.envNotIgnored', { file: envFile }) }); } // Hassas bilgiler const sensitiveKeys = ['SECRET', 'KEY', 'TOKEN', 'PASSWORD', 'CREDENTIAL']; const lines = content.split('\n'); for (const line of lines) { if (line.trim() && !line.startsWith('#')) { const [key, value] = line.split('='); if (key && value && sensitiveKeys.some(sk => key.includes(sk))) { issues.push({ file: envFile, issue: i18n.t('modules.security.general.issues.sensitiveEnvVar', { key: key }), severity: 'medium', recommendation: i18n.t('modules.security.general.recommendations.sensitiveEnvVar', { file: envFile }) }); } } } } catch (error) { // .env dosyası okunamadıysa devam et } } } // next.config.js kontrolü const nextConfigPath = path.join(analyzer.projectPath, 'next.config.js'); if (fs.existsSync(nextConfigPath)) { try { const content = fs.readFileSync(nextConfigPath, 'utf8'); // Content Security Policy kontrolü if (!content.includes('Content-Security-Policy') && !content.includes('contentSecurityPolicy')) { issues.push({ file: 'next.config.js', issue: i18n.t('modules.security.general.issues.cspMissing'), severity: 'medium', recommendation: i18n.t('modules.security.general.recommendations.cspMissing') }); } // Güvensiz external scripts if (content.includes('dangerouslyAllowSVG') || content.includes('dangerouslyAllowHTML')) { issues.push({ file: 'next.config.js', issue: i18n.t('modules.security.general.issues.unsafeConfig'), severity: 'medium', recommendation: i18n.t('modules.security.general.recommendations.unsafeConfig') }); } } catch (error) { // next.config.js okunamadıysa devam et } } return { issues, recommendations: [ { title: i18n.t('modules.security.general.recommendations.dependencies.title'), description: i18n.t('modules.security.general.recommendations.dependencies.description') }, { title: i18n.t('modules.security.general.recommendations.envSecurity.title'), description: i18n.t('modules.security.general.recommendations.envSecurity.description') }, { title: i18n.t('modules.security.general.recommendations.csp.title'), description: i18n.t('modules.security.general.recommendations.csp.description') }, { title: i18n.t('modules.security.general.recommendations.safeConfig.title'), description: i18n.t('modules.security.general.recommendations.safeConfig.description') }, { title: i18n.t('modules.security.general.recommendations.securityAudits.title'), description: i18n.t('modules.security.general.recommendations.securityAudits.description') } ] }; }, /** * Görselleştirme fonksiyonları */ visualize: { /** * Metin formatında görselleştirme * @param {Object} results - Analiz sonuçları * @returns {string} - Metin formatında görselleştirme */ text(results) { let output = `# ${i18n.t('modules.security.visualize.title')}\n\n`; // Özet output += `## ${i18n.t('modules.security.visualize.summary')}\n\n`; output += `${i18n.t('modules.security.visualize.totalIssues', { count: results.metadata.totalIssues })}:\n`; output += `- ${i18n.t('modules.security.visualize.criticalIssues')}: ${results.metadata.criticalIssues}\n`; output += `- ${i18n.t('modules.security.visualize.highIssues')}: ${results.metadata.highIssues}\n`; output += `- ${i18n.t('modules.security.visualize.mediumIssues')}: ${results.metadata.mediumIssues}\n`; output += `- ${i18n.t('modules.security.visualize.lowIssues')}: ${results.metadata.lowIssues}\n\n`; // Server Component Güvenliği output += `## ${i18n.t('modules.security.serverComponent.title')}\n\n`; if (results.results.serverComponentSecurity.issues.length === 0) { output += `${i18n.t('modules.security.serverComponent.noIssues')}\n\n`; } else { output += `### ${i18n.t('modules.security.visualize.detectedIssues')}\n\n`; results.results.serverComponentSecurity.issues.forEach(issue => { output += `- **${issue.file}** (${issue.severity.toUpperCase()})\n`; output += ` - ${i18n.t('modules.security.visualize.issue')}: ${issue.issue}\n`; output += ` - ${i18n.t('modules.security.visualize.recommendation')}: ${issue.recommendation}\n\n`; }); } output += `### ${i18n.t('modules.security.visualize.recommendations')}\n\n`; results.results.serverComponentSecurity.recommendations.forEach(recommendation => { output += `- **${recommendation.title}**\n`; output += ` - ${recommendation.description}\n\n`; }); // API Route Güvenliği output += `## ${i18n.t('modules.security.apiRoute.title')}\n\n`; if (results.results.apiRouteSecurity.issues.length === 0) { output += `${i18n.t('modules.security.apiRoute.noIssues')}\n\n`; } else { output += `### ${i18n.t('modules.security.visualize.detectedIssues')}\n\n`; results.results.apiRouteSecurity.issues.forEach(issue => { output += `- **${issue.file}** (${issue.severity.toUpperCase()})\n`; output += ` - ${i18n.t('modules.security.visualize.issue')}: ${issue.issue}\n`; output += ` - ${i18n.t('modules.security.visualize.recommendation')}: ${issue.recommendation}\n\n`; }); } output += `### ${i18n.t('modules.security.visualize.recommendations')}\n\n`; results.results.apiRouteSecurity.recommendations.forEach(recommendation => { output += `- **${recommendation.title}**\n`; output += ` - ${recommendation.description}\n\n`; }); // Genel Güvenlik output += `## ${i18n.t('modules.security.general.title')}\n\n`; if (results.results.generalSecurity.issues.length === 0) { output += `${i18n.t('modules.security.general.noIssues')}\n\n`; } else { output += `### ${i18n.t('modules.security.visualize.detectedIssues')}\n\n`; results.results.generalSecurity.issues.forEach(issue => { output += `- **${issue.file}** (${issue.severity.toUpperCase()})\n`; output += ` - ${i18n.t('modules.security.visualize.issue')}: ${issue.issue}\n`; output += ` - ${i18n.t('modules.security.visualize.recommendation')}: ${issue.recommendation}\n\n`; }); } output += `### ${i18n.t('modules.security.visualize.recommendations')}\n\n`; results.results.generalSecurity.recommendations.forEach(recommendation => { output += `- **${recommendation.title}**\n`; output += ` - ${recommendation.description}\n\n`; }); return output; }, /** * HTML formatında görselleştirme * @param {Object} results - Analiz sonuçları * @returns {string} - HTML formatında görselleştirme */ html(results) { let html = ` <div class="security-container"> <h2>${i18n.t('modules.security.visualize.title')}</h2> <!-- Özet --> <div class="section"> <h3>${i18n.t('modules.security.visualize.summary')}</h3> <div class="summary"> <p>${i18n.t('modules.security.visualize.totalIssues', { count: results.metadata.totalIssues })}:</p> <ul class="summary-list"> <li class="severity-critical">${i18n.t('modules.security.visualize.criticalIssues')}: ${results.metadata.criticalIssues}</li> <li class="severity-high">${i18n.t('modules.security.visualize.highIssues')}: ${results.metadata.highIssues}</li> <li class="severity-medium">${i18n.t('modules.security.visualize.mediumIssues')}: ${results.metadata.mediumIssues}</li> <li class="severity-low">${i18n.t('modules.security.visualize.lowIssues')}: ${results.metadata.lowIssues}</li> </ul> </div> </div> <!-- Server Component Güvenliği --> <div class="section"> <h3>${i18n.t('modules.security.serverComponent.title')}</h3>`; if (results.results.serverComponentSecurity.issues.length === 0) { html += ` <div class="success-message"> <p>✅ ${i18n.t('modules.security.serverComponent.noIssues')}</p> </div>`; } else { html += ` <div class="subsection"> <h4>${i18n.t('modules.security.visualize.detectedIssues')}</h4> <ul class="issue-list">`; results.results.serverComponentSecurity.issues.forEach(issue => { html += ` <li class="issue-item severity-${issue.severity}"> <div class="issue-header"> <span class="issue-file">${issue.file}</span> <span class="issue-severity">${issue.severity.toUpperCase()}</span> </div> <div class="issue-description">${issue.issue}</div> <div class="issue-recommendation">${issue.recommendation}</div> </li>`; }); html += ` </ul> </div>`; } html += ` <div class="subsection"> <h4>${i18n.t('modules.security.visualize.recommendations')}</h4> <ul class="recommendation-list">`; results.results.serverComponentSecurity.recommendations.forEach(recommendation => { html += ` <li class="recommendation-item"> <div class="recommendation-title">${recommendation.title}</div> <div class="recommendation-description">${recommendation.description}</div> </li>`; }); html += ` </ul> </div> </div> <!-- API Route Güvenliği --> <div class="section"> <h3>${i18n.t('modules.security.apiRoute.title')}</h3>`; if (results.results.apiRouteSecurity.issues.length === 0) { html += ` <div class="success-message"> <p>✅ ${i18n.t('modules.security.apiRoute.noIssues')}</p> </div>`; } else { html += ` <div class="subsection"> <h4>${i18n.t('modules.security.visualize.detectedIssues')}</h4> <ul class="issue-list">`; results.results.apiRouteSecurity.issues.forEach(issue => { html += ` <li class="issue-item severity-${issue.severity}"> <div class="issue-header"> <span class="issue-file">${issue.file}</span> <span class="issue-severity">${issue.severity.toUpperCase()}</span> </div> <div class="issue-description">${issue.issue}</div> <div class="issue-recommendation">${issue.recommendation}</div> </li>`; }); html += ` </ul> </div>`; } html += ` <div class="subsection"> <h4>${i18n.t('modules.security.visualize.recommendations')}</h4> <ul class="recommendation-list">`; results.results.apiRouteSecurity.recommendations.forEach(recommendation => { html += ` <li class="recommendation-item"> <div class="recommendation-title">${recommendation.title}</div> <div class="recommendation-description">${recommendation.description}</div> </li>`; }); html += ` </ul> </div> </div> <!-- Genel Güvenlik --> <div class="section"> <h3>${i18n.t('modules.security.general.title')}</h3>`; if (results.results.generalSecurity.issues.length === 0) { html += ` <div class="success-message"> <p>✅ ${i18n.t('modules.security.general.noIssues')}</p> </div>`; } else { html += ` <div class="subsection"> <h4>${i18n.t('modules.security.visualize.detectedIssues')}</h4> <ul class="issue-list">`; results.results.generalSecurity.issues.forEach(issue => { html += ` <li class="issue-item severity-${issue.severity}"> <div class="issue-header"> <span class="issue-file">${issue.file}</span> <span class="issue-severity">${issue.severity.toUpperCase()}</span> </div> <div class="issue-description">${issue.issue}</div> <div class="issue-recommendation">${issue.recommendation}</div> </li>`; }); html += ` </ul> </div>`; } html += ` <div class="subsection"> <h4>${i18n.t('modules.security.visualize.recommendations')}</h4> <ul class="recommendation-list">`; results.results.generalSecurity.recommendations.forEach(recommendation => { html += ` <li class="recommendation-item"> <div class="recommendation-title">${recommendation.title}</div> <div class="recommendation-description">${recommendation.description}</div> </li>`; }); html += ` </ul> </div> </div> </div>`; return html; }, /** * JSON formatında görselleştirme * @param {Object} results - Analiz sonuçları * @returns {string} - JSON formatında görselleştirme */ json(results) { return JSON.stringify(results, null, 2); } } };