/**
 * Custom Rule Engine
 *
 * Allows projects to define custom validation rules that are
 * checked during certification alongside built-in checks.
 *
 * Rules are defined in `.vaspera/hardening-rules.yaml` or `.vaspera/hardening-rules.json`
 *
 * @module certification/rules
 */
import { readFile } from "fs/promises";
import { join } from "path";
import { logger } from "../logger.js";
/**
 * Built-in rule templates that can be extended
 */
export const RULE_TEMPLATES = {
    "no-hardcoded-secrets": {
        name: "No Hardcoded Secrets",
        pattern: "(password|secret|api_key|apikey|token)\\s*[:=]\\s*[\"'][^\"']{8,}[\"']",
        severity: "critical",
        message: "Hardcoded secret detected. Use environment variables instead.",
        category: "security",
        agent: "security",
        confidence: 90,
    },
    "no-console-log": {
        name: "No Console Logs",
        pattern: "console\\.(log|debug|info|warn|error)\\s*\\(",
        severity: "low",
        message: "Console statement found. Use proper logging.",
        category: "quality",
        agent: "quality",
        confidence: 100,
    },
    "no-any-type": {
        name: "No Any Type",
        pattern: ":\\s*any\\b",
        severity: "medium",
        message: "Avoid using 'any' type. Use specific types or generics.",
        category: "typesafety",
        agent: "typesafety",
        confidence: 95,
    },
    "async-await-consistency": {
        name: "Async/Await Consistency",
        pattern: "\\.then\\s*\\([^)]*\\)\\s*\\.catch",
        severity: "low",
        message: "Consider using async/await instead of .then().catch() chains.",
        category: "quality",
        agent: "quality",
        confidence: 80,
    },
    "no-eval": {
        name: "No Eval",
        pattern: "\\beval\\s*\\(",
        severity: "critical",
        message: "eval() is dangerous and should never be used.",
        category: "security",
        agent: "security",
        confidence: 100,
    },
    "no-sql-concatenation": {
        name: "No SQL Concatenation",
        pattern: "(SELECT|INSERT|UPDATE|DELETE)[^;]*\\+\\s*(\\w+|[\"'])",
        severity: "critical",
        message: "Potential SQL injection. Use parameterized queries.",
        category: "security",
        agent: "security",
        confidence: 85,
    },
};
/**
 * Load custom rules from project configuration
 */
export async function loadCustomRules(projectPath) {
    const rules = [];
    // Try YAML first, then JSON
    const configPaths = [
        join(projectPath, ".vaspera", "hardening-rules.yaml"),
        join(projectPath, ".vaspera", "hardening-rules.yml"),
        join(projectPath, ".vaspera", "hardening-rules.json"),
    ];
    for (const configPath of configPaths) {
        try {
            const content = await readFile(configPath, "utf-8");
            let config;
            if (configPath.endsWith(".json")) {
                config = JSON.parse(content);
            }
            else {
                // Simple YAML parsing for common cases
                config = parseSimpleYaml(content);
            }
            if (config.rules && Array.isArray(config.rules)) {
                for (const rule of config.rules) {
                    // Apply template if referenced
                    if (rule.id && RULE_TEMPLATES[rule.id]) {
                        rules.push({
                            ...RULE_TEMPLATES[rule.id],
                            ...rule,
                            enabled: rule.enabled !== false,
                        });
                    }
                    else {
                        rules.push({
                            ...rule,
                            enabled: rule.enabled !== false,
                            confidence: rule.confidence ?? 80,
                        });
                    }
                }
            }
            logger.info("rules.loaded", {
                configPath,
                ruleCount: rules.length,
            });
            break; // Stop after first successful load
        }
        catch {
            // Config file doesn't exist or is invalid, continue to next
        }
    }
    return rules;
}
/**
 * Simple YAML parser for rule configuration
 * (Only supports the subset needed for rules)
 */
function parseSimpleYaml(content) {
    const lines = content.split("\n");
    const config = { rules: [] };
    let currentRule = null;
    let inRules = false;
    for (const line of lines) {
        const trimmed = line.trim();
        if (!trimmed || trimmed.startsWith("#"))
            continue;
        if (trimmed === "rules:" || trimmed === "custom_rules:") {
            inRules = true;
            continue;
        }
        if (!inRules)
            continue;
        // New rule item
        if (trimmed.startsWith("- id:") || trimmed.startsWith("-id:")) {
            if (currentRule && currentRule.id) {
                config.rules.push(currentRule);
            }
            currentRule = {
                id: trimmed.replace(/^-\s*id:\s*["']?/, "").replace(/["']?\s*$/, ""),
            };
            continue;
        }
        // Rule properties
        if (currentRule && trimmed.includes(":")) {
            const [key, ...valueParts] = trimmed.replace(/^-\s*/, "").split(":");
            const value = valueParts.join(":").trim().replace(/^["']|["']$/g, "");
            switch (key.trim()) {
                case "name":
                    currentRule.name = value;
                    break;
                case "description":
                    currentRule.description = value;
                    break;
                case "pattern":
                    currentRule.pattern = value;
                    break;
                case "severity":
                    currentRule.severity = value;
                    break;
                case "message":
                    currentRule.message = value;
                    break;
                case "category":
                    currentRule.category = value;
                    break;
                case "agent":
                    currentRule.agent = value;
                    break;
                case "confidence":
                    currentRule.confidence = parseInt(value, 10);
                    break;
                case "enabled":
                    currentRule.enabled = value === "true";
                    break;
            }
        }
    }
    // Don't forget the last rule
    if (currentRule && currentRule.id) {
        config.rules.push(currentRule);
    }
    return config;
}
/**
 * Check a file against custom rules
 */
export async function checkFileAgainstRules(filePath, content, rules) {
    const matches = [];
    for (const rule of rules) {
        if (!rule.enabled)
            continue;
        // Check file patterns
        if (rule.excludeFiles) {
            const shouldExclude = rule.excludeFiles.some((pattern) => {
                const regex = new RegExp(pattern.replace(/\*/g, ".*"));
                return regex.test(filePath);
            });
            if (shouldExclude)
                continue;
        }
        if (rule.includeFiles) {
            const shouldInclude = rule.includeFiles.some((pattern) => {
                const regex = new RegExp(pattern.replace(/\*/g, ".*"));
                return regex.test(filePath);
            });
            if (!shouldInclude)
                continue;
        }
        try {
            const regex = new RegExp(rule.pattern, "gm");
            const lines = content.split("\n");
            let lineNumber = 0;
            for (const line of lines) {
                lineNumber++;
                const lineMatches = line.matchAll(regex);
                for (const match of lineMatches) {
                    // Get context (2 lines before and after)
                    const contextStart = Math.max(0, lineNumber - 3);
                    const contextEnd = Math.min(lines.length, lineNumber + 2);
                    const context = lines.slice(contextStart, contextEnd).join("\n");
                    matches.push({
                        rule,
                        file: filePath,
                        line: lineNumber,
                        matchedText: match[0],
                        context,
                    });
                }
            }
        }
        catch (error) {
            logger.warn("rules.regex_error", {
                ruleId: rule.id,
                pattern: rule.pattern,
                error: String(error),
            });
        }
    }
    return matches;
}
/**
 * Convert rule matches to findings
 */
export function matchesToFindings(matches) {
    return matches.map((match, index) => ({
        id: `${match.rule.id}-${index + 1}`.padEnd(7, "0").slice(0, 7),
        severity: match.rule.severity,
        category: match.rule.category || "custom",
        file: match.file,
        line: match.line,
        description: match.rule.message,
        evidence: `Matched pattern: ${match.matchedText}\n\nContext:\n${match.context}`,
        confidence: match.rule.confidence || 80,
        verifications: [],
        created_at: new Date().toISOString(),
    }));
}
/**
 * Get all available rule templates
 */
export function listRuleTemplates() {
    return Object.entries(RULE_TEMPLATES).map(([id, template]) => ({
        id,
        name: template.name || id,
        severity: template.severity || "medium",
        category: template.category || "custom",
    }));
}
/**
 * Generate a sample rules configuration file
 */
export function generateSampleConfig() {
    return `# Vaspera Hardening - Custom Rules Configuration
# Place this file in .vaspera/hardening-rules.yaml

version: "1.0"

rules:
  # Use a built-in template
  - id: no-hardcoded-secrets
    enabled: true

  # Custom rule example
  - id: "trade-001"
    name: "All trades must use limit orders"
    pattern: 'submitOrder.*type:\\s*"market"'
    severity: critical
    message: "Direct market orders violate trading rules. Use limit orders."
    category: trading
    agent: security
    confidence: 95
    excludeFiles:
      - "*.test.ts"
      - "*.spec.ts"

  - id: "trade-002"
    name: "Position sizing validation"
    pattern: "positionSize\\s*=\\s*\\d+\\.?\\d*\\s*\\*"
    severity: high
    message: "Direct position sizing without Kelly Criterion validation"
    category: trading
    agent: reliability
`;
}
//# sourceMappingURL=rules.js.map