pentest-scanner.ts•12.9 kB
// import * as url from 'url'; // Uncomment if URL parsing needed
import type { ScanRequest, ScanResult, Finding } from '../types/index.js';
import type { BoundaryEnforcer } from '../boundaries/enforcer.js';
import { DockerOrchestrator, type ScannerConfig } from './docker-orchestrator.js';
export class PentestScanner {
private orchestrator: DockerOrchestrator;
constructor(private boundaryEnforcer: BoundaryEnforcer) {
const projectScope = this.boundaryEnforcer.getProjectScope();
if (!projectScope) {
throw new Error('Project scope not initialized');
}
this.orchestrator = new DockerOrchestrator(projectScope);
}
async scan(request: ScanRequest): Promise<ScanResult> {
const scanId = this.generateScanId();
const startTime = Date.now();
// Validate URL boundaries
const validation = await this.boundaryEnforcer.validateUrl(request.target);
if (!validation.allowed) {
throw new Error(`URL boundary violation: ${validation.reason}`);
}
console.error(`Starting pentest scan: ${scanId}`);
const allFindings: Finding[] = [];
const errors: string[] = [];
let tokenUsage = 0;
// Determine test types to run
const testTypes = request.tools || this.getDefaultTests(request.profile);
try {
// Run OWASP ZAP scan
if (testTypes.includes('zap') || testTypes.includes('web_scan')) {
const zapResults = await this.runZAP(request.target, testTypes);
allFindings.push(...zapResults.findings);
tokenUsage += zapResults.tokenUsage;
}
// Run specific vulnerability tests
if (testTypes.includes('sql_injection')) {
const sqlResults = await this.testSQLInjection(request.target);
allFindings.push(...sqlResults.findings);
tokenUsage += sqlResults.tokenUsage;
}
if (testTypes.includes('xss')) {
const xssResults = await this.testXSS(request.target);
allFindings.push(...xssResults.findings);
tokenUsage += xssResults.tokenUsage;
}
if (testTypes.includes('security_headers')) {
const headerResults = await this.testSecurityHeaders(request.target);
allFindings.push(...headerResults.findings);
tokenUsage += headerResults.tokenUsage;
}
} catch (error) {
const errorMsg = `Pentest failed: ${error instanceof Error ? error.message : 'Unknown error'}`;
console.error(errorMsg);
errors.push(errorMsg);
}
// Calculate summary
const summary = this.calculateSummary(allFindings);
const result: ScanResult = {
scanId,
status: errors.length === 0 ? 'success' : (allFindings.length > 0 ? 'partial' : 'failed'),
summary,
findings: allFindings,
tokenUsage,
scanTimeMs: Date.now() - startTime,
errors: errors.length > 0 ? errors : undefined,
};
console.error(`Pentest scan completed: ${result.status}, ${allFindings.length} findings`);
return result;
}
private async runZAP(targetUrl: string, testTypes: string[]): Promise<{ findings: Finding[]; tokenUsage: number }> {
const zapConfig = this.getZAPConfig(testTypes);
const config: ScannerConfig = {
image: 'owasp/zap2docker-stable:latest',
command: [
'zap-baseline.py',
'-t', targetUrl,
'-J', '/zap/wrk/zap-results.json',
'-j', // Use JSON output
'-a', // Include all tests
...zapConfig.additionalArgs
],
environment: {
ZAP_PORT: '8090',
},
volumes: [],
resourceLimits: {
memory: 4 * 1024 * 1024 * 1024, // 4GB
cpus: 2,
pidsLimit: 300,
},
timeout: zapConfig.timeout,
networkMode: 'project_network', // Allow access to project apps
};
const result = await this.orchestrator.runScanner('zap', config, '');
// ZAP returns different exit codes for different scenarios
// 0 = no alerts, 1 = alerts found, 2 = warnings, 3 = errors
if (result.exitCode > 3) {
throw new Error(`ZAP failed with exit code ${result.exitCode}: ${result.stderr}`);
}
return this.parseZAPOutput(result.stdout, targetUrl);
}
private async testSQLInjection(targetUrl: string): Promise<{ findings: Finding[]; tokenUsage: number }> {
// Use a lightweight SQL injection test (not full SQLMap for basic tests)
const config: ScannerConfig = {
image: 'alpine/curl:latest',
command: [
'sh', '-c',
`
# Basic SQL injection tests
echo "Testing SQL injection endpoints..."
# Test common injection points
curl -s "${targetUrl}?id=1' OR '1'='1" -o /tmp/sqli_test1.html
curl -s "${targetUrl}?search=admin'--" -o /tmp/sqli_test2.html
curl -s -X POST "${targetUrl}" -d "username=admin' OR '1'='1'--&password=test" -o /tmp/sqli_test3.html
# Check for SQL error messages
grep -i "sql\\|mysql\\|error\\|warning" /tmp/sqli_test*.html || echo "No SQL errors detected"
`
],
environment: {},
volumes: [],
resourceLimits: {
memory: 256 * 1024 * 1024, // 256MB
cpus: 1,
pidsLimit: 10,
},
timeout: 60000, // 1 minute
};
const result = await this.orchestrator.runScanner('sql-test', config, '');
return this.parseSQLTestOutput(result.stdout, targetUrl);
}
private async testXSS(targetUrl: string): Promise<{ findings: Finding[]; tokenUsage: number }> {
const config: ScannerConfig = {
image: 'alpine/curl:latest',
command: [
'sh', '-c',
`
# Basic XSS tests
echo "Testing XSS vulnerabilities..."
# Test reflected XSS
curl -s "${targetUrl}?q=<script>alert('XSS')</script>" -o /tmp/xss_test1.html
curl -s "${targetUrl}?search=<img src=x onerror=alert('XSS')>" -o /tmp/xss_test2.html
# Check if payload is reflected
grep -i "script\\|onerror\\|onload" /tmp/xss_test*.html || echo "No XSS reflection detected"
`
],
environment: {},
volumes: [],
resourceLimits: {
memory: 128 * 1024 * 1024, // 128MB
cpus: 1,
pidsLimit: 10,
},
timeout: 60000, // 1 minute
};
const result = await this.orchestrator.runScanner('xss-test', config, '');
return this.parseXSSTestOutput(result.stdout, targetUrl);
}
private async testSecurityHeaders(targetUrl: string): Promise<{ findings: Finding[]; tokenUsage: number }> {
const config: ScannerConfig = {
image: 'alpine/curl:latest',
command: [
'sh', '-c',
`
# Test security headers
echo "Testing security headers..."
curl -I -s "${targetUrl}" | grep -E "(X-Frame-Options|Content-Security-Policy|X-XSS-Protection|X-Content-Type-Options|Strict-Transport-Security)" || echo "MISSING_SECURITY_HEADERS"
`
],
environment: {},
volumes: [],
resourceLimits: {
memory: 64 * 1024 * 1024, // 64MB
cpus: 0.5,
pidsLimit: 5,
},
timeout: 30000, // 30 seconds
};
const result = await this.orchestrator.runScanner('headers-test', config, '');
return this.parseSecurityHeadersOutput(result.stdout, targetUrl);
}
private getZAPConfig(testTypes: string[]): { additionalArgs: string[]; timeout: number } {
const config = {
additionalArgs: [] as string[],
timeout: 1800000, // 30 minutes default
};
if (testTypes.includes('quick') || testTypes.some(t => t === 'quick')) {
config.additionalArgs.push('-l', 'Informational'); // Low detail
config.timeout = 300000; // 5 minutes
} else if (testTypes.includes('thorough')) {
config.additionalArgs.push('-l', 'High'); // High detail
config.timeout = 3600000; // 1 hour
}
// Add specific test configurations
if (testTypes.includes('api_security')) {
config.additionalArgs.push('-z', '-config api.disablekey=true');
}
if (testTypes.includes('authentication')) {
config.additionalArgs.push('-z', '-config scanner.strength=High');
}
return config;
}
private parseZAPOutput(output: string, targetUrl: string): { findings: Finding[]; tokenUsage: number } {
const findings: Finding[] = [];
try {
// ZAP baseline output includes results in the stdout
const lines = output.split('\n');
for (const line of lines) {
if (line.includes('WARN') || line.includes('FAIL')) {
const severity = line.includes('FAIL') ? 'high' : 'medium';
const description = line.trim();
findings.push({
id: `zap_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`,
type: 'web_vulnerability',
severity: severity as 'high' | 'medium',
title: this.extractZAPTitle(line),
description,
location: {
endpoint: targetUrl,
},
remediation: this.getZAPRemediation(line),
});
}
}
} catch (error) {
console.error('Failed to parse ZAP output:', error);
}
return {
findings,
tokenUsage: Math.min(findings.length * 30 + 100, 500)
};
}
private parseSQLTestOutput(output: string, targetUrl: string): { findings: Finding[]; tokenUsage: number } {
const findings: Finding[] = [];
if (output.includes('sql') || output.includes('mysql') || output.includes('error')) {
findings.push({
id: `sql_test_${Date.now()}`,
type: 'sql_injection',
severity: 'high',
title: 'Potential SQL Injection Vulnerability',
description: 'SQL error messages detected in application response',
location: {
endpoint: targetUrl,
},
remediation: 'Use parameterized queries and input validation',
});
}
return {
findings,
tokenUsage: 50
};
}
private parseXSSTestOutput(output: string, targetUrl: string): { findings: Finding[]; tokenUsage: number } {
const findings: Finding[] = [];
if (output.includes('script') || output.includes('onerror') || output.includes('onload')) {
findings.push({
id: `xss_test_${Date.now()}`,
type: 'cross_site_scripting',
severity: 'high',
title: 'Potential XSS Vulnerability',
description: 'Script tags or event handlers detected in application response',
location: {
endpoint: targetUrl,
},
remediation: 'Implement proper input validation and output encoding',
});
}
return {
findings,
tokenUsage: 40
};
}
private parseSecurityHeadersOutput(output: string, targetUrl: string): { findings: Finding[]; tokenUsage: number } {
const findings: Finding[] = [];
if (output.includes('MISSING_SECURITY_HEADERS')) {
findings.push({
id: `headers_test_${Date.now()}`,
type: 'security_misconfiguration',
severity: 'medium',
title: 'Missing Security Headers',
description: 'Important security headers are missing from the application response',
location: {
endpoint: targetUrl,
},
remediation: 'Add security headers: X-Frame-Options, CSP, X-XSS-Protection, etc.',
});
}
return {
findings,
tokenUsage: 30
};
}
private extractZAPTitle(line: string): string {
// Extract meaningful title from ZAP output line
const match = line.match(/\[(.*?)\]/);
return match ? match[1] : 'Web Security Issue';
}
private getZAPRemediation(line: string): string {
if (line.includes('X-Frame-Options')) {
return 'Add X-Frame-Options header to prevent clickjacking';
}
if (line.includes('Content-Security-Policy')) {
return 'Implement Content Security Policy to prevent XSS attacks';
}
if (line.includes('HTTPS')) {
return 'Enforce HTTPS and implement HSTS';
}
return 'Review and fix the identified security issue';
}
private calculateSummary(findings: Finding[]) {
const summary = {
vulnerabilities: findings.length,
critical: 0,
high: 0,
medium: 0,
low: 0,
informational: 0,
};
for (const finding of findings) {
summary[finding.severity]++;
}
return summary;
}
private getDefaultTests(profile?: string): string[] {
switch (profile) {
case 'quick':
return ['security_headers'];
case 'thorough':
return ['zap', 'sql_injection', 'xss', 'security_headers'];
default: // standard
return ['zap', 'security_headers'];
}
}
private generateScanId(): string {
return `pentest_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
async cleanup(): Promise<void> {
await this.orchestrator.cleanup();
}
}