/**
 * DAST Scanner Module
 *
 * Aggregates OWASP ZAP and Nuclei for dynamic application
 * security testing.
 *
 * @module scanners/dast
 */
import { logger } from "../../logger.js";
import { DEFAULT_DAST_POLICY } from "./types.js";
import { checkZapAvailable, runZap, getZapInstallInstructions } from "./zap.js";
import { checkNucleiAvailable, runNuclei, getNucleiInstallInstructions } from "./nuclei.js";
// Re-export types
export * from "./types.js";
// Re-export individual scanners
export { checkZapAvailable, runZap, getZapInstallInstructions } from "./zap.js";
export { checkNucleiAvailable, runNuclei, getNucleiInstallInstructions } from "./nuclei.js";
/**
 * Check availability of all DAST tools
 */
export async function checkDASTAvailability() {
    const [zapResult, nucleiResult] = await Promise.all([
        checkZapAvailable(),
        checkNucleiAvailable(),
    ]);
    return [zapResult, nucleiResult];
}
/**
 * Get installation instructions for missing DAST tools
 */
export function getDASTInstallInstructions(availability) {
    const instructions = [];
    for (const scanner of availability) {
        if (!scanner.available) {
            if (scanner.scanner === "zap") {
                instructions.push(getZapInstallInstructions());
            }
            else if (scanner.scanner === "nuclei") {
                instructions.push(getNucleiInstallInstructions());
            }
        }
    }
    if (instructions.length === 0) {
        return "All DAST tools are installed and available.";
    }
    return instructions.join("\n---\n");
}
/**
 * Deduplicate findings based on URL and rule pattern
 */
function deduplicateFindings(findings) {
    const seen = new Map();
    for (const finding of findings) {
        // Create a unique key based on URL, severity, and rule pattern
        const normalizedRuleId = finding.ruleId
            .replace(/^zap-/, "")
            .replace(/^nuclei-/, "")
            .toLowerCase();
        const key = `${finding.url}|${finding.severity}|${normalizedRuleId}`;
        const existing = seen.get(key);
        if (!existing || finding.confidence > existing.confidence) {
            seen.set(key, finding);
        }
    }
    return Array.from(seen.values());
}
/**
 * Run DAST scan with specified scanners
 */
export async function runDASTScan(target, options) {
    const startTime = new Date();
    const scanners = options.scanners || ["zap", "nuclei"];
    const policy = { ...DEFAULT_DAST_POLICY, ...options.policy };
    logger.info("dast.scan_start", {
        target: target.url,
        scanners,
        passiveOnly: policy.passiveOnly,
    });
    // Authorization check
    if (!options.authorized) {
        throw new Error("DAST scan requires explicit authorization. Set 'authorized: true' to confirm you have permission to scan the target.");
    }
    // Run selected scanners
    const scanResults = [];
    const runPromises = [];
    if (scanners.includes("zap")) {
        runPromises.push(runZap(target, policy));
    }
    if (scanners.includes("nuclei")) {
        runPromises.push(runNuclei(target, policy));
    }
    const results = await Promise.all(runPromises);
    scanResults.push(...results);
    // Aggregate findings
    const allFindings = [];
    for (const result of scanResults) {
        allFindings.push(...result.findings);
    }
    // Deduplicate
    const uniqueFindings = deduplicateFindings(allFindings);
    // Calculate stats
    const bySeverity = {};
    for (const finding of uniqueFindings) {
        bySeverity[finding.severity] = (bySeverity[finding.severity] || 0) + 1;
    }
    const byScanner = {
        zap: 0,
        nuclei: 0,
    };
    for (const result of scanResults) {
        byScanner[result.scanner] = result.findings.length;
    }
    const failedScanners = scanResults
        .filter((r) => !r.success)
        .map((r) => r.scanner);
    const totalDuration = scanResults.reduce((sum, r) => sum + r.duration, 0);
    const aggregated = {
        timestamp: startTime.toISOString(),
        target,
        scanners: scanResults,
        totalFindings: allFindings.length,
        uniqueFindings,
        bySeverity,
        byScanner,
        totalDuration,
        allSucceeded: failedScanners.length === 0,
        failedScanners,
    };
    logger.info("dast.scan_complete", {
        totalFindings: allFindings.length,
        uniqueFindings: uniqueFindings.length,
        totalDuration,
        allSucceeded: aggregated.allSucceeded,
    });
    return aggregated;
}
/**
 * Quick passive scan (no active attacks)
 */
export async function runPassiveScan(target, options) {
    return runDASTScan(target, {
        ...options,
        authorized: true,
        policy: {
            ...options?.policy,
            passiveOnly: true,
        },
    });
}
/**
 * Format DAST findings for display
 */
export function formatDASTFindings(findings) {
    if (findings.length === 0) {
        return "No vulnerabilities found.";
    }
    const lines = [
        `## DAST Scan Results\n`,
        `Found ${findings.length} unique vulnerabilities:\n`,
    ];
    // Group by severity
    const bySeverity = new Map();
    for (const finding of findings) {
        const existing = bySeverity.get(finding.severity) || [];
        existing.push(finding);
        bySeverity.set(finding.severity, existing);
    }
    const severityOrder = ["critical", "high", "medium", "low", "info"];
    for (const severity of severityOrder) {
        const group = bySeverity.get(severity);
        if (!group || group.length === 0)
            continue;
        const emoji = {
            critical: "🔴",
            high: "🟠",
            medium: "🟡",
            low: "🔵",
            info: "⚪",
        }[severity] || "⚪";
        lines.push(`\n### ${emoji} ${severity.toUpperCase()} (${group.length})\n`);
        for (const finding of group) {
            lines.push(`- **${finding.name}** (${finding.scanner})`);
            lines.push(`  - URL: ${finding.url}`);
            if (finding.cweIds?.length) {
                lines.push(`  - CWE: ${finding.cweIds.join(", ")}`);
            }
            if (finding.solution) {
                lines.push(`  - Fix: ${finding.solution.slice(0, 200)}${finding.solution.length > 200 ? "..." : ""}`);
            }
        }
    }
    return lines.join("\n");
}
/**
 * Convert DAST findings to SARIF format
 */
export function convertToSARIF(results) {
    const rules = [];
    const rulesSeen = new Set();
    const sarif = {
        $schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
        version: "2.1.0",
        runs: [
            {
                tool: {
                    driver: {
                        name: "Vaspera DAST",
                        informationUri: "https://github.com/vaspera/hardening",
                        version: "1.0.0",
                        rules,
                    },
                },
                results: results.uniqueFindings.map((finding) => {
                    // Add rule if not seen
                    if (!rulesSeen.has(finding.ruleId)) {
                        rulesSeen.add(finding.ruleId);
                        rules.push({
                            id: finding.ruleId,
                            name: finding.name,
                            shortDescription: { text: finding.name },
                            fullDescription: finding.description ? { text: finding.description } : undefined,
                            defaultConfiguration: {
                                level: finding.severity === "critical" || finding.severity === "high" ? "error" :
                                    finding.severity === "medium" ? "warning" : "note",
                            },
                            properties: finding.tags ? { tags: finding.tags } : undefined,
                        });
                    }
                    return {
                        ruleId: finding.ruleId,
                        level: finding.severity === "critical" || finding.severity === "high" ? "error" :
                            finding.severity === "medium" ? "warning" : "note",
                        message: {
                            text: finding.description || finding.name,
                        },
                        locations: [
                            {
                                physicalLocation: {
                                    artifactLocation: {
                                        uri: finding.url,
                                    },
                                },
                            },
                        ],
                        properties: {
                            scanner: finding.scanner,
                            confidence: finding.confidence,
                            cweIds: finding.cweIds,
                            cveIds: finding.cveIds,
                            evidence: finding.evidence,
                        },
                    };
                }),
            },
        ],
    };
    return sarif;
}
//# sourceMappingURL=index.js.map