/**
 * Nuclei Scanner Integration
 *
 * Integrates with ProjectDiscovery's Nuclei for dynamic
 * application security testing using template-based scanning.
 *
 * @module scanners/dast/nuclei
 */
import spawn from "cross-spawn";
import { logger } from "../../logger.js";
import { NUCLEI_SEVERITY_MAPPING } from "./types.js";
/**
 * Check if Nuclei is available
 */
export async function checkNucleiAvailable() {
    return new Promise((resolve) => {
        const child = spawn("nuclei", ["-version"], { timeout: 10000 });
        let stdout = "";
        let stderr = "";
        child.stdout?.on("data", (data) => {
            stdout += data.toString();
        });
        child.stderr?.on("data", (data) => {
            stderr += data.toString();
        });
        child.on("close", (code) => {
            if (code === 0) {
                // Parse version from output (e.g., "Nuclei Engine Version: v3.1.0")
                const versionMatch = (stdout + stderr).match(/v?\d+\.\d+\.\d+/);
                const version = versionMatch ? versionMatch[0] : "unknown";
                resolve({
                    scanner: "nuclei",
                    available: true,
                    version,
                    path: "nuclei",
                    features: {
                        passiveScan: true,
                        activeScan: true,
                        apiScan: true,
                        authentication: true,
                    },
                });
            }
            else {
                resolve({
                    scanner: "nuclei",
                    available: false,
                    error: "Nuclei not found. Install via: go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest",
                });
            }
        });
        child.on("error", () => {
            resolve({
                scanner: "nuclei",
                available: false,
                error: "Nuclei not found. Install via: go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest",
            });
        });
    });
}
/**
 * Build Nuclei command arguments
 */
function buildNucleiArgs(target, policy) {
    const args = [
        "-u", target.url,
        "-json", // JSON output
        "-silent", // Minimal output
        "-no-color", // No ANSI colors
    ];
    // Add severity filter based on risk threshold
    if (policy.riskThreshold) {
        const severityMap = {
            high: "critical,high",
            medium: "critical,high,medium",
            low: "critical,high,medium,low",
            informational: "critical,high,medium,low,info",
        };
        args.push("-severity", severityMap[policy.riskThreshold] || "critical,high,medium");
    }
    // Add specific templates if provided
    if (policy.templates && policy.templates.length > 0) {
        for (const template of policy.templates) {
            args.push("-t", template);
        }
    }
    // Add template tags
    if (policy.templateTags && policy.templateTags.length > 0) {
        args.push("-tags", policy.templateTags.join(","));
    }
    // Exclude tags
    if (policy.excludeTags && policy.excludeTags.length > 0) {
        args.push("-etags", policy.excludeTags.join(","));
    }
    // Passive-only mode (no active exploitation)
    if (policy.passiveOnly) {
        args.push("-passive");
    }
    // Rate limiting
    if (policy.requestDelay && policy.requestDelay > 0) {
        // Nuclei uses rate-limit as requests per second
        const rps = Math.floor(1000 / policy.requestDelay);
        args.push("-rate-limit", String(Math.max(1, rps)));
    }
    // Concurrency
    if (policy.threads) {
        args.push("-concurrency", String(policy.threads));
    }
    // Add custom headers
    if (target.headers) {
        for (const [key, value] of Object.entries(target.headers)) {
            args.push("-header", `${key}: ${value}`);
        }
    }
    // Add authentication header
    if (target.authentication) {
        const auth = target.authentication;
        switch (auth.type) {
            case "bearer":
                if (auth.credentials.token) {
                    args.push("-header", `Authorization: Bearer ${auth.credentials.token}`);
                }
                break;
            case "basic":
                if (auth.credentials.username && auth.credentials.password) {
                    const encoded = Buffer.from(`${auth.credentials.username}:${auth.credentials.password}`).toString("base64");
                    args.push("-header", `Authorization: Basic ${encoded}`);
                }
                break;
            case "api-key":
                if (auth.credentials.apiKey && auth.credentials.apiKeyHeader) {
                    args.push("-header", `${auth.credentials.apiKeyHeader}: ${auth.credentials.apiKey}`);
                }
                break;
            case "cookie":
                if (auth.credentials.cookie) {
                    args.push("-header", `Cookie: ${auth.credentials.cookie}`);
                }
                break;
        }
    }
    return args;
}
/**
 * Parse Nuclei JSON line output to DASTFinding
 */
export function parseNucleiResult(result) {
    const severity = NUCLEI_SEVERITY_MAPPING[result.info.severity] || "info";
    // Extract CWE and CVE IDs
    const cweIds = result.info.classification?.["cwe-id"]?.map((id) => id.startsWith("CWE-") ? id : `CWE-${id}`);
    const cveIds = result.info.classification?.["cve-id"];
    // Build references array
    const references = [];
    if (result.info.reference) {
        references.push(...result.info.reference);
    }
    if (result["template-url"]) {
        references.push(result["template-url"]);
    }
    return {
        scanner: "nuclei",
        ruleId: `nuclei-${result["template-id"]}`,
        name: result.info.name,
        description: result.info.description || `${result.info.name} detected`,
        severity,
        confidence: getConfidenceFromSeverity(result.info.severity),
        url: result.matched || result.host,
        method: extractMethod(result.request),
        evidence: result["extracted-results"]?.join("\n"),
        cweIds,
        cveIds,
        references,
        tags: result.info.tags,
        timestamp: result.timestamp || new Date().toISOString(),
        rawOutput: result,
    };
}
/**
 * Extract HTTP method from curl command or request
 */
function extractMethod(request) {
    if (!request)
        return undefined;
    const methodMatch = request.match(/^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s/);
    return methodMatch ? methodMatch[1] : undefined;
}
/**
 * Get confidence score based on severity
 */
function getConfidenceFromSeverity(severity) {
    switch (severity.toLowerCase()) {
        case "critical":
            return 95;
        case "high":
            return 90;
        case "medium":
            return 80;
        case "low":
            return 70;
        default:
            return 60;
    }
}
/**
 * Run Nuclei scan
 */
export async function runNuclei(target, policy = {}) {
    const startTime = new Date();
    const mergedPolicy = { ...{ passiveOnly: true, maxDuration: 300, riskThreshold: "medium" }, ...policy };
    logger.info("nuclei.scan_start", {
        target: target.url,
        passiveOnly: mergedPolicy.passiveOnly,
    });
    // Check availability
    const availability = await checkNucleiAvailable();
    if (!availability.available) {
        return {
            scanner: "nuclei",
            target,
            findings: [],
            duration: Date.now() - startTime.getTime(),
            success: false,
            error: availability.error || "Nuclei not available",
            stats: {
                requestCount: 0,
                urlsDiscovered: 0,
                uniqueFindings: 0,
                bySeverity: {},
            },
            startTime: startTime.toISOString(),
            endTime: new Date().toISOString(),
            policy: mergedPolicy,
        };
    }
    return new Promise((resolve) => {
        const args = buildNucleiArgs(target, mergedPolicy);
        logger.debug("nuclei.command", { args: args.join(" ") });
        const child = spawn("nuclei", args, {
            timeout: (mergedPolicy.maxDuration || 300) * 1000,
        });
        let stdout = "";
        let stderr = "";
        child.stdout?.on("data", (data) => {
            stdout += data.toString();
        });
        child.stderr?.on("data", (data) => {
            stderr += data.toString();
        });
        child.on("close", (code) => {
            const endTime = new Date();
            const findings = [];
            // Parse JSON lines output
            const lines = stdout.split("\n").filter(Boolean);
            for (const line of lines) {
                try {
                    const result = JSON.parse(line);
                    findings.push(parseNucleiResult(result));
                }
                catch {
                    // Skip non-JSON lines
                    logger.debug("nuclei.parse_skip", { line: line.slice(0, 100) });
                }
            }
            // Calculate stats
            const bySeverity = {};
            for (const finding of findings) {
                bySeverity[finding.severity] = (bySeverity[finding.severity] || 0) + 1;
            }
            const success = code === 0 || findings.length > 0;
            const result = {
                scanner: "nuclei",
                target,
                findings,
                duration: endTime.getTime() - startTime.getTime(),
                success,
                error: !success && stderr ? stderr.slice(0, 500) : undefined,
                stats: {
                    requestCount: 0, // Not available from Nuclei 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("nuclei.scan_complete", {
                findings: findings.length,
                duration: result.duration,
                success,
            });
            resolve(result);
        });
        child.on("error", (error) => {
            const endTime = new Date();
            resolve({
                scanner: "nuclei",
                target,
                findings: [],
                duration: endTime.getTime() - startTime.getTime(),
                success: false,
                error: String(error),
                stats: {
                    requestCount: 0,
                    urlsDiscovered: 0,
                    uniqueFindings: 0,
                    bySeverity: {},
                },
                version: availability.version,
                startTime: startTime.toISOString(),
                endTime: endTime.toISOString(),
                policy: mergedPolicy,
            });
        });
    });
}
/**
 * Get Nuclei installation instructions
 */
export function getNucleiInstallInstructions() {
    return `
# Nuclei Installation

## Go (Recommended)
\`\`\`bash
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
\`\`\`

## Homebrew (macOS)
\`\`\`bash
brew install nuclei
\`\`\`

## Docker
\`\`\`bash
docker pull projectdiscovery/nuclei:latest
\`\`\`

## Binary Download
Download from https://github.com/projectdiscovery/nuclei/releases

## Update Templates
\`\`\`bash
nuclei -update-templates
\`\`\`

## Verify Installation
\`\`\`bash
nuclei -version
\`\`\`
`;
}
//# sourceMappingURL=nuclei.js.map