UNPKG

bugnitor-security-scanner

Version:

AI-Era Security Scanner: Intelligent automated security review agent specializing in AI-generated vulnerability patterns

605 lines • 28.3 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 () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.SecurityScanner = void 0; const fs = __importStar(require("fs")); const path = __importStar(require("path")); const glob_1 = require("glob"); const secrets_1 = require("./secrets"); const enhanced_secrets_1 = require("./enhanced-secrets"); const dangerous_api_detector_1 = require("./dangerous-api-detector"); const code_quality_analyzer_1 = require("./code-quality-analyzer"); const ai_vulnerability_detector_1 = require("./ai-vulnerability-detector"); const vulnerabilities_1 = require("./vulnerabilities"); const advanced_vulnerabilities_1 = require("./advanced-vulnerabilities"); const dependency_analyzer_1 = require("./dependency-analyzer"); const cicd_analyzer_1 = require("./cicd-analyzer"); const security_grader_1 = require("./security-grader"); class SecurityScanner { constructor() { this.defaultExcludePatterns = [ 'node_modules/**', '.git/**', 'dist/**', 'build/**', '*.min.js', '*.map', '.env.example', 'package-lock.json', 'yarn.lock', '*.log' ]; this.dependencyAnalyzer = new dependency_analyzer_1.DependencyAnalyzer(); this.cicdAnalyzer = new cicd_analyzer_1.CICDAnalyzer(); this.securityGrader = new security_grader_1.SecurityGrader(); this.enhancedSecretDetector = new enhanced_secrets_1.EnhancedSecretDetector(); this.dangerousAPIDetector = new dangerous_api_detector_1.DangerousAPIDetector(); this.codeQualityAnalyzer = new code_quality_analyzer_1.CodeQualityAnalyzer(); this.aiVulnerabilityDetector = new ai_vulnerability_detector_1.AIVulnerabilityDetector(); } getLineNumber(content, index) { return content.substring(0, index).split('\n').length; } getColumnNumber(content, index) { const lines = content.substring(0, index).split('\n'); return lines[lines.length - 1].length + 1; } async scan(options) { const findings = []; const fileAnalyses = []; const excludePatterns = [...this.defaultExcludePatterns, ...(options.excludePatterns || [])]; const pattern = options.includePatterns?.length ? `{${options.includePatterns.join(',')}}` : '**/*'; const files = await (0, glob_1.glob)(pattern, { cwd: options.path, absolute: true, ignore: excludePatterns, dot: false }); console.log(`\nšŸ“Š Scanning ${files.length} files...`); let processedFiles = 0; for (const file of files) { try { const stats = fs.statSync(file); if (!stats.isFile()) continue; // Skip very large files (over 10MB) to avoid performance issues const maxFileSize = 10 * 1024 * 1024; // 10MB if (stats.size > maxFileSize) { console.warn(`\nSkipping large file: ${path.relative(options.path, file)} (${Math.round(stats.size / 1024 / 1024)}MB)`); continue; } // Skip binary files that shouldn't contain secrets const fileExtension = path.extname(file).toLowerCase(); const binaryExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico', '.pdf', '.zip', '.exe', '.dll', '.so', '.dylib', '.woff', '.woff2', '.ttf', '.eot', '.mp4', '.mp3', '.avi', '.mov']; if (binaryExtensions.includes(fileExtension)) { continue; } processedFiles++; // Check if file is binary by reading first few bytes const buffer = fs.readFileSync(file); if (this.isBinaryFile(buffer, file)) { continue; } const content = buffer.toString('utf-8'); const relativePath = path.relative(options.path, file); const absolutePath = path.resolve(file); // Show progress if (processedFiles % 10 === 0 || processedFiles === files.length) { process.stdout.write(`\ršŸ“ Processed: ${processedFiles}/${files.length} files`); } const fileFindings = []; if (!options.vulnerabilitiesOnly) { const secretFindings = this.scanForSecrets(content, relativePath, absolutePath); const enhancedSecretFindings = this.scanForEnhancedSecrets(content, relativePath, absolutePath); fileFindings.push(...secretFindings); fileFindings.push(...enhancedSecretFindings); } if (!options.secretsOnly) { const vulnerabilityFindings = this.scanForVulnerabilities(content, relativePath, absolutePath); const advancedVulnFindings = this.scanForAdvancedVulnerabilities(content, relativePath, absolutePath); const dangerousAPIFindings = this.scanForDangerousAPIs(content, relativePath, absolutePath); const aiVulnFindings = this.scanForAIVulnerabilities(content, relativePath, absolutePath); fileFindings.push(...vulnerabilityFindings); fileFindings.push(...advancedVulnFindings); fileFindings.push(...dangerousAPIFindings); fileFindings.push(...aiVulnFindings); } // Analyze code quality const qualityAnalysis = this.codeQualityAnalyzer.analyzeCodeQuality(content, relativePath, content.split('\n').length); fileFindings.push(...qualityAnalysis.findings); // Create file analysis const fileAnalysis = { filePath: relativePath, absolutePath: absolutePath, relativePath: relativePath, size: stats.size, findings: fileFindings, linesOfCode: content.split('\n').length, fileType: path.extname(file).substring(1) || 'unknown', qualityScore: qualityAnalysis.qualityScore, summary: this.generateSummary(fileFindings) }; fileAnalyses.push(fileAnalysis); findings.push(...fileFindings); } catch (error) { console.warn(`\nWarning: Could not scan file ${file}: ${error}`); } } console.log('\n'); // New line after progress // Analyze dependencies console.log('šŸ” Analyzing dependencies...'); const dependencyResult = await this.dependencyAnalyzer.analyzeDependencies(options.path); findings.push(...dependencyResult.findings); // Analyze CI/CD configurations console.log('šŸ” Analyzing CI/CD configurations...'); const cicdResult = await this.cicdAnalyzer.analyzeCICD(options.path); findings.push(...cicdResult.findings); const filteredFindings = this.filterBySeverity(findings, options.minSeverity); const filteredFileAnalyses = fileAnalyses.map(fa => ({ ...fa, findings: this.filterBySeverity(fa.findings, options.minSeverity), summary: this.generateSummary(this.filterBySeverity(fa.findings, options.minSeverity)) })); const folderStructure = this.buildFolderStructure(filteredFileAnalyses, options.path); // Calculate security grade console.log('šŸ“Š Calculating security grade...'); const securityGrade = this.securityGrader.calculateSecurityGrade(filteredFindings, fileAnalyses.length); const nextSteps = this.securityGrader.generateNextSteps(filteredFindings, securityGrade); return { projectPath: options.path, scanTime: new Date(), findings: filteredFindings, fileAnalyses: filteredFileAnalyses, folderStructure, securityGrade, summary: { ...this.generateSummary(filteredFindings), filesScanned: fileAnalyses.length, filesWithIssues: filteredFileAnalyses.filter(fa => fa.findings.length > 0).length, foldersScanned: this.countFolders(folderStructure), byCategory: this.generateCategorySummary(filteredFindings), averageConfidence: this.calculateAverageConfidence(filteredFindings) }, nextSteps }; } scanForSecrets(content, filename, absolutePath) { const findings = []; const results = (0, secrets_1.detectSecrets)(content, filename); for (const result of results) { for (const match of result.matches) { const line = this.getLineNumber(content, match.index || 0); const column = this.getColumnNumber(content, match.index || 0); const codeContext = this.getCodeContext(content, match.index || 0); // Calculate context-aware confidence const contextAwareConfidence = this.calculateContextAwareConfidence(result.pattern.confidence, codeContext.matchedCode, codeContext.contextBefore, codeContext.contextAfter, filename); // Skip findings with very low confidence if (contextAwareConfidence < 0.5) continue; findings.push({ type: 'sensitive_data', category: 'Exposed Secrets', severity: contextAwareConfidence > 0.8 ? 'critical' : 'high', title: result.pattern.name, description: result.pattern.description, file: filename, line, column, code: codeContext.matchedCode, codeContext: { before: codeContext.contextBefore, after: codeContext.contextAfter }, recommendation: 'Remove hardcoded secrets and use environment variables or secure configuration management', confidence: contextAwareConfidence, cwe: 'CWE-798', owasp: 'A02:2021 – Cryptographic Failures', impact: 'Credential compromise, unauthorized access', effort: 'low' }); } } return findings; } scanForEnhancedSecrets(content, filename, absolutePath) { const findings = []; const results = this.enhancedSecretDetector.detectSecrets(content, filename); for (const result of results) { for (const match of result.matches) { const line = this.getLineNumber(content, match.match.index || 0); const column = this.getColumnNumber(content, match.match.index || 0); const codeContext = this.getCodeContext(content, match.match.index || 0); findings.push({ type: 'sensitive_data', category: result.pattern.name, severity: result.pattern.severity, title: result.pattern.name, description: result.pattern.description, file: filename, line, column, code: codeContext.matchedCode, codeContext: { before: codeContext.contextBefore, after: codeContext.contextAfter }, recommendation: result.pattern.remediation.description, confidence: match.confidence, cwe: result.pattern.cwe, owasp: result.pattern.owasp, impact: 'Credential compromise, unauthorized access', effort: result.pattern.remediation.effort }); } } return findings; } scanForDangerousAPIs(content, filename, absolutePath) { const findings = []; const results = this.dangerousAPIDetector.detectDangerousAPIs(content, filename); for (const result of results) { for (const match of result.matches) { const line = this.getLineNumber(content, match.match.index || 0); const column = this.getColumnNumber(content, match.match.index || 0); const codeContext = this.getCodeContext(content, match.match.index || 0); findings.push({ type: 'injection', category: result.pattern.category, severity: result.pattern.severity, title: result.pattern.name, description: result.pattern.description, file: filename, line, column, code: codeContext.matchedCode, codeContext: { before: codeContext.contextBefore, after: codeContext.contextAfter }, recommendation: result.pattern.remediation.description, confidence: match.confidence, cwe: result.pattern.cwe, owasp: result.pattern.owasp, impact: result.pattern.impact, effort: result.pattern.remediation.effort }); } } return findings; } scanForAIVulnerabilities(content, filename, absolutePath) { const findings = []; const results = this.aiVulnerabilityDetector.detectAIVulnerabilities(content, filename); for (const result of results) { for (const match of result.matches) { const line = this.getLineNumber(content, match.match.index || 0); const column = this.getColumnNumber(content, match.match.index || 0); const codeContext = this.getCodeContext(content, match.match.index || 0); findings.push({ type: 'injection', category: `AI-Generated: ${result.pattern.category}`, severity: result.pattern.severity, title: `šŸ¤– ${result.pattern.name}`, description: `${result.pattern.description} | AI Context: ${result.pattern.aiContext}`, file: filename, line, column, code: codeContext.matchedCode, codeContext: { before: codeContext.contextBefore, after: codeContext.contextAfter }, recommendation: result.pattern.remediation.description, confidence: match.confidence, cwe: result.pattern.cwe, owasp: result.pattern.owasp, impact: result.pattern.impact, effort: result.pattern.remediation.effort }); } } return findings; } scanForVulnerabilities(content, filename, absolutePath) { const findings = []; const results = (0, vulnerabilities_1.checkVulnerabilities)(content, filename); for (const result of results) { for (const match of result.matches) { const line = this.getLineNumber(content, match.index || 0); const column = this.getColumnNumber(content, match.index || 0); const codeContext = this.getCodeContext(content, match.index || 0); findings.push({ type: 'config', category: 'Legacy Vulnerability', severity: result.rule.severity, title: result.rule.name, description: result.rule.description, file: filename, line, column, code: codeContext.matchedCode, codeContext: { before: codeContext.contextBefore, after: codeContext.contextAfter }, recommendation: result.rule.recommendation, confidence: 0.8, impact: 'Security vulnerability', effort: 'medium' }); } } return findings; } scanForAdvancedVulnerabilities(content, filename, absolutePath) { const findings = []; const results = (0, advanced_vulnerabilities_1.checkAdvancedVulnerabilities)(content, filename); for (const result of results) { for (const match of result.matches) { const line = this.getLineNumber(content, match.index || 0); const column = this.getColumnNumber(content, match.index || 0); const codeContext = this.getCodeContext(content, match.index || 0); findings.push({ type: result.rule.type, category: result.rule.category, severity: result.rule.severity, title: result.rule.name, description: result.rule.description, file: filename, line, column, code: codeContext.matchedCode, codeContext: { before: codeContext.contextBefore, after: codeContext.contextAfter }, recommendation: result.rule.recommendation, confidence: result.rule.confidence, cwe: result.rule.cwe, owasp: result.rule.owasp, impact: result.rule.impact, effort: result.rule.effort }); } } return findings; } getCodeContext(content, index) { const lines = content.split('\n'); const currentLineIndex = this.getLineNumber(content, index) - 1; const contextBefore = currentLineIndex > 0 ? lines[currentLineIndex - 1] : ''; const currentLine = lines[currentLineIndex] || ''; const contextAfter = currentLineIndex < lines.length - 1 ? lines[currentLineIndex + 1] : ''; return { matchedCode: currentLine.trim(), contextBefore: contextBefore.trim(), contextAfter: contextAfter.trim() }; } buildFolderStructure(fileAnalyses, basePath) { const folderMap = new Map(); // Initialize root folder const rootFolder = { folderPath: basePath, files: [], subFolders: [], summary: { critical: 0, high: 0, medium: 0, low: 0, total: 0, filesScanned: 0, filesWithIssues: 0 } }; folderMap.set(basePath, rootFolder); // Process each file for (const fileAnalysis of fileAnalyses) { const filePath = path.resolve(basePath, fileAnalysis.relativePath); const folderPath = path.dirname(filePath); // Create folder structure if it doesn't exist this.ensureFolderExists(folderMap, folderPath, basePath); // Add file to its folder const folder = folderMap.get(folderPath); folder.files.push(fileAnalysis); // Update folder statistics this.updateFolderSummary(folder, fileAnalysis); } // Build folder hierarchy this.buildFolderHierarchy(folderMap, basePath); return rootFolder; } ensureFolderExists(folderMap, folderPath, basePath) { if (folderMap.has(folderPath)) return; const folder = { folderPath, files: [], subFolders: [], summary: { critical: 0, high: 0, medium: 0, low: 0, total: 0, filesScanned: 0, filesWithIssues: 0 } }; folderMap.set(folderPath, folder); // Recursively create parent folders const parentPath = path.dirname(folderPath); if (parentPath !== folderPath && parentPath.startsWith(basePath)) { this.ensureFolderExists(folderMap, parentPath, basePath); } } buildFolderHierarchy(folderMap, basePath) { for (const [folderPath, folder] of folderMap) { if (folderPath === basePath) continue; const parentPath = path.dirname(folderPath); const parent = folderMap.get(parentPath); if (parent && !parent.subFolders.includes(folder)) { parent.subFolders.push(folder); // Update parent summary with child folder data parent.summary.critical += folder.summary.critical; parent.summary.high += folder.summary.high; parent.summary.medium += folder.summary.medium; parent.summary.low += folder.summary.low; parent.summary.total += folder.summary.total; parent.summary.filesScanned += folder.summary.filesScanned; parent.summary.filesWithIssues += folder.summary.filesWithIssues; } } } updateFolderSummary(folder, fileAnalysis) { folder.summary.filesScanned++; if (fileAnalysis.findings.length > 0) { folder.summary.filesWithIssues++; } folder.summary.critical += fileAnalysis.summary.critical; folder.summary.high += fileAnalysis.summary.high; folder.summary.medium += fileAnalysis.summary.medium; folder.summary.low += fileAnalysis.summary.low; folder.summary.total += fileAnalysis.summary.total; } countFolders(folder) { let count = 1; for (const subFolder of folder.subFolders) { count += this.countFolders(subFolder); } return count; } filterBySeverity(findings, minSeverity) { if (!minSeverity) return findings; const severityLevels = { low: 0, medium: 1, high: 2, critical: 3 }; const minLevel = severityLevels[minSeverity]; return findings.filter(finding => severityLevels[finding.severity] >= minLevel); } generateSummary(findings) { const summary = { critical: 0, high: 0, medium: 0, low: 0, total: findings.length }; for (const finding of findings) { summary[finding.severity]++; } return summary; } generateCategorySummary(findings) { const categorySummary = {}; for (const finding of findings) { const category = finding.category || finding.type; categorySummary[category] = (categorySummary[category] || 0) + 1; } return categorySummary; } calculateAverageConfidence(findings) { if (findings.length === 0) return 0; const totalConfidence = findings.reduce((sum, finding) => sum + finding.confidence, 0); return Math.round((totalConfidence / findings.length) * 100) / 100; } isBinaryFile(buffer, filename) { // Check for common binary file signatures const signatures = [ [0xFF, 0xD8, 0xFF], // JPEG [0x89, 0x50, 0x4E, 0x47], // PNG [0x47, 0x49, 0x46], // GIF [0x25, 0x50, 0x44, 0x46], // PDF [0x50, 0x4B], // ZIP/Office files [0x4D, 0x5A] // Windows executable ]; // Check file signatures for (const sig of signatures) { if (buffer.length >= sig.length) { let matches = true; for (let i = 0; i < sig.length; i++) { if (buffer[i] !== sig[i]) { matches = false; break; } } if (matches) return true; } } // Check for high percentage of non-printable characters in first 1KB const sampleSize = Math.min(1024, buffer.length); let nonPrintableCount = 0; for (let i = 0; i < sampleSize; i++) { const byte = buffer[i]; // Count bytes that are not printable ASCII or common whitespace if (byte < 32 && byte !== 9 && byte !== 10 && byte !== 13) { nonPrintableCount++; } else if (byte > 126) { nonPrintableCount++; } } // If more than 30% non-printable, likely binary return (nonPrintableCount / sampleSize) > 0.3; } calculateContextAwareConfidence(baseConfidence, matchedCode, contextBefore, contextAfter, filename) { let confidence = baseConfidence; // Reduce confidence for test files if (filename.includes('test') || filename.includes('spec') || filename.includes('mock')) { confidence *= 0.7; } // Reduce confidence for example/demo files if (filename.includes('example') || filename.includes('demo') || filename.includes('sample')) { confidence *= 0.6; } // Increase confidence if in config/environment files if (filename.includes('config') || filename.includes('.env') || filename.includes('settings')) { confidence *= 1.2; } // Look for context indicators that suggest it's a real secret const secretIndicators = ['api_key', 'secret', 'token', 'password', 'credential', 'auth']; const testIndicators = ['example', 'test', 'dummy', 'fake', 'mock', 'placeholder']; const fullContext = (contextBefore + ' ' + matchedCode + ' ' + contextAfter).toLowerCase(); // Increase confidence if secret indicators are present for (const indicator of secretIndicators) { if (fullContext.includes(indicator)) { confidence *= 1.1; break; } } // Decrease confidence if test indicators are present for (const indicator of testIndicators) { if (fullContext.includes(indicator)) { confidence *= 0.5; break; } } // Reduce confidence for very short or very common patterns if (matchedCode.length < 10) { confidence *= 0.8; } // Check for common placeholder patterns if (/^[a-z]{1,3}$|^test|^example|^placeholder|^your-|^my-/.test(matchedCode.toLowerCase())) { confidence *= 0.3; } return Math.min(1.0, Math.max(0.0, confidence)); } } exports.SecurityScanner = SecurityScanner; //# sourceMappingURL=scanner.js.map