/**
 * OWASP ZAP Scanner Integration
 *
 * Integrates with OWASP ZAP (Zed Attack Proxy) for dynamic
 * application security testing.
 *
 * @module scanners/dast/zap
 */
import spawn from "cross-spawn";
import { logger } from "../../logger.js";
import { ZAP_RISK_MAPPING, ZAP_CONFIDENCE_MAPPING, } from "./types.js";
/**
 * ZAP API endpoint (default local)
 */
const ZAP_API_URL = process.env.ZAP_API_URL || "http://localhost:8080";
const ZAP_API_KEY = process.env.ZAP_API_KEY || "";
/**
 * Check if ZAP is available
 */
export async function checkZapAvailable() {
    return new Promise((resolve) => {
        // Try docker first
        const dockerChild = spawn("docker", ["images", "owasp/zap2docker-stable", "--format", "{{.Repository}}"], {
            timeout: 10000,
        });
        let dockerOutput = "";
        dockerChild.stdout?.on("data", (data) => {
            dockerOutput += data.toString();
        });
        dockerChild.on("close", (dockerCode) => {
            if (dockerCode === 0 && dockerOutput.includes("owasp/zap2docker-stable")) {
                resolve({
                    scanner: "zap",
                    available: true,
                    version: "docker",
                    path: "docker",
                    features: {
                        passiveScan: true,
                        activeScan: true,
                        apiScan: true,
                        authentication: true,
                    },
                });
                return;
            }
            // Try zap-cli
            const cliChild = spawn("zap-cli", ["--version"], { timeout: 5000 });
            let cliVersion = "";
            cliChild.stdout?.on("data", (data) => {
                cliVersion += data.toString();
            });
            cliChild.on("close", (cliCode) => {
                if (cliCode === 0) {
                    resolve({
                        scanner: "zap",
                        available: true,
                        version: cliVersion.trim(),
                        path: "zap-cli",
                        features: {
                            passiveScan: true,
                            activeScan: true,
                            apiScan: true,
                            authentication: true,
                        },
                    });
                    return;
                }
                // Try checking if ZAP API is running
                checkZapApi().then((apiAvailable) => {
                    if (apiAvailable) {
                        resolve({
                            scanner: "zap",
                            available: true,
                            version: "api",
                            path: ZAP_API_URL,
                            features: {
                                passiveScan: true,
                                activeScan: true,
                                apiScan: true,
                                authentication: true,
                            },
                        });
                    }
                    else {
                        resolve({
                            scanner: "zap",
                            available: false,
                            error: "ZAP not found. Install via Docker (owasp/zap2docker-stable), zap-cli, or start ZAP with API enabled.",
                        });
                    }
                });
            });
            cliChild.on("error", () => {
                checkZapApi().then((apiAvailable) => {
                    if (apiAvailable) {
                        resolve({
                            scanner: "zap",
                            available: true,
                            version: "api",
                            path: ZAP_API_URL,
                            features: {
                                passiveScan: true,
                                activeScan: true,
                                apiScan: true,
                                authentication: true,
                            },
                        });
                    }
                    else {
                        resolve({
                            scanner: "zap",
                            available: false,
                            error: "ZAP not found. Install via Docker (owasp/zap2docker-stable), zap-cli, or start ZAP with API enabled.",
                        });
                    }
                });
            });
        });
        dockerChild.on("error", () => {
            // Docker not available, try other methods
            const cliChild = spawn("zap-cli", ["--version"], { timeout: 5000 });
            cliChild.on("close", (code) => {
                if (code === 0) {
                    resolve({
                        scanner: "zap",
                        available: true,
                        path: "zap-cli",
                        features: {
                            passiveScan: true,
                            activeScan: true,
                            apiScan: true,
                            authentication: true,
                        },
                    });
                }
                else {
                    resolve({
                        scanner: "zap",
                        available: false,
                        error: "ZAP not found",
                    });
                }
            });
            cliChild.on("error", () => {
                resolve({
                    scanner: "zap",
                    available: false,
                    error: "ZAP not found",
                });
            });
        });
    });
}
/**
 * Check if ZAP API is running
 */
async function checkZapApi() {
    try {
        const url = `${ZAP_API_URL}/JSON/core/view/version/?apikey=${ZAP_API_KEY}`;
        const response = await fetch(url, { signal: AbortSignal.timeout(5000) });
        return response.ok;
    }
    catch {
        return false;
    }
}
/**
 * Run ZAP scan using Docker
 */
async function runZapDocker(target, policy) {
    return new Promise((resolve) => {
        const args = [
            "run",
            "--rm",
            "-t",
        ];
        // Add network host for local targets
        if (target.url.includes("localhost") || target.url.includes("127.0.0.1")) {
            args.push("--network", "host");
        }
        args.push("owasp/zap2docker-stable");
        // Choose scan type based on policy
        if (policy.passiveOnly) {
            args.push("zap-baseline.py");
        }
        else {
            args.push("zap-full-scan.py");
        }
        args.push("-t", target.url);
        args.push("-J", "zap-report.json"); // JSON output
        args.push("-I"); // Don't fail on warnings
        if (policy.maxDuration) {
            args.push("-m", String(Math.floor(policy.maxDuration / 60))); // Minutes
        }
        if (policy.ajaxSpider) {
            args.push("-j"); // Enable AJAX spider
        }
        logger.info("zap.docker_scan", { target: target.url, passiveOnly: policy.passiveOnly });
        const child = spawn("docker", args, {
            timeout: (policy.maxDuration || 300) * 1000 + 60000, // Add 1 min buffer
        });
        let stdout = "";
        let stderr = "";
        child.stdout?.on("data", (data) => {
            stdout += data.toString();
        });
        child.stderr?.on("data", (data) => {
            stderr += data.toString();
        });
        child.on("close", (code) => {
            // ZAP returns non-zero for warnings, which is OK
            resolve({
                success: code === 0 || code === 1 || code === 2,
                output: stdout,
                error: code !== 0 && code !== 1 && code !== 2 ? stderr : undefined,
            });
        });
        child.on("error", (error) => {
            resolve({
                success: false,
                output: stdout,
                error: String(error),
            });
        });
    });
}
/**
 * Run ZAP scan via API
 */
async function runZapApi(target, policy) {
    try {
        const baseUrl = ZAP_API_URL;
        const apiKey = ZAP_API_KEY;
        // Start spider
        logger.info("zap.api_spider", { target: target.url });
        const spiderResponse = await fetch(`${baseUrl}/JSON/spider/action/scan/?apikey=${apiKey}&url=${encodeURIComponent(target.url)}&maxChildren=${policy.maxChildren || 10}&recurse=true`, { signal: AbortSignal.timeout(30000) });
        if (!spiderResponse.ok) {
            throw new Error(`Spider failed: ${spiderResponse.statusText}`);
        }
        const spiderResult = await spiderResponse.json();
        const scanId = spiderResult.scan;
        // Wait for spider to complete
        let spiderStatus = "0";
        const startTime = Date.now();
        const maxWait = (policy.maxDuration || 300) * 1000;
        while (spiderStatus !== "100" && Date.now() - startTime < maxWait) {
            await new Promise((r) => setTimeout(r, 2000));
            const statusResponse = await fetch(`${baseUrl}/JSON/spider/view/status/?apikey=${apiKey}&scanId=${scanId}`);
            const statusResult = await statusResponse.json();
            spiderStatus = statusResult.status;
        }
        // Run passive scan (always)
        logger.info("zap.api_passive_scan", { target: target.url });
        // Wait for passive scan
        let pscanRecords = "1";
        while (pscanRecords !== "0" && Date.now() - startTime < maxWait) {
            await new Promise((r) => setTimeout(r, 2000));
            const pscanResponse = await fetch(`${baseUrl}/JSON/pscan/view/recordsToScan/?apikey=${apiKey}`);
            const pscanResult = await pscanResponse.json();
            pscanRecords = pscanResult.recordsToScan;
        }
        // Run active scan if not passive-only
        if (!policy.passiveOnly) {
            logger.info("zap.api_active_scan", { target: target.url });
            const ascanResponse = await fetch(`${baseUrl}/JSON/ascan/action/scan/?apikey=${apiKey}&url=${encodeURIComponent(target.url)}&recurse=true&scanPolicyName=${policy.zapPolicy || ""}`, { signal: AbortSignal.timeout(30000) });
            if (ascanResponse.ok) {
                const ascanResult = await ascanResponse.json();
                const ascanId = ascanResult.scan;
                // Wait for active scan
                let ascanStatus = "0";
                while (ascanStatus !== "100" && Date.now() - startTime < maxWait) {
                    await new Promise((r) => setTimeout(r, 5000));
                    const statusResponse = await fetch(`${baseUrl}/JSON/ascan/view/status/?apikey=${apiKey}&scanId=${ascanId}`);
                    const statusResult = await statusResponse.json();
                    ascanStatus = statusResult.status;
                }
            }
        }
        // Get alerts
        const alertsResponse = await fetch(`${baseUrl}/JSON/core/view/alerts/?apikey=${apiKey}&baseurl=${encodeURIComponent(target.url)}&start=0&count=1000`);
        const alertsResult = await alertsResponse.json();
        return {
            success: true,
            alerts: alertsResult.alerts || [],
        };
    }
    catch (error) {
        return {
            success: false,
            alerts: [],
            error: String(error),
        };
    }
}
/**
 * Parse ZAP JSON output to DASTFinding
 */
export function parseZapAlerts(alerts) {
    return alerts.map((alert) => ({
        scanner: "zap",
        ruleId: `zap-${alert.pluginId}`,
        name: alert.name || alert.alert,
        description: alert.description,
        severity: ZAP_RISK_MAPPING[alert.risk] || "info",
        confidence: ZAP_CONFIDENCE_MAPPING[alert.confidence] || 50,
        url: alert.url,
        method: alert.method,
        parameter: alert.param,
        evidence: alert.evidence,
        attack: alert.attack,
        cweIds: alert.cweid ? [`CWE-${alert.cweid}`] : undefined,
        references: alert.reference ? alert.reference.split("\n").filter(Boolean) : undefined,
        solution: alert.solution,
        tags: Object.keys(alert.tags || {}),
        timestamp: new Date().toISOString(),
        rawOutput: alert,
    }));
}
/**
 * Run ZAP scan
 */
export async function runZap(target, policy = {}) {
    const startTime = new Date();
    const mergedPolicy = { ...{ passiveOnly: true, maxDuration: 300 }, ...policy };
    logger.info("zap.scan_start", {
        target: target.url,
        passiveOnly: mergedPolicy.passiveOnly,
    });
    // Check availability
    const availability = await checkZapAvailable();
    if (!availability.available) {
        return {
            scanner: "zap",
            target,
            findings: [],
            duration: Date.now() - startTime.getTime(),
            success: false,
            error: availability.error || "ZAP not available",
            stats: {
                requestCount: 0,
                urlsDiscovered: 0,
                uniqueFindings: 0,
                bySeverity: {},
            },
            startTime: startTime.toISOString(),
            endTime: new Date().toISOString(),
            policy: mergedPolicy,
        };
    }
    let findings = [];
    let success = false;
    let error;
    // Run scan based on available method
    if (availability.path === "docker") {
        const result = await runZapDocker(target, mergedPolicy);
        success = result.success;
        error = result.error;
        // Parse JSON output if available
        if (result.output) {
            try {
                // Try to extract JSON from output
                const jsonMatch = result.output.match(/\{[\s\S]*"alerts"[\s\S]*\}/);
                if (jsonMatch) {
                    const parsed = JSON.parse(jsonMatch[0]);
                    if (parsed.site?.[0]?.alerts) {
                        findings = parseZapAlerts(parsed.site[0].alerts);
                    }
                }
            }
            catch {
                // JSON parsing failed, try line-by-line parsing
                logger.debug("zap.parse_failed", { output: result.output.slice(0, 500) });
            }
        }
    }
    else if (availability.path === ZAP_API_URL || availability.version === "api") {
        const result = await runZapApi(target, mergedPolicy);
        success = result.success;
        error = result.error;
        findings = parseZapAlerts(result.alerts);
    }
    else {
        // zap-cli not fully implemented, fallback to API check
        const apiResult = await runZapApi(target, mergedPolicy);
        success = apiResult.success;
        error = apiResult.error;
        findings = parseZapAlerts(apiResult.alerts);
    }
    const endTime = new Date();
    // Calculate stats
    const bySeverity = {};
    for (const finding of findings) {
        bySeverity[finding.severity] = (bySeverity[finding.severity] || 0) + 1;
    }
    const result = {
        scanner: "zap",
        target,
        findings,
        duration: endTime.getTime() - startTime.getTime(),
        success,
        error,
        stats: {
            requestCount: 0, // Not available from ZAP output
            urlsDiscovered: new Set(findings.map((f) => f.url)).size,
            uniqueFindings: findings.length,
            bySeverity,
        },
        version: availability.version,
        startTime: startTime.toISOString(),
        endTime: endTime.toISOString(),
        policy: mergedPolicy,
    };
    logger.info("zap.scan_complete", {
        findings: findings.length,
        duration: result.duration,
        success,
    });
    return result;
}
/**
 * Get ZAP installation instructions
 */
export function getZapInstallInstructions() {
    return `
# OWASP ZAP Installation

## Docker (Recommended)
\`\`\`bash
docker pull owasp/zap2docker-stable
\`\`\`

## macOS
\`\`\`bash
brew install --cask owasp-zap
\`\`\`

## Linux
\`\`\`bash
# Download from https://www.zaproxy.org/download/
wget https://github.com/zaproxy/zaproxy/releases/download/v2.14.0/ZAP_2.14.0_Linux.tar.gz
tar -xzf ZAP_2.14.0_Linux.tar.gz
\`\`\`

## Windows
Download installer from https://www.zaproxy.org/download/

## Start ZAP with API
\`\`\`bash
zap.sh -daemon -port 8080 -config api.key=your-api-key
\`\`\`
`;
}
//# sourceMappingURL=zap.js.map