UNPKG

@boundless-oss/atlas

Version:

Atlas - MCP Server for comprehensive startup project management

1,018 lines (874 loc) 31.9 kB
import { ConfigManager } from '../../config/config-manager.js'; import { SecurityConfig, SecurityScanResult, SecurityFinding, SecretPattern, SecretFinding, VulnerabilityReport, SecuritySetupConfig, SecuritySetupResult, SecurityReport } from './types.js'; import { promises as fs } from 'fs'; import path from 'path'; import { exec } from 'child_process'; import { promisify } from 'util'; const execAsync = promisify(exec); export class SecurityScanner { private configManager: ConfigManager; private secretPatterns: SecretPattern[]; constructor(configManager: ConfigManager) { this.configManager = configManager; this.secretPatterns = this.initializeSecretPatterns(); } async performScan(config: SecurityConfig): Promise<SecurityScanResult> { const startTime = Date.now(); const findings: SecurityFinding[] = []; try { // Scan for secrets if (config.includeSecrets) { const secretFindings = await this.scanForSecrets({ scanPath: '.' }); findings.push(...this.convertSecretsToFindings(secretFindings)); } // Check dependencies if (config.includeDependencies) { const vulnReport = await this.checkVulnerabilities({ checkNpm: true }); findings.push(...this.convertVulnerabilitiesToFindings(vulnReport)); } // Scan files for security issues if (config.includeFiles) { const fileFindings = await this.scanSourceFiles(); findings.push(...fileFindings); } // Check permissions if (config.includePermissions) { const permissionFindings = await this.checkPermissions(); findings.push(...permissionFindings); } const endTime = Date.now(); const summary = this.calculateSummary(findings, endTime - startTime); return { summary, findings, recommendations: this.generateRecommendations(findings), metadata: { scanDate: new Date().toISOString(), scanType: config.scanType, version: '1.0.0', }, }; } catch (error) { throw new Error(`Security scan failed: ${(error as Error).message}`); } } async scanForSecrets(options: { scanPath: string; excludePaths?: string[]; customPatterns?: string[]; }): Promise<SecretFinding[]> { const secrets: SecretFinding[] = []; const excludePaths = options.excludePaths || ['.git', 'node_modules', '.atlas']; await this.scanDirectory(options.scanPath, secrets, excludePaths); return secrets; } async checkVulnerabilities(options: { checkNpm?: boolean; checkYarn?: boolean; severity?: string; autoFix?: boolean; }): Promise<VulnerabilityReport> { const report: VulnerabilityReport = { total: 0, critical: 0, high: 0, moderate: 0, low: 0, details: [], lastChecked: new Date().toISOString(), }; try { if (options.checkNpm) { // Check if package.json exists const packageJsonPath = path.join(process.cwd(), 'package.json'); try { await fs.access(packageJsonPath); const npmAuditResult = await this.runNpmAudit(options.autoFix); this.mergeVulnerabilityReport(report, npmAuditResult); } catch { // No package.json, skip npm check } } } catch (error) { // Handle audit errors gracefully } return report; } async setupSecurityConfiguration(config: SecuritySetupConfig): Promise<SecuritySetupResult> { const filesCreated: string[] = []; const toolsConfigured: string[] = []; const nextSteps: string[] = []; if (config.createSecurityPolicy) { await this.createSecurityPolicy(); filesCreated.push('SECURITY.md'); toolsConfigured.push('Security Policy'); } if (config.enableSecretDetection) { await this.setupSecretDetection(); filesCreated.push('.gitleaks.toml'); toolsConfigured.push('Secret Detection'); nextSteps.push('Install gitleaks: brew install gitleaks (macOS) or download from GitHub'); } if (config.enableVulnerabilityChecks) { await this.setupVulnerabilityChecks(); filesCreated.push('.github/workflows/security.yml'); toolsConfigured.push('Vulnerability Scanning'); } if (config.setupGitHooks && config.enablePreCommitScans) { await this.setupSecurityGitHooks(); filesCreated.push('.git/hooks/pre-commit'); toolsConfigured.push('Git Hooks'); nextSteps.push('Make Git hooks executable: chmod +x .git/hooks/pre-commit'); } if (config.enablePreCommitScans) { await this.setupPreCommitConfig(); filesCreated.push('.pre-commit-config.yaml'); toolsConfigured.push('Pre-commit Security'); nextSteps.push('Install pre-commit: pip install pre-commit && pre-commit install'); } return { filesCreated, toolsConfigured, nextSteps }; } async generateSecurityReport(options: { includeOverview: boolean; includeDetails: boolean; includeRecommendations: boolean; }): Promise<SecurityReport> { // Perform a comprehensive scan const scanResult = await this.performScan({ scanType: 'comprehensive', includeFiles: true, includeDependencies: true, includeSecrets: true, includePermissions: true, outputFormat: 'detailed', }); const sections: any = {}; let markdown = '# Security Report\n\n'; if (options.includeOverview) { sections.overview = this.generateOverviewSection(scanResult); markdown += sections.overview + '\n\n'; } if (options.includeDetails) { sections.details = this.generateDetailsSection(scanResult); markdown += sections.details + '\n\n'; } if (options.includeRecommendations) { sections.recommendations = this.generateRecommendationsSection(scanResult); markdown += sections.recommendations + '\n\n'; } return { markdown, summary: scanResult.summary, sections, }; } private initializeSecretPatterns(): SecretPattern[] { return [ { name: 'AWS Access Key', pattern: /AKIA[0-9A-Z]{16}/gi, severity: 'high', description: 'AWS Access Key ID detected', }, { name: 'AWS Secret Key', pattern: /[0-9a-zA-Z/+]{40}/gi, severity: 'high', description: 'Potential AWS Secret Access Key', }, { name: 'GitHub Token', pattern: /ghp_[0-9a-zA-Z]{36}/gi, severity: 'high', description: 'GitHub Personal Access Token', }, { name: 'API Key', pattern: /api[_-]?key[_-]?[=:]\s*['\"]?[0-9a-zA-Z]{20,}['\"]?/gi, severity: 'medium', description: 'Generic API key pattern', }, { name: 'Database URL', pattern: /(mongodb|mysql|postgres):\/\/[^\s]+/gi, severity: 'medium', description: 'Database connection string', }, { name: 'Private Key', pattern: /-----BEGIN (RSA )?PRIVATE KEY-----/gi, severity: 'high', description: 'Private key detected', }, { name: 'JWT Token', pattern: /eyJ[0-9a-zA-Z+/]+=*\./gi, severity: 'medium', description: 'JSON Web Token detected', }, ]; } private async scanDirectory(dirPath: string, secrets: SecretFinding[], excludePaths: string[]): Promise<void> { try { const items = await fs.readdir(dirPath, { withFileTypes: true }); for (const item of items) { const fullPath = path.join(dirPath, item.name); const relativePath = path.relative(process.cwd(), fullPath); // Skip excluded paths if (excludePaths.some(exclude => relativePath.includes(exclude))) { continue; } if (item.isDirectory()) { await this.scanDirectory(fullPath, secrets, excludePaths); } else if (item.isFile() && this.shouldScanFile(item.name)) { await this.scanFile(fullPath, secrets); } } } catch (error) { // Skip directories we can't read } } private shouldScanFile(filename: string): boolean { const textExtensions = ['.js', '.ts', '.jsx', '.tsx', '.json', '.md', '.txt', '.env', '.yml', '.yaml', '.toml', '.ini']; const ext = path.extname(filename).toLowerCase(); return textExtensions.includes(ext) || filename.startsWith('.env'); } private async scanFile(filePath: string, secrets: SecretFinding[]): Promise<void> { try { const content = await fs.readFile(filePath, 'utf-8'); const lines = content.split('\n'); lines.forEach((line, lineNumber) => { for (const pattern of this.secretPatterns) { const matches = line.match(pattern.pattern); if (matches) { matches.forEach(match => { secrets.push({ type: pattern.name, file: path.relative(process.cwd(), filePath), line: lineNumber + 1, pattern: match.substring(0, 50) + (match.length > 50 ? '...' : ''), severity: pattern.severity, context: line.trim(), }); }); } } }); } catch (error) { // Skip files we can't read } } private async scanSourceFiles(): Promise<SecurityFinding[]> { const findings: SecurityFinding[] = []; // Check for common security anti-patterns in code const patterns = [ { pattern: /eval\s*\(/gi, title: 'Dangerous eval() usage', severity: 'high' as const, description: 'Use of eval() can lead to code injection vulnerabilities', recommendation: 'Avoid using eval(). Use JSON.parse() for JSON or other safe alternatives', }, { pattern: /innerHTML\s*=/gi, title: 'Potential XSS via innerHTML', severity: 'medium' as const, description: 'Direct innerHTML assignment can lead to XSS vulnerabilities', recommendation: 'Use textContent, createTextNode, or sanitize input before using innerHTML', }, { pattern: /document\.write\s*\(/gi, title: 'Dangerous document.write usage', severity: 'medium' as const, description: 'document.write can be vulnerable to XSS attacks', recommendation: 'Use modern DOM manipulation methods instead of document.write', }, ]; try { await this.scanForCodePatterns(patterns, findings); } catch (error) { // Handle scan errors gracefully } return findings; } private async scanForCodePatterns( patterns: Array<{ pattern: RegExp; title: string; severity: 'critical' | 'high' | 'medium' | 'low'; description: string; recommendation: string; }>, findings: SecurityFinding[] ): Promise<void> { const scanPath = process.cwd(); await this.scanDirectoryForPatterns(scanPath, patterns, findings); } private async scanDirectoryForPatterns( dirPath: string, patterns: any[], findings: SecurityFinding[] ): Promise<void> { try { const items = await fs.readdir(dirPath, { withFileTypes: true }); for (const item of items) { const fullPath = path.join(dirPath, item.name); const relativePath = path.relative(process.cwd(), fullPath); // Skip excluded paths if (['node_modules', '.git', '.atlas', 'dist', 'build'].some(exclude => relativePath.includes(exclude))) { continue; } if (item.isDirectory()) { await this.scanDirectoryForPatterns(fullPath, patterns, findings); } else if (item.isFile() && this.shouldScanForCode(item.name)) { await this.scanFileForPatterns(fullPath, patterns, findings); } } } catch (error) { // Skip directories we can't read } } private shouldScanForCode(filename: string): boolean { const codeExtensions = ['.js', '.ts', '.jsx', '.tsx', '.html', '.php', '.py']; const ext = path.extname(filename).toLowerCase(); return codeExtensions.includes(ext); } private async scanFileForPatterns( filePath: string, patterns: any[], findings: SecurityFinding[] ): Promise<void> { try { const content = await fs.readFile(filePath, 'utf-8'); const lines = content.split('\n'); lines.forEach((line, lineNumber) => { for (const pattern of patterns) { if (pattern.pattern.test(line)) { findings.push({ id: `code-${findings.length + 1}`, title: pattern.title, description: pattern.description, severity: pattern.severity, category: 'code', file: path.relative(process.cwd(), filePath), line: lineNumber + 1, recommendation: pattern.recommendation, }); } } }); } catch (error) { // Skip files we can't read } } private async checkPermissions(): Promise<SecurityFinding[]> { const findings: SecurityFinding[] = []; try { // Check for overly permissive files const sensitiveFiles = ['.env', '.env.local', 'id_rsa', 'id_ed25519', '.ssh/id_rsa']; for (const file of sensitiveFiles) { try { const stats = await fs.stat(path.join(process.cwd(), file)); const mode = stats.mode & parseInt('777', 8); if (mode & parseInt('044', 8)) { // Readable by group or others findings.push({ id: `perm-${findings.length + 1}`, title: 'Overly permissive file permissions', description: `File ${file} is readable by group or others`, severity: 'medium', category: 'permissions', file, recommendation: `Run: chmod 600 ${file}`, }); } } catch { // File doesn't exist, skip } } } catch (error) { // Handle permission check errors } return findings; } private async runNpmAudit(autoFix: boolean = false): Promise<VulnerabilityReport> { try { const command = autoFix ? 'npm audit fix --json' : 'npm audit --json'; const { stdout } = await execAsync(command); const auditResult = JSON.parse(stdout); return { total: auditResult.metadata?.vulnerabilities?.total || 0, critical: auditResult.metadata?.vulnerabilities?.critical || 0, high: auditResult.metadata?.vulnerabilities?.high || 0, moderate: auditResult.metadata?.vulnerabilities?.moderate || 0, low: auditResult.metadata?.vulnerabilities?.low || 0, lastChecked: new Date().toISOString(), details: this.parseNpmAuditDetails(auditResult), }; } catch (error) { // npm audit returns non-zero exit code when vulnerabilities found // Try to parse the error output try { const errorOutput = (error as any).stdout || '{}'; const auditResult = JSON.parse(errorOutput); return { total: auditResult.metadata?.vulnerabilities?.total || 0, critical: auditResult.metadata?.vulnerabilities?.critical || 0, high: auditResult.metadata?.vulnerabilities?.high || 0, moderate: auditResult.metadata?.vulnerabilities?.moderate || 0, low: auditResult.metadata?.vulnerabilities?.low || 0, lastChecked: new Date().toISOString(), details: this.parseNpmAuditDetails(auditResult), }; } catch { return { total: 0, critical: 0, high: 0, moderate: 0, low: 0, lastChecked: new Date().toISOString(), }; } } } private parseNpmAuditDetails(auditResult: any): any[] { const details: any[] = []; if (auditResult.vulnerabilities) { Object.entries(auditResult.vulnerabilities).forEach(([name, vuln]: [string, any]) => { details.push({ name: vuln.title || name, package: name, severity: vuln.severity, description: vuln.overview || 'No description available', recommendation: vuln.recommendation || 'Update to latest version', cve: vuln.cves?.[0], }); }); } return details.slice(0, 10); // Limit to top 10 } private mergeVulnerabilityReport(target: VulnerabilityReport, source: VulnerabilityReport): void { target.total += source.total; target.critical += source.critical; target.high += source.high; target.moderate += source.moderate; target.low += source.low; if (source.details) { target.details = [...(target.details || []), ...source.details]; } } private convertSecretsToFindings(secrets: SecretFinding[]): SecurityFinding[] { return secrets.map((secret, index) => ({ id: `secret-${index + 1}`, title: `Exposed ${secret.type}`, description: `Potential ${secret.type} found in source code`, severity: secret.severity as 'critical' | 'high' | 'medium' | 'low', category: 'secrets' as const, file: secret.file, line: secret.line, recommendation: 'Remove the secret and use environment variables or secure secret management', })); } private convertVulnerabilitiesToFindings(vulnReport: VulnerabilityReport): SecurityFinding[] { const findings: SecurityFinding[] = []; if (vulnReport.details) { vulnReport.details.forEach((vuln, index) => { findings.push({ id: `vuln-${index + 1}`, title: `Vulnerable dependency: ${vuln.name}`, description: vuln.description, severity: this.mapVulnerabilitySeverity(vuln.severity), category: 'dependencies', recommendation: vuln.recommendation, cwe: vuln.cve, }); }); } return findings; } private mapVulnerabilitySeverity(severity: string): 'critical' | 'high' | 'medium' | 'low' { switch (severity.toLowerCase()) { case 'critical': return 'critical'; case 'high': return 'high'; case 'moderate': return 'medium'; case 'low': return 'low'; default: return 'medium'; } } private calculateSummary(findings: SecurityFinding[], scanTime: number) { const critical = findings.filter(f => f.severity === 'critical').length; const high = findings.filter(f => f.severity === 'high').length; const medium = findings.filter(f => f.severity === 'medium').length; const low = findings.filter(f => f.severity === 'low').length; const status: 'pass' | 'warning' | 'fail' = critical > 0 ? 'fail' : high > 0 ? 'warning' : 'pass'; return { status, totalIssues: findings.length, critical, high, medium, low, scanTime, }; } private generateRecommendations(findings: SecurityFinding[]): string[] { const recommendations = new Set<string>(); if (findings.some(f => f.category === 'secrets')) { recommendations.add('Implement proper secret management using environment variables'); recommendations.add('Add secret scanning to your CI/CD pipeline'); } if (findings.some(f => f.category === 'dependencies')) { recommendations.add('Regularly update dependencies to patch vulnerabilities'); recommendations.add('Enable automated dependency scanning'); } if (findings.some(f => f.category === 'code')) { recommendations.add('Review and fix code security issues'); recommendations.add('Implement secure coding practices'); } recommendations.add('Set up automated security scanning in pre-commit hooks'); recommendations.add('Create a security policy and incident response plan'); return Array.from(recommendations); } private generateOverviewSection(scanResult: SecurityScanResult): string { return `## Security Overview **Scan Date**: ${scanResult.metadata.scanDate} **Scan Type**: ${scanResult.metadata.scanType} **Overall Status**: ${scanResult.summary.status === 'pass' ? '✅ PASS' : scanResult.summary.status === 'warning' ? '⚠️ WARNING' : '❌ FAIL'} ### Summary - **Total Issues**: ${scanResult.summary.totalIssues} - **Critical**: ${scanResult.summary.critical} - **High**: ${scanResult.summary.high} - **Medium**: ${scanResult.summary.medium} - **Low**: ${scanResult.summary.low} ### Risk Assessment ${scanResult.summary.critical > 0 ? '🔴 **HIGH RISK**: Critical vulnerabilities require immediate attention' : scanResult.summary.high > 0 ? '🟠 **MEDIUM RISK**: High severity issues should be addressed promptly' : scanResult.summary.medium > 0 ? '🟡 **LOW RISK**: Medium severity issues should be planned for resolution' : '🟢 **MINIMAL RISK**: No critical security issues detected'}`; } private generateDetailsSection(scanResult: SecurityScanResult): string { if (scanResult.findings.length === 0) { return '## Detailed Findings\n\n✅ No security issues found in this scan.'; } const sections = []; sections.push('## Detailed Findings\n'); const categorized = this.categorizeFindings(scanResult.findings); Object.entries(categorized).forEach(([category, findings]) => { if (findings.length > 0) { sections.push(`### ${this.getCategoryTitle(category)}\n`); findings.forEach((finding, i) => { const emoji = this.getSeverityEmoji(finding.severity); sections.push(`${i + 1}. ${emoji} **${finding.title}** (${finding.severity})`); sections.push(` - **Description**: ${finding.description}`); if (finding.file) sections.push(` - **File**: ${finding.file}${finding.line ? `:${finding.line}` : ''}`); if (finding.recommendation) sections.push(` - **Recommendation**: ${finding.recommendation}`); sections.push(''); }); } }); return sections.join('\n'); } private generateRecommendationsSection(scanResult: SecurityScanResult): string { const recommendations = scanResult.recommendations || []; if (recommendations.length === 0) { return '## Recommendations\n\n✅ No specific recommendations at this time. Continue following security best practices.'; } return `## Recommendations ${recommendations.map((rec, i) => `${i + 1}. ${rec}`).join('\n')} ### Additional Resources - [OWASP Top 10](https://owasp.org/www-project-top-ten/) - [NIST Cybersecurity Framework](https://www.nist.gov/cyberframework) - [Security Code Review Guidelines](https://owasp.org/www-project-code-review-guide/)`; } private categorizeFindings(findings: SecurityFinding[]): Record<string, SecurityFinding[]> { return findings.reduce((acc, finding) => { if (!acc[finding.category]) acc[finding.category] = []; acc[finding.category].push(finding); return acc; }, {} as Record<string, SecurityFinding[]>); } private getCategoryTitle(category: string): string { const titles: Record<string, string> = { secrets: '🔐 Secrets and Credentials', dependencies: '📦 Dependency Vulnerabilities', code: '💻 Code Security Issues', permissions: '🔒 File Permissions', configuration: '⚙️ Configuration Issues', }; return titles[category] || category; } private getSeverityEmoji(severity: string): string { switch (severity) { case 'critical': return '🔴'; case 'high': return '🟠'; case 'medium': return '🟡'; case 'low': return '🔵'; default: return '⚪'; } } private async createSecurityPolicy(): Promise<void> { const policy = `# Security Policy ## Supported Versions We release patches for security vulnerabilities. Which versions are eligible for receiving such patches depends on the CVSS v3.0 Rating: | Version | Supported | | ------- | ------------------ | | 1.x.x | :white_check_mark: | ## Reporting a Vulnerability If you discover a security vulnerability, please follow these steps: 1. **Do not** open a public issue 2. Send an email to [security@yourproject.com] with: - Description of the vulnerability - Steps to reproduce - Potential impact - Any suggested fixes ## Security Measures This project implements the following security measures: - Automated dependency scanning - Secret detection in CI/CD - Code security analysis - Regular security audits ## Response Timeline - Initial response: Within 48 hours - Status update: Within 1 week - Resolution: Varies based on severity ## Security Best Practices - Keep dependencies up to date - Use environment variables for secrets - Follow secure coding practices - Enable two-factor authentication - Regularly review access permissions `; await fs.writeFile(path.join(process.cwd(), 'SECURITY.md'), policy); } private async setupSecretDetection(): Promise<void> { const gitleaksConfig = `# Gitleaks configuration title = "Gitleaks Config" [[rules]] id = "aws-access-key" description = "AWS Access Key" regex = '''AKIA[0-9A-Z]{16}''' [[rules]] id = "github-token" description = "GitHub Token" regex = '''ghp_[0-9a-zA-Z]{36}''' [[rules]] id = "api-key" description = "Generic API Key" regex = '''(?i)api[_-]?key[_-]?[=:]\s*['\"]?[0-9a-zA-Z]{20,}['\"]?''' [allowlist] paths = [ "README.md", ".gitleaks.toml" ] `; await fs.writeFile(path.join(process.cwd(), '.gitleaks.toml'), gitleaksConfig); } private async setupVulnerabilityChecks(): Promise<void> { const workflowPath = path.join(process.cwd(), '.github', 'workflows', 'security.yml'); await fs.mkdir(path.dirname(workflowPath), { recursive: true }); const workflow = `name: Security Scan on: push: branches: [ main, develop ] pull_request: branches: [ main ] schedule: - cron: '0 2 * * 1' # Weekly on Monday jobs: security: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Run Gitleaks uses: actions/checkout@v4 - uses: gitleaks/gitleaks-action@v2 env: GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }} - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '18' cache: 'npm' - name: Install dependencies run: npm ci - name: Run npm audit run: npm audit --audit-level moderate `; await fs.writeFile(workflowPath, workflow); } private async setupSecurityGitHooks(): Promise<void> { const hookPath = path.join(process.cwd(), '.git', 'hooks', 'pre-commit'); const hook = `#!/bin/sh # Security pre-commit hook echo "Running security checks..." # Check for secrets with gitleaks if command -v gitleaks >/dev/null 2>&1; then gitleaks detect --staged --verbose if [ $? -ne 0 ]; then echo "❌ Secrets detected! Please remove them before committing." exit 1 fi else echo "⚠️ Gitleaks not installed. Skipping secret detection." fi # Run npm audit if package.json exists if [ -f "package.json" ]; then npm audit --audit-level moderate if [ $? -ne 0 ]; then echo "❌ Security vulnerabilities found! Please fix them before committing." exit 1 fi fi echo "✅ Security checks passed!" `; await fs.writeFile(hookPath, hook, { mode: 0o755 }); } private async setupPreCommitConfig(): Promise<void> { const config = `repos: - repo: https://github.com/gitleaks/gitleaks rev: v8.16.1 hooks: - id: gitleaks - repo: local hooks: - id: npm-audit name: npm audit entry: npm audit --audit-level moderate language: system pass_filenames: false files: package.json `; await fs.writeFile(path.join(process.cwd(), '.pre-commit-config.yaml'), config); } async checkDataAccess(options: { scanPath: string; excludePaths: string[]; includeWarnings: boolean; includeSuggestions: boolean; }): Promise<any[]> { const violations: any[] = []; const atlasAccessPatterns = [ { pattern: /\.atlas\//, type: 'Direct Path Access', severity: 'high', description: 'Direct access to .atlas/ directory detected', suggestion: 'Use MCP tools instead of direct file access' }, { pattern: /fs\.read.*\(['"`]\.atlas/, type: 'File System Read', severity: 'critical', description: 'Reading .atlas files directly with fs module', suggestion: 'Use appropriate MCP tool for data access' }, { pattern: /fs\.write.*\(['"`]\.atlas/, type: 'File System Write', severity: 'critical', description: 'Writing to .atlas files directly', suggestion: 'Use MCP tools to modify data' }, { pattern: /require\(['"`]\.atlas/, type: 'Direct Require', severity: 'high', description: 'Requiring .atlas files directly', suggestion: 'Use MCP data access tools' }, { pattern: /import.*from ['"`]\.atlas/, type: 'Direct Import', severity: 'high', description: 'Importing from .atlas directory', suggestion: 'Use MCP API instead of direct imports' }, { pattern: /JSON\.parse.*\.atlas/, type: 'Direct JSON Parsing', severity: 'medium', description: 'Parsing .atlas JSON files directly', suggestion: 'Use MCP tools that return parsed data' } ]; try { await this.scanDirectoryForDataAccess( options.scanPath, options.excludePaths, atlasAccessPatterns, violations ); } catch (error) { // Handle errors gracefully } return violations; } private async scanDirectoryForDataAccess( dirPath: string, excludePaths: string[], patterns: any[], violations: any[] ): Promise<void> { try { const items = await fs.readdir(dirPath, { withFileTypes: true }); for (const item of items) { const fullPath = path.join(dirPath, item.name); const relativePath = path.relative(process.cwd(), fullPath); // Skip excluded paths if (excludePaths.some(exclude => relativePath.includes(exclude))) { continue; } if (item.isDirectory()) { await this.scanDirectoryForDataAccess(fullPath, excludePaths, patterns, violations); } else if (item.isFile() && this.shouldScanForDataAccess(item.name)) { await this.scanFileForDataAccess(fullPath, patterns, violations); } } } catch (error) { // Skip directories we can't read } } private shouldScanForDataAccess(filename: string): boolean { const extensions = ['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs']; const ext = path.extname(filename).toLowerCase(); return extensions.includes(ext); } private async scanFileForDataAccess( filePath: string, patterns: any[], violations: any[] ): Promise<void> { try { const content = await fs.readFile(filePath, 'utf-8'); const lines = content.split('\n'); const relativePath = path.relative(process.cwd(), filePath); lines.forEach((line, index) => { patterns.forEach(patternDef => { if (patternDef.pattern.test(line)) { violations.push({ file: relativePath, line: index + 1, type: patternDef.type, severity: patternDef.severity, description: patternDef.description, suggestion: patternDef.suggestion, codeSnippet: line.trim() }); } }); }); } catch (error) { // Skip files we can't read } } }