/**
 * Secret scanning utilities using TruffleHog.
 *
 * Provides functions for scanning content for leaked credentials and secrets.
 * Uses TruffleHog CLI under the hood for detection and verification.
 */
import { spawn, execSync } from "node:child_process";
import { getErrorMeta, getPrefixedLogger } from "../../util/logger.js";
/**
 * Detects if trufflehog is running via a Nix wrapper that already adds --no-update.
 * The Nix wrapper is a bash script that adds --no-update before forwarding args.
 */
function isNixWrappedTrufflehog() {
    const logger = getPrefixedLogger("[secret-scan]");
    try {
        const trufflehogPath = execSync("which trufflehog", {
            encoding: "utf-8",
        }).trim();
        return trufflehogPath.includes("/nix/store/");
    }
    catch (error) {
        logger.debug("Nix detection failed, assuming non-Nix trufflehog", getErrorMeta(error));
        return false;
    }
}
// Cache the result since it won't change during runtime
let _isNixWrapped;
function shouldSkipNoUpdateFlag() {
    if (_isNixWrapped === undefined) {
        _isNixWrapped = isNixWrappedTrufflehog();
    }
    return _isNixWrapped;
}
/**
 * Redacts secrets in content by replacing raw values with redacted versions.
 *
 * Replaces each detected secret's raw value with its redacted form.
 * Secrets are processed longest-first to handle overlapping matches correctly.
 *
 * @param content - The original content containing secrets
 * @param findings - Array of secret findings from TruffleHog
 * @returns Content with all detected secrets redacted
 */
export function redactSecrets(content, findings) {
    if (findings.length === 0) {
        return content;
    }
    let redacted = content;
    // Sort findings by raw length (longest first) to handle overlapping secrets
    const sortedFindings = [...findings].sort((a, b) => b.raw.length - a.raw.length);
    for (const finding of sortedFindings) {
        if (finding.raw && finding.redacted) {
            // Use global replacement to catch all occurrences
            redacted = redacted.split(finding.raw).join(finding.redacted);
        }
    }
    return redacted;
}
/**
 * Parses TruffleHog JSON output into SecretFinding objects.
 *
 * @param jsonLine - Single line of JSON output from TruffleHog
 * @returns Parsed SecretFinding or null if parsing fails
 */
function parseTrufflehogResult(jsonLine) {
    const logger = getPrefixedLogger("[secret-scan]");
    try {
        const result = JSON.parse(jsonLine);
        const raw = result.Raw ?? "";
        // Generate a sensible redacted value if TruffleHog doesn't provide one
        // (custom regex detectors often return empty Redacted field)
        const redacted = result.Redacted || generateRedactedValue(raw);
        return {
            detectorType: result.DetectorType?.toString() ?? "unknown",
            detectorName: result.DetectorName ?? "unknown",
            verified: result.Verified ?? false,
            raw,
            redacted,
            line: result.SourceMetadata?.Data?.Filesystem?.line,
            extraData: result.ExtraData,
            sourceMetadata: {
                file: result.SourceMetadata?.Data?.Filesystem?.file,
                line: result.SourceMetadata?.Data?.Filesystem?.line,
            },
        };
    }
    catch (error) {
        logger.warn("Failed to parse TruffleHog result", getErrorMeta(error));
        logger.debug("Unparseable TruffleHog output line", { jsonLine });
        return null;
    }
}
/**
 * Generates a redacted version of a secret value.
 *
 * Preserves a prefix for recognizable patterns (like sk_live_, ghp_, AKIA)
 * and replaces the rest with asterisks.
 *
 * @param raw - The raw secret value
 * @returns A redacted version with preserved prefix and masked suffix
 */
function generateRedactedValue(raw) {
    if (!raw || raw.length <= 4) {
        return "[REDACTED]";
    }
    // For short secrets, just show first 2 chars
    if (raw.length <= 8) {
        return raw.slice(0, 2) + "****";
    }
    // For longer secrets, preserve recognizable prefixes
    const prefixLength = Math.min(8, Math.floor(raw.length / 3));
    return raw.slice(0, prefixLength) + "****";
}
/**
 * Runs TruffleHog CLI using stdin and returns parsed findings.
 *
 * Uses TruffleHog's built-in detectors for common secret types (Stripe, GitHub,
 * AWS, private keys, etc.) without custom configuration.
 *
 * @param content - Content to scan for secrets (piped to stdin)
 * @param timeout - Timeout in milliseconds
 * @param verifiedOnly - Whether to only return verified secrets
 * @returns Promise resolving to array of SecretFinding objects
 */
export async function runTrufflehog(content, timeout, verifiedOnly) {
    return new Promise((resolve, reject) => {
        const args = ["stdin", "--json"];
        if (!shouldSkipNoUpdateFlag()) {
            args.push("--no-update");
        }
        if (verifiedOnly) {
            args.push("--results=verified");
        }
        else {
            args.push("--results=verified,unknown,unverified");
        }
        const proc = spawn("trufflehog", args, {
            timeout,
            stdio: ["pipe", "pipe", "pipe"],
        });
        const findings = [];
        let stdout = "";
        let stderr = "";
        proc.stdout.on("data", (data) => {
            stdout += data.toString();
        });
        proc.stderr.on("data", (data) => {
            stderr += data.toString();
        });
        proc.on("close", (code) => {
            // TruffleHog returns 0 for no secrets, 183 for secrets found
            if (code === 0 || code === 183) {
                // Parse each line of JSON output
                const lines = stdout.split("\n").filter((line) => line.trim());
                for (const line of lines) {
                    const finding = parseTrufflehogResult(line);
                    if (finding) {
                        findings.push(finding);
                    }
                }
                resolve(findings);
            }
            else {
                reject(new Error(`TruffleHog exited with code ${code}: ${stderr || stdout}`));
            }
        });
        proc.on("error", (error) => {
            if (error.message.includes("ENOENT")) {
                reject(new Error("TruffleHog is not installed. Please install it: https://github.com/trufflesecurity/trufflehog"));
            }
            else {
                reject(error);
            }
        });
        // Write content to stdin and close the stream
        proc.stdin.write(content);
        proc.stdin.end();
    });
}
/**
 * Checks if TruffleHog is available on the system.
 *
 * @returns Promise resolving to true if TruffleHog is installed and accessible
 */
export async function isTrufflehogAvailable() {
    return new Promise((resolve) => {
        const args = ["--version"];
        if (!shouldSkipNoUpdateFlag()) {
            args.push("--no-update");
        }
        const proc = spawn("trufflehog", args, {
            timeout: 5000,
            stdio: ["ignore", "pipe", "pipe"],
        });
        proc.on("close", (code) => {
            resolve(code === 0);
        });
        proc.on("error", () => {
            resolve(false);
        });
    });
}
/**
 * A reusable secret redactor that can perform fast synchronous redaction.
 *
 * Use this when you need to redact secrets from multiple pieces of content
 * using the same set of findings (e.g., redacting LLM responses based on
 * secrets found in the input prompt).
 *
 * @example
 * ```typescript
 * // Scan input once
 * const result = await scanContentForSecrets(userPrompt);
 * const redactor = new SecretRedactor(result.findings);
 *
 * // Redact multiple outputs synchronously
 * const safeResponse = redactor.redact(llmResponse);
 * const safeToolOutput = redactor.redact(toolResult);
 * ```
 */
export class SecretRedactor {
    sortedFindings;
    /**
     * Creates a new SecretRedactor with the given findings.
     *
     * @param findings - Secret findings to use for redaction
     */
    constructor(findings) {
        // Sort findings by raw length (longest first) to handle overlapping secrets
        this.sortedFindings = [...findings].sort((a, b) => b.raw.length - a.raw.length);
    }
    /**
     * Redacts secrets from the given content synchronously.
     *
     * @param content - The content to redact secrets from
     * @returns Content with secrets replaced by redacted versions
     */
    redact(content) {
        if (this.sortedFindings.length === 0 || !content) {
            return content;
        }
        let redacted = content;
        for (const finding of this.sortedFindings) {
            if (finding.raw && finding.redacted) {
                redacted = redacted.split(finding.raw).join(finding.redacted);
            }
        }
        return redacted;
    }
    /**
     * Redacts secrets from any value (string, object, or array).
     *
     * Recursively processes objects and arrays, redacting any string values.
     *
     * @param value - The value to redact secrets from
     * @returns Value with secrets redacted from all string content
     */
    redactDeep(value) {
        if (typeof value === "string") {
            return this.redact(value);
        }
        if (Array.isArray(value)) {
            return value.map((item) => this.redactDeep(item));
        }
        if (value !== null && typeof value === "object") {
            const result = {};
            for (const [key, val] of Object.entries(value)) {
                result[key] = this.redactDeep(val);
            }
            return result;
        }
        return value;
    }
    /**
     * Returns true if this redactor has any findings to redact.
     */
    hasFindings() {
        return this.sortedFindings.length > 0;
    }
    /**
     * Returns the number of findings this redactor has.
     */
    get findingCount() {
        return this.sortedFindings.length;
    }
}
export function formatSecretFindings(findings) {
    if (findings.length === 0) {
        return "No secrets detected.";
    }
    const lines = [`Found ${findings.length} potential secret(s):\n`];
    for (const finding of findings) {
        const status = finding.verified ? "✓ VERIFIED" : "? unverified";
        lines.push(`  [${status}] ${finding.detectorName}`);
        lines.push(`    Type: ${finding.detectorType}`);
        lines.push(`    Value: ${finding.redacted}`);
        if (finding.sourceMetadata?.line) {
            lines.push(`    Line: ${finding.sourceMetadata.line}`);
        }
        lines.push("");
    }
    return lines.join("\n");
}
//# sourceMappingURL=secret-scanner.js.map