UNPKG

arda-site-scan

Version:

A standalone CLI tool for comprehensive website analysis including screenshots, SEO, and accessibility testing using Playwright

492 lines 25.7 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ApiKeyTester = void 0; const chalk_1 = __importDefault(require("chalk")); const session_manager_js_1 = require("../utils/session-manager.js"); const test_output_handler_js_1 = require("../utils/test-output-handler.js"); class ApiKeyTester { sessionManager; allFindings = []; scannedPages = 0; // Comprehensive API key patterns for major providers API_KEY_PATTERNS = [ // AWS { name: 'AWS Access Key ID', regex: /AKIA[0-9A-Z]{16}/g, severity: 'critical', description: 'AWS Access Key ID' }, { name: 'AWS Secret Access Key', regex: /[0-9a-zA-Z\/+]{40}/g, severity: 'critical', description: 'AWS Secret Access Key (40 chars)' }, { name: 'AWS Session Token', regex: /AWS[0-9A-Z]{16,}/g, severity: 'critical', description: 'AWS Session Token' }, // Google API Keys { name: 'Google API Key', regex: /AIza[0-9A-Za-z\-_]{35}/g, severity: 'high', description: 'Google API Key' }, { name: 'Google OAuth Token', regex: /ya29\.[0-9A-Za-z\-_]+/g, severity: 'high', description: 'Google OAuth Access Token' }, { name: 'Google Service Account', regex: /\"type\": \"service_account\"/g, severity: 'critical', description: 'Google Service Account JSON' }, // GitHub { name: 'GitHub Token', regex: /gh[pousr]_[A-Za-z0-9]{36}/g, severity: 'high', description: 'GitHub Personal Access Token' }, { name: 'GitHub App Token', regex: /ghs_[A-Za-z0-9]{36}/g, severity: 'high', description: 'GitHub App Token' }, { name: 'GitHub Classic Token', regex: /ghp_[A-Za-z0-9]{36}/g, severity: 'high', description: 'GitHub Classic Token' }, // Stripe { name: 'Stripe Live Key', regex: /sk_live_[0-9a-zA-Z]{24}/g, severity: 'critical', description: 'Stripe Live Secret Key' }, { name: 'Stripe Test Key', regex: /sk_test_[0-9a-zA-Z]{24}/g, severity: 'medium', description: 'Stripe Test Secret Key' }, { name: 'Stripe Publishable Key', regex: /pk_live_[0-9a-zA-Z]{24}/g, severity: 'high', description: 'Stripe Live Publishable Key' }, { name: 'Stripe Webhook Secret', regex: /whsec_[a-zA-Z0-9]{32}/g, severity: 'high', description: 'Stripe Webhook Secret' }, // PayPal { name: 'PayPal Client ID', regex: /A[0-9A-Z]{80}/g, severity: 'high', description: 'PayPal Client ID' }, // Microsoft Azure { name: 'Azure Client Secret', regex: /[0-9A-Za-z\-_.~]{34}/g, severity: 'high', description: 'Azure Client Secret' }, // Twilio { name: 'Twilio Auth Token', regex: /[0-9a-f]{32}/g, severity: 'high', description: 'Twilio Auth Token' }, { name: 'Twilio SID', regex: /AC[a-zA-Z0-9]{32}/g, severity: 'medium', description: 'Twilio Account SID' }, // Slack { name: 'Slack Token', regex: /xox[bpas]-[0-9]{12}-[0-9]{12}-[0-9a-zA-Z]{24}/g, severity: 'high', description: 'Slack API Token' }, { name: 'Slack Webhook', regex: /https:\/\/hooks\.slack\.com\/services\/T[0-9A-Z]{8}\/B[0-9A-Z]{8}\/[0-9a-zA-Z]{24}/g, severity: 'medium', description: 'Slack Webhook URL' }, // SendGrid { name: 'SendGrid API Key', regex: /SG\.[0-9A-Za-z\-_]{22}\.[0-9A-Za-z\-_]{43}/g, severity: 'high', description: 'SendGrid API Key' }, // Mailgun { name: 'Mailgun API Key', regex: /key-[0-9a-zA-Z]{32}/g, severity: 'high', description: 'Mailgun API Key' }, // Firebase { name: 'Firebase URL', regex: /https:\/\/[0-9a-z_-]+\.firebaseio\.com/g, severity: 'medium', description: 'Firebase Database URL' }, // JWT Tokens { name: 'JWT Token', regex: /eyJ[A-Za-z0-9_-]*\.eyJ[A-Za-z0-9_-]*\.[A-Za-z0-9_-]*/g, severity: 'medium', description: 'JSON Web Token' }, // Generic patterns { name: 'Generic API Key', regex: /['\"]?api[_-]?key['\"]?\s*[:=]\s*['\"][0-9a-zA-Z]{16,}['\"]/gi, severity: 'medium', description: 'Generic API Key pattern' }, { name: 'Generic Access Token', regex: /['\"]?access[_-]?token['\"]?\s*[:=]\s*['\"][0-9a-zA-Z]{16,}['\"]/gi, severity: 'medium', description: 'Generic Access Token pattern' }, { name: 'Generic Secret', regex: /['\"]?secret['\"]?\s*[:=]\s*['\"][0-9a-zA-Z]{16,}['\"]/gi, severity: 'medium', description: 'Generic Secret pattern' }, { name: 'Generic Password', regex: /['\"]?password['\"]?\s*[:=]\s*['\"][^'\"]{8,}['\"]/gi, severity: 'medium', description: 'Generic Password pattern' } ]; // High entropy string patterns (for unknown API keys) HIGH_ENTROPY_PATTERNS = [ { name: 'High Entropy String', regex: /[A-Za-z0-9+/]{32,}={0,2}/g, minEntropy: 4.5, severity: 'low', description: 'High entropy string (possible API key)' }, { name: 'Base64 Pattern', regex: /[A-Za-z0-9+/]{20,}={0,2}/g, minEntropy: 4.0, severity: 'low', description: 'Base64 encoded string' }, { name: 'Hex Pattern', regex: /[a-fA-F0-9]{32,}/g, minEntropy: 2.0, severity: 'low', description: 'Long hexadecimal string' } ]; constructor() { this.sessionManager = new session_manager_js_1.SessionManager(); } async runApiKeyScan(page, pageUrl, sessionId) { const pageName = test_output_handler_js_1.StandardTestOutputHandler.getPageNameFromUrl(pageUrl); // Create initial test result using standardized system const testResult = this.sessionManager.createStandardTestResult('api-key-scan', 'pending'); try { console.log(chalk_1.default.gray(` 🔐 Scanning for API keys...`)); // Scan the page for API keys const pageFindings = await this.scanPageForApiKeys(page, pageUrl); // Add findings to the global collection this.allFindings.push(...pageFindings); this.scannedPages++; // Check if this is the last page to process (we'll generate report in the orchestrator) testResult.status = 'success'; testResult.endTime = new Date(); if (pageFindings.length > 0) { console.log(chalk_1.default.yellow(` ⚠️ Found ${pageFindings.length} potential API key(s) on this page`)); } else { console.log(chalk_1.default.green(` ✅ No API keys found on this page`)); } } catch (error) { testResult.status = 'failed'; testResult.error = error instanceof Error ? error.message : String(error); testResult.endTime = new Date(); console.log(chalk_1.default.red(` ❌ API key scan failed: ${testResult.error}`)); } return testResult; } async generateFinalReport(sessionId, totalUrls) { const testResult = this.sessionManager.createStandardTestResult('api-key-scan', 'pending'); try { console.log(chalk_1.default.gray(` 📋 Generating API key security report...`)); const results = { findings: this.allFindings, totalFindings: this.allFindings.length, pagesCanned: this.scannedPages, timestamp: new Date().toISOString() }; // Generate comprehensive security report const report = this.generateApiKeyReport(results, totalUrls); // Save using the standardized output system const context = { url: 'site-wide', pageName: 'api-key-security-report' }; const saveResult = await this.sessionManager.saveTestOutput(report, sessionId, 'api-key-scan', context); if (saveResult.success) { testResult.status = 'success'; testResult.outputPath = saveResult.outputPath; } else { throw new Error(saveResult.error || 'Failed to save API key security report'); } testResult.endTime = new Date(); if (this.allFindings.length > 0) { const criticalCount = this.allFindings.filter(f => f.severity === 'critical').length; const highCount = this.allFindings.filter(f => f.severity === 'high').length; console.log(chalk_1.default.red(` 🚨 API key scan completed: ${this.allFindings.length} findings (${criticalCount} critical, ${highCount} high)`)); } else { console.log(chalk_1.default.green(` ✅ API key scan completed: No API keys found across ${this.scannedPages} pages`)); } } catch (error) { testResult.status = 'failed'; testResult.error = error instanceof Error ? error.message : String(error); testResult.endTime = new Date(); console.log(chalk_1.default.red(` ❌ Failed to generate API key report: ${testResult.error}`)); } return testResult; } async scanPageForApiKeys(page, pageUrl) { const findings = []; // Get page content for scanning const pageContent = await page.evaluate(() => { return { html: document.documentElement.outerHTML, scripts: Array.from(document.querySelectorAll('script')).map(script => ({ content: script.textContent || '', src: script.src || '' })), styles: Array.from(document.querySelectorAll('style')).map(style => style.textContent || ''), links: Array.from(document.querySelectorAll('link[rel="stylesheet"]')).map(link => link.href) }; }); // Scan HTML content findings.push(...this.scanContent(pageContent.html, 'html', pageUrl)); // Scan JavaScript content pageContent.scripts.forEach(script => { if (script.content) { findings.push(...this.scanContent(script.content, 'javascript', pageUrl, script.src)); } }); // Scan CSS content pageContent.styles.forEach(style => { findings.push(...this.scanContent(style, 'css', pageUrl)); }); // Additional external resources scan try { const externalJs = await this.scanExternalResources(page, pageUrl); findings.push(...externalJs); } catch (error) { // Silently continue if external resource scanning fails } return findings; } scanContent(content, source, url, context) { const findings = []; // Scan with defined patterns this.API_KEY_PATTERNS.forEach(pattern => { let match; while ((match = pattern.regex.exec(content)) !== null) { // Skip if match is too common (reduce false positives) if (this.isLikelyFalsePositive(match[0], pattern.name)) { continue; } findings.push({ type: pattern.name, value: this.maskSensitiveValue(match[0]), location: { source, context: context || this.getContext(content, match.index, 50), line: this.getLineNumber(content, match.index) }, severity: pattern.severity, confidence: this.calculateConfidence(match[0], pattern.name, content, match.index), url }); } }); // Scan for high entropy strings this.HIGH_ENTROPY_PATTERNS.forEach(pattern => { let match; while ((match = pattern.regex.exec(content)) !== null) { const entropy = this.calculateEntropy(match[0]); if (entropy >= pattern.minEntropy && !this.isLikelyFalsePositive(match[0], pattern.name)) { findings.push({ type: pattern.name, value: this.maskSensitiveValue(match[0]), location: { source, context: context || this.getContext(content, match.index, 50), line: this.getLineNumber(content, match.index) }, severity: pattern.severity, confidence: Math.min(0.8, entropy / 6), // Scale entropy to confidence url }); } } }); return findings; } async scanExternalResources(page, pageUrl) { const findings = []; try { // Get all script sources const scriptSrcs = await page.evaluate(() => { return Array.from(document.querySelectorAll('script[src]')).map(script => script.src); }); // Limit to same-origin scripts to avoid CORS issues const sameOriginScripts = scriptSrcs.filter(src => { try { const scriptUrl = new URL(src); const pageUrlObj = new URL(pageUrl); return scriptUrl.origin === pageUrlObj.origin; } catch { return false; } }); // Scan a limited number of external scripts to avoid performance issues for (const src of sameOriginScripts.slice(0, 5)) { try { const response = await page.goto(src, { timeout: 5000, waitUntil: 'domcontentloaded' }); if (response && response.ok()) { const scriptContent = await response.text(); findings.push(...this.scanContent(scriptContent, 'javascript', pageUrl, src)); } } catch { // Silently continue if script loading fails } } } catch { // Silently continue if external resource scanning fails } return findings; } isLikelyFalsePositive(value, patternName) { const falsePositivePatterns = [ /^(example|test|demo|placeholder|sample)/i, /^(your|my|the)[_-]?(api|key|token)/i, /^(xxx|yyy|zzz)/i, /^[0]+$/, /^[1]+$/, /lorem ipsum/i, /^(null|undefined|empty)$/i ]; // Check against common false positive patterns if (falsePositivePatterns.some(pattern => pattern.test(value))) { return true; } // Pattern-specific false positive checks if (patternName.includes('AWS') && value.length < 16) { return true; } if (patternName.includes('JWT') && value.split('.').length !== 3) { return true; } return false; } calculateConfidence(value, patternName, content, index) { let confidence = 0.5; // Base confidence // Increase confidence for specific patterns if (patternName.includes('AWS') || patternName.includes('Google') || patternName.includes('Stripe')) { confidence += 0.3; } // Check context for additional confidence indicators const context = this.getContext(content, index, 100).toLowerCase(); // Positive indicators if (context.includes('api') || context.includes('key') || context.includes('token') || context.includes('secret')) { confidence += 0.2; } if (context.includes('auth') || context.includes('bearer') || context.includes('header')) { confidence += 0.15; } // Negative indicators if (context.includes('example') || context.includes('test') || context.includes('demo')) { confidence -= 0.3; } if (context.includes('placeholder') || context.includes('sample')) { confidence -= 0.2; } return Math.max(0.1, Math.min(1.0, confidence)); } calculateEntropy(str) { const len = str.length; const frequencies = {}; for (let i = 0; i < len; i++) { const char = str.charAt(i); frequencies[char] = (frequencies[char] || 0) + 1; } let entropy = 0; for (const char in frequencies) { const p = frequencies[char] / len; entropy -= p * Math.log2(p); } return entropy; } getContext(content, index, length) { const start = Math.max(0, index - length / 2); const end = Math.min(content.length, index + length / 2); return content.substring(start, end).replace(/\s+/g, ' ').trim(); } getLineNumber(content, index) { return content.substring(0, index).split('\n').length; } maskSensitiveValue(value) { if (value.length <= 8) { return value.substring(0, 2) + '*'.repeat(value.length - 2); } return value.substring(0, 4) + '*'.repeat(value.length - 8) + value.substring(value.length - 4); } generateApiKeyReport(results, scannedUrls) { let report = `# API Key Security Scan Report\n\n`; report += `**Scan Date:** ${results.timestamp}\n`; report += `**Pages Scanned:** ${results.pagesCanned}\n`; report += `**Total Findings:** ${results.totalFindings}\n\n`; // Executive Summary report += `## 🚨 Executive Summary\n\n`; if (results.totalFindings === 0) { report += `✅ **No API keys or sensitive tokens were detected** across ${results.pagesCanned} page(s).\n\n`; report += `This is excellent! Your website appears to be free of exposed API keys and authentication tokens.\n\n`; } else { const severityCounts = this.groupBySeverity(results.findings); report += `⚠️ **${results.totalFindings} potential security issue(s) detected** across ${results.pagesCanned} page(s).\n\n`; report += `**Severity Breakdown:**\n`; if (severityCounts.critical > 0) report += `- 🔴 **Critical:** ${severityCounts.critical} findings\n`; if (severityCounts.high > 0) report += `- 🟠 **High:** ${severityCounts.high} findings\n`; if (severityCounts.medium > 0) report += `- 🟡 **Medium:** ${severityCounts.medium} findings\n`; if (severityCounts.low > 0) report += `- 🟢 **Low:** ${severityCounts.low} findings\n`; report += '\n'; // Immediate actions required if (severityCounts.critical > 0 || severityCounts.high > 0) { report += `🚨 **IMMEDIATE ACTION REQUIRED**: Critical or high-severity findings require urgent attention.\n\n`; } } // Detailed Findings if (results.totalFindings > 0) { report += `## 🔍 Detailed Findings\n\n`; const findingsByPage = this.groupByUrl(results.findings); Object.entries(findingsByPage).forEach(([url, findings]) => { report += `### ${url}\n\n`; findings.forEach((finding, index) => { const severityIcon = this.getSeverityIcon(finding.severity); const confidencePercent = Math.round(finding.confidence * 100); report += `#### ${severityIcon} Finding ${index + 1}: ${finding.type}\n`; report += `**Severity:** ${finding.severity.toUpperCase()}\n`; report += `**Confidence:** ${confidencePercent}%\n`; report += `**Location:** ${finding.location.source}`; if (finding.location.line) report += ` (line ${finding.location.line})`; report += '\n'; report += `**Value:** \`${finding.value}\`\n`; if (finding.location.context) { report += `**Context:** \`${finding.location.context}\`\n`; } report += '\n'; }); }); } // Risk Assessment report += `## 🎯 Risk Assessment\n\n`; if (results.totalFindings === 0) { report += `**Overall Risk Level: LOW** ✅\n\n`; report += `No exposed API keys were found. Continue monitoring for new deployments.\n\n`; } else { const severityCounts = this.groupBySeverity(results.findings); let riskLevel = 'LOW'; if (severityCounts.critical > 0) riskLevel = 'CRITICAL'; else if (severityCounts.high > 0) riskLevel = 'HIGH'; else if (severityCounts.medium > 0) riskLevel = 'MEDIUM'; const riskIcon = riskLevel === 'CRITICAL' ? '🔴' : riskLevel === 'HIGH' ? '🟠' : riskLevel === 'MEDIUM' ? '🟡' : '🟢'; report += `**Overall Risk Level: ${riskLevel}** ${riskIcon}\n\n`; if (riskLevel === 'CRITICAL' || riskLevel === 'HIGH') { report += `⚠️ **High-priority security vulnerabilities detected.** Exposed API keys can lead to:\n`; report += `- Unauthorized access to services and data\n`; report += `- Financial losses from abuse of paid APIs\n`; report += `- Data breaches and compliance violations\n`; report += `- Reputation damage and loss of customer trust\n\n`; } } // Remediation Steps report += `## 🔧 Remediation Steps\n\n`; if (results.totalFindings > 0) { report += `**Immediate Actions:**\n`; report += `1. **Revoke exposed API keys immediately** - Generate new keys for all exposed credentials\n`; report += `2. **Remove sensitive data from code** - Never store API keys in client-side code\n`; report += `3. **Implement environment variables** - Use server-side environment variables for secrets\n`; report += `4. **Add secrets to .gitignore** - Prevent future commits of sensitive data\n`; report += `5. **Audit git history** - Check if keys were committed to version control\n\n`; } report += `**Security Best Practices:**\n`; report += `- Use environment variables for all API keys and secrets\n`; report += `- Implement key rotation policies\n`; report += `- Use least-privilege access principles\n`; report += `- Monitor API usage for anomalies\n`; report += `- Implement proper secret management solutions\n`; report += `- Regular security audits and scans\n\n`; // Prevention Guide report += `## 🛡️ Prevention Guide\n\n`; report += `**Development Practices:**\n`; report += `- Never hardcode API keys in source code\n`; report += `- Use configuration files that are not tracked by version control\n`; report += `- Implement pre-commit hooks to detect secrets\n`; report += `- Use secret scanning tools in CI/CD pipelines\n`; report += `- Regular developer security training\n\n`; report += `**Deployment Practices:**\n`; report += `- Use container orchestration secrets management\n`; report += `- Implement proper IAM roles and policies\n`; report += `- Monitor and log all API key usage\n`; report += `- Regular security assessments\n\n`; // Compliance Notes report += `## 📋 Compliance & Standards\n\n`; report += `**Security Frameworks:**\n`; report += `- OWASP Top 10 - A02:2021 Cryptographic Failures\n`; report += `- NIST Cybersecurity Framework\n`; report += `- SOC 2 Type II compliance requirements\n`; report += `- PCI DSS for payment-related keys\n\n`; // Scan Details report += `## 📊 Scan Details\n\n`; report += `**Pages Scanned:**\n`; scannedUrls.forEach((url, index) => { report += `${index + 1}. ${url}\n`; }); report += '\n'; report += `**Detection Patterns Used:**\n`; report += `- ${this.API_KEY_PATTERNS.length} specific API key patterns\n`; report += `- ${this.HIGH_ENTROPY_PATTERNS.length} entropy-based detection patterns\n`; report += `- Context-aware false positive reduction\n`; report += `- Confidence scoring algorithm\n\n`; report += `**Generated by:** Playwright Site Scanner API Key Security Test\n`; report += `**Report Version:** 1.0\n`; return report; } groupBySeverity(findings) { return { critical: findings.filter(f => f.severity === 'critical').length, high: findings.filter(f => f.severity === 'high').length, medium: findings.filter(f => f.severity === 'medium').length, low: findings.filter(f => f.severity === 'low').length }; } groupByUrl(findings) { return findings.reduce((groups, finding) => { if (!groups[finding.url]) { groups[finding.url] = []; } groups[finding.url].push(finding); return groups; }, {}); } getSeverityIcon(severity) { const icons = { critical: '🔴', high: '🟠', medium: '🟡', low: '🟢' }; return icons[severity] || '⚪'; } } exports.ApiKeyTester = ApiKeyTester; //# sourceMappingURL=api-key-tester.js.map