/**
 * OpenAPI Security Scanner
 *
 * Validates OpenAPI/Swagger specifications for security issues
 * using Spectral with security-focused rules.
 *
 * @module scanners/openapi
 */
import { exec } from "child_process";
import { promisify } from "util";
import { readFile, readdir } from "fs/promises";
import { join, extname } from "path";
const execAsync = promisify(exec);
const SECURITY_RULES = `
extends: ["spectral:oas"]
rules:
  # Authentication
  operation-security-defined:
    description: Operations must have security defined
    given: "$.paths[*][get,post,put,patch,delete]"
    then:
      field: security
      function: truthy
    severity: error

  # HTTPS only
  servers-https:
    description: Server URLs should use HTTPS
    given: "$.servers[*].url"
    then:
      function: pattern
      functionOptions:
        match: "^https://"
    severity: error

  # No credentials in URLs
  no-credentials-in-url:
    description: URLs should not contain credentials
    given: "$.servers[*].url"
    then:
      function: pattern
      functionOptions:
        notMatch: "://[^/]*:[^/]*@"
    severity: error

  # Rate limiting headers
  rate-limit-headers:
    description: Responses should include rate limiting headers
    given: "$.paths[*][*].responses[*].headers"
    then:
      function: schema
      functionOptions:
        schema:
          anyOf:
            - required: ["X-RateLimit-Limit"]
            - required: ["X-Rate-Limit-Limit"]
            - required: ["RateLimit-Limit"]
    severity: warn

  # Sensitive data in query params
  no-sensitive-query-params:
    description: Sensitive data should not be in query parameters
    given: "$.paths[*][*].parameters[?(@.in=='query')]"
    then:
      field: name
      function: pattern
      functionOptions:
        notMatch: "(?i)(password|secret|token|api_key|apikey|auth|credential)"
    severity: error
`;
export async function checkSpectralAvailable() {
    try {
        const { stdout } = await execAsync("npx spectral --version", { timeout: 10000 });
        return {
            scanner: "spectral",
            available: true,
            version: stdout.trim(),
        };
    }
    catch {
        return {
            scanner: "spectral",
            available: false,
            error: "Spectral not available. Install with: npm install -g @stoplight/spectral-cli",
        };
    }
}
function mapSeverity(severity) {
    switch (severity) {
        case 0:
            return "high";
        case 1:
            return "medium";
        case 2:
            return "low";
        default:
            return "info";
    }
}
export async function runSpectral(specPath, options) {
    const startTime = Date.now();
    try {
        const availability = await checkSpectralAvailable();
        if (!availability.available) {
            return {
                scanner: "spectral",
                findings: [],
                duration: Date.now() - startTime,
                success: false,
                error: availability.error,
            };
        }
        let command = `npx spectral lint "${specPath}" -f json`;
        if (options?.rulesetPath) {
            command += ` -r "${options.rulesetPath}"`;
        }
        const { stdout, stderr } = await execAsync(command, {
            timeout: options?.timeout || 60000,
            maxBuffer: 10 * 1024 * 1024,
        }).catch((error) => {
            if (error.stdout) {
                return { stdout: error.stdout, stderr: error.stderr || "" };
            }
            throw error;
        });
        const results = JSON.parse(stdout || "[]");
        const findings = results.map((result) => ({
            scanner: "spectral",
            ruleId: `spectral:${result.code}`,
            file: result.source,
            line: result.range.start.line + 1,
            column: result.range.start.character + 1,
            endLine: result.range.end.line + 1,
            message: result.message,
            severity: mapSeverity(result.severity),
            confidence: 100,
            metadata: {
                path: result.path.join("."),
            },
        }));
        return {
            scanner: "spectral",
            findings,
            duration: Date.now() - startTime,
            success: true,
            version: availability.version,
        };
    }
    catch (error) {
        return {
            scanner: "spectral",
            findings: [],
            duration: Date.now() - startTime,
            success: false,
            error: error instanceof Error ? error.message : "Unknown error",
        };
    }
}
export async function findOpenAPISpecs(projectPath) {
    const specs = [];
    const extensions = [".yaml", ".yml", ".json"];
    const patterns = ["openapi", "swagger", "api-spec", "api"];
    async function searchDir(dir, depth = 0) {
        if (depth > 3)
            return;
        try {
            const entries = await readdir(dir, { withFileTypes: true });
            for (const entry of entries) {
                const fullPath = join(dir, entry.name);
                if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
                    await searchDir(fullPath, depth + 1);
                }
                else if (entry.isFile()) {
                    const ext = extname(entry.name).toLowerCase();
                    const nameWithoutExt = entry.name.slice(0, -ext.length).toLowerCase();
                    if (extensions.includes(ext) && patterns.some((p) => nameWithoutExt.includes(p))) {
                        try {
                            const content = await readFile(fullPath, "utf-8");
                            if (content.includes("openapi") || content.includes("swagger")) {
                                specs.push(fullPath);
                            }
                        }
                        catch {
                            // Skip files that can't be read
                        }
                    }
                }
            }
        }
        catch {
            // Skip directories that can't be read
        }
    }
    await searchDir(projectPath);
    return specs;
}
export async function runOpenAPIScan(projectPath, options) {
    const startTime = Date.now();
    const specs = await findOpenAPISpecs(projectPath);
    if (specs.length === 0) {
        return {
            scanner: "openapi",
            findings: [],
            duration: Date.now() - startTime,
            success: true,
            filesScanned: 0,
        };
    }
    const allFindings = [];
    for (const spec of specs) {
        const result = await runSpectral(spec, options);
        allFindings.push(...result.findings);
    }
    return {
        scanner: "openapi",
        findings: allFindings,
        duration: Date.now() - startTime,
        success: true,
        filesScanned: specs.length,
    };
}
export async function detectOpenAPI(projectPath) {
    const specs = await findOpenAPISpecs(projectPath);
    return specs.length > 0;
}
//# sourceMappingURL=openapi.js.map