@boundless-oss/atlas
Version:
Atlas - MCP Server for comprehensive startup project management
1,018 lines (874 loc) • 31.9 kB
text/typescript
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
}
}
}