import fs from "node:fs";
import path from "node:path";
import { execa } from "execa";
import { debugLog } from "../core/logger.js";
import { safeJsonParse } from "../core/helpers.js";
import { hasGlobPattern, isPathInScope } from "../core/path_scope_utils.js";
import { checkDeniedPath } from "../core/denied_paths.js";
/**
 * 作用域控制器 — 检查文件路径是否在允许范围内，并检测敏感信息泄露。
 *
 * 核心功能：
 * 1. 路径范围校验：解析符号链接后，判断文件是否在声明的允许路径内（防止路径穿越）
 * 2. 敏感信息检测：优先使用 gitleaks 扫描密钥，回退到正则表达式匹配
 * 3. 安全模式识别：跳过通过环境变量引用的配置行（如 process.env.X）
 */
/** gitleaks 可用性缓存，避免重复执行 which 检测 */
let gitleaksAvailable = null;
let gitleaksCheckedAt = 0;
const GITLEAKS_CACHE_TTL = 5 * 60 * 1000; // 5 分钟
/**
 * 检测系统中是否安装了 gitleaks 密钥扫描工具。
 * 结果会被缓存 5 分钟，超时后重新检测。
 * @returns true 表示 gitleaks 可用
 */
async function isGitleaksAvailable() {
    const now = Date.now();
    if (gitleaksAvailable !== null && now - gitleaksCheckedAt < GITLEAKS_CACHE_TTL) {
        return gitleaksAvailable;
    }
    try {
        await execa("which", ["gitleaks"], { stdio: "pipe" });
        gitleaksAvailable = true;
    }
    catch {
        gitleaksAvailable = false;
    }
    gitleaksCheckedAt = now;
    return gitleaksAvailable;
}
/**
 * 密钥/敏感信息正则匹配规则。
 * 每条规则包含：匹配正则、类型标识、严重等级（blocked 表示禁止，warning 表示警告）。
 *
 * 覆盖的密钥类型：AWS 访问密钥、API 密钥、数据库密码、JWT 密钥等。
 */
const SECRET_PATTERNS = [
    // AWS 访问密钥 ID：以 AKIA 开头的 20 位大写字母数字
    { pattern: /AKIA[0-9A-Z]{16}/, type: "aws_access_key", severity: "blocked" },
    // AWS 密钥赋值语句
    { pattern: /aws_secret_access_key\s*=\s*['"][^'"]+['"]/, type: "aws_secret", severity: "blocked" },
    // 通用 API 密钥（如 OpenAI sk- 前缀）
    { pattern: /sk-[a-zA-Z0-9]{32,}/, type: "api_key", severity: "blocked" },
    // 32 位以上十六进制字符串（可能是令牌或哈希值）
    { pattern: /['"][a-f0-9]{32,}['"]/, type: "possible_token", severity: "warning" },
    // 数据库密码赋值语句（8 位以上）
    { pattern: /password\s*=\s*['"][^'"]{8,}['"]/, type: "db_password", severity: "blocked" },
    // JWT 签名密钥
    { pattern: /jwt_secret\s*=\s*['"][^'"]+['"]/, type: "jwt_secret", severity: "blocked" },
    // 通用密钥赋值
    { pattern: /secret_key\s*=\s*['"][^'"]+['"]/, type: "secret_key", severity: "blocked" },
];
/**
 * 安全模式正则列表 — 匹配到这些模式的行将被跳过，
 * 因为它们通过环境变量引用敏感值，而非硬编码。
 */
const SAFE_PATTERNS = [
    /process\.env\./,
    /System\.getenv\(/,
    /import\.meta\.env\./,
    // 环境变量名本身不是敏感值
    /NODE_ENV/,
];
/**
 * 检查文件路径是否在允许的范围内，并可选地扫描文件内容中的敏感信息。
 *
 * 检查流程（黑名单 + 可选收窄）：
 * 1. 黑名单检查：项目相对路径命中内置安全黑名单 → blocked
 * 2. 收窄检查：allowedPaths 非空时，路径必须在 scope 内；空则跳过（黑名单模式）
 * 3. 密钥扫描：若提供了内容参数，对内容进行密钥/敏感信息检测
 *
 * @param filePath - 待检查的文件绝对路径
 * @param allowedPaths - 收窄路径列表（空数组 = 不收窄，仅受黑名单约束）
 * @param content - 可选的文件内容，用于敏感信息检测
 * @param projectRelativePath - 项目相对路径（POSIX 格式），用于黑名单匹配
 * @returns 作用域检查结果，包含是否允许、原因、严重等级和密钥详情
 */
export async function checkScope(filePath, allowedPaths, content, projectRelativePath) {
    // 输入校验：null/undefined 安全拒绝而非崩溃
    if (!filePath || typeof filePath !== "string") {
        return { allowed: false, reason: "文件路径无效", has_secrets: false, severity: "blocked" };
    }
    debugLog(`作用域检查: 检查文件 ${filePath}`);
    // 步骤 1: 黑名单检查（始终生效，优先于 scope）
    if (projectRelativePath) {
        const deniedReason = checkDeniedPath(projectRelativePath);
        if (deniedReason) {
            debugLog(`作用域检查: 文件 ${projectRelativePath} 命中黑名单: ${deniedReason}`);
            return {
                allowed: false,
                reason: deniedReason,
                has_secrets: false,
                severity: "blocked",
            };
        }
    }
    // 步骤 2: 收窄模式检查（allowedPaths 非空时生效）
    if (Array.isArray(allowedPaths) && allowedPaths.length > 0) {
        const resolvedPath = realPath(filePath);
        const inScope = allowedPaths.some((allowed) => {
            const resolved = hasGlobPattern(allowed)
                ? (path.isAbsolute(allowed) ? allowed : path.resolve(allowed))
                : realPath(allowed);
            return isPathInScope(resolvedPath, resolved);
        });
        if (!inScope) {
            debugLog(`作用域检查: 文件 ${filePath} 不在允许的 scope 内`);
            return {
                allowed: false,
                reason: `文件 ${filePath} 不在允许的 scope 内`,
                has_secrets: false,
                severity: "blocked",
            };
        }
    }
    // 如果提供了内容，检查密钥
    if (content) {
        const secretResult = await detectSecrets(content, filePath);
        if (secretResult.length > 0) {
            const hasBlocked = secretResult.some((s) => s.severity === "blocked");
            debugLog(`作用域检查: 文件 ${filePath} 检测到敏感信息: ${secretResult.map((s) => s.type).join(", ")}`);
            return {
                allowed: !hasBlocked,
                reason: hasBlocked
                    ? `检测到密钥/密码: ${secretResult.map((s) => s.type).join(", ")}。请使用环境变量。`
                    : `可能包含敏感信息: ${secretResult.map((s) => s.type).join(", ")}`,
                has_secrets: true,
                secret_details: secretResult.map((s) => ({
                    file: filePath,
                    line: s.line,
                    type: s.type,
                })),
                severity: hasBlocked ? "blocked" : "warning",
            };
        }
    }
    return { allowed: true, has_secrets: false, severity: "ok" };
}
/**
 * 检测文件内容中的密钥和敏感信息。
 * 优先使用 gitleaks 进行专业扫描；不可用时回退到正则匹配。
 * @param content - 文件文本内容
 * @param filePath - 文件路径（用于 gitleaks 的 `-s` 参数）
 * @returns 检测到的敏感信息列表（最多返回 10 条 gitleaks 结果）
 */
async function detectSecrets(content, filePath) {
    // 如果可用，优先使用 gitleaks 进行更全面的扫描
    if (await isGitleaksAvailable()) {
        try {
            // --no-git: 不依赖 git 仓库；-f json: JSON 格式输出；-s: 指定扫描目标文件
            const { stdout: result } = await execa("gitleaks", ["detect", "--no-git", "-f", "json", "-s", "--", filePath], // -- 防止 filePath 被解析为 flag
            { timeout: 5000 });
            const findings = safeJsonParse(result || "[]");
            if (Array.isArray(findings) && findings.length > 0) {
                return findings.slice(0, 10).map((f) => ({
                    type: String(f.RuleID || "gitleaks_finding"),
                    line: Number(f.StartLine || 0),
                    severity: "blocked",
                }));
            }
            return []; // gitleaks 未发现问题
        }
        catch (e) {
            // gitleaks exit 1 = 检测到问题，stdout 包含 JSON
            const stdout = e instanceof Error ? e.stdout : undefined;
            if (stdout) {
                try {
                    const findings = safeJsonParse(stdout);
                    if (Array.isArray(findings) && findings.length > 0) {
                        return findings.slice(0, 10).map((f) => ({
                            type: String(f.RuleID || "gitleaks_finding"),
                            line: Number(f.StartLine || 0),
                            severity: "blocked",
                        }));
                    }
                }
                catch { /* JSON parse failed, fall through to regex */ }
            }
            // 真正的错误（非 exit 1），回退到正则匹配
        }
    }
    // 回退: 基于正则的检测
    const lines = content.split("\n");
    const results = [];
    for (let i = 0; i < lines.length; i++) {
        const line = lines[i];
        // 跳过使用环境变量的行
        if (SAFE_PATTERNS.some((p) => p.test(line)))
            continue;
        // 跳过注释行（// 单行注释、# shebang/注释、<!-- HTML注释、* 多行注释片段）
        if (/^\s*(\/\/|#|<!--|\*)/.test(line))
            continue;
        for (const rule of SECRET_PATTERNS) {
            if (rule.pattern.test(line)) {
                results.push({ type: rule.type, line: i + 1, severity: rule.severity });
            }
        }
    }
    return results;
}
/**
 * 解析文件的真实路径（处理符号链接）。
 * 若文件不存在或解析失败，回退到简单的路径规范化。
 * @param p - 待解析的路径
 * @returns 解析后的绝对路径
 */
function realPath(p) {
    try {
        return fs.realpathSync(path.resolve(p));
    }
    catch {
        return path.resolve(p);
    }
}
//# sourceMappingURL=scope_controller.js.map