/**
 * Antagonist Attack Narrative Synthesizer
 *
 * Synthesizes individual findings into attack narratives by:
 * 1. Building attack graphs from findings and chains
 * 2. Finding complete attack paths
 * 3. Mapping to MITRE ATT&CK kill chain
 * 4. Generating human-readable narratives
 *
 * @module agents/antagonist/synthesizer
 */
import Anthropic from "@anthropic-ai/sdk";
import { SYNTHESIS_SYSTEM_PROMPT, buildSynthesisPrompt, } from "./prompts.js";
import { logger } from "../../logger.js";
/**
 * Maps finding categories to kill chain phases
 */
const CATEGORY_TO_PHASE = {
    "sensitive-disclosure": "reconnaissance",
    "path-traversal": "discovery",
    "sql-injection": "initial-access",
    "command-injection": "execution",
    "code-injection": "execution",
    xss: "initial-access",
    "auth-bypass": "initial-access",
    "broken-access-control": "privilege-escalation",
    "session-management": "credential-access",
    "hardcoded-secret": "credential-access",
    ssrf: "lateral-movement",
    "exfil-path": "exfiltration",
    "prompt-injection": "initial-access",
    "excessive-agency": "execution",
};
/**
 * Maps severity to difficulty estimation
 */
const SEVERITY_TO_DIFFICULTY = {
    critical: "trivial",
    high: "easy",
    medium: "moderate",
    low: "hard",
    info: "expert",
};
/**
 * Build attack graph from findings and exploit chains
 */
function buildAttackGraph(findings, chains, paths) {
    const graph = new Map();
    for (const finding of findings) {
        const phase = CATEGORY_TO_PHASE[finding.category] || "execution";
        graph.set(finding.id, {
            findingId: finding.id,
            finding,
            phase,
            inEdges: [],
            outEdges: [],
        });
    }
    for (const chain of chains) {
        for (let i = 0; i < chain.steps.length - 1; i++) {
            const source = chain.steps[i].findingId;
            const target = chain.steps[i + 1].findingId;
            const sourceNode = graph.get(source);
            const targetNode = graph.get(target);
            if (sourceNode && targetNode) {
                if (!sourceNode.outEdges.includes(target)) {
                    sourceNode.outEdges.push(target);
                }
                if (!targetNode.inEdges.includes(source)) {
                    targetNode.inEdges.push(source);
                }
            }
        }
    }
    return graph;
}
/**
 * Find entry points (nodes with no incoming edges and high severity)
 */
function findEntryPoints(graph) {
    const entries = [];
    for (const node of graph.values()) {
        if (node.inEdges.length === 0 &&
            ["critical", "high"].includes(node.finding.severity)) {
            entries.push(node);
        }
    }
    if (entries.length === 0) {
        for (const node of graph.values()) {
            if (node.inEdges.length === 0) {
                entries.push(node);
            }
        }
    }
    return entries.slice(0, 5);
}
/**
 * Find paths from entry point to high-value targets
 */
function findAttackPaths(graph, entryId, maxDepth = 5) {
    const paths = [];
    const visited = new Set();
    function dfs(currentId, currentPath, depth) {
        if (depth > maxDepth)
            return;
        if (visited.has(currentId))
            return;
        visited.add(currentId);
        currentPath.push(currentId);
        const node = graph.get(currentId);
        if (!node) {
            visited.delete(currentId);
            currentPath.pop();
            return;
        }
        if (node.outEdges.length === 0 && currentPath.length > 1) {
            paths.push([...currentPath]);
        }
        for (const nextId of node.outEdges) {
            dfs(nextId, currentPath, depth + 1);
        }
        visited.delete(currentId);
        currentPath.pop();
    }
    dfs(entryId, [], 0);
    return paths;
}
/**
 * Calculate overall difficulty from a path
 */
function calculatePathDifficulty(findings, path) {
    const difficulties = path.map((id) => {
        const finding = findings.find((f) => f.id === id);
        return finding ? SEVERITY_TO_DIFFICULTY[finding.severity] : "moderate";
    });
    const order = ["trivial", "easy", "moderate", "hard", "expert"];
    const maxIndex = Math.max(...difficulties.map((d) => order.indexOf(d)));
    return order[Math.min(maxIndex + 1, order.length - 1)];
}
/**
 * Calculate likelihood based on severity and path length
 */
function calculateLikelihood(findings, path) {
    const severities = path.map((id) => {
        const finding = findings.find((f) => f.id === id);
        return finding?.severity || "low";
    });
    const hasCritical = severities.includes("critical");
    const hasHigh = severities.includes("high");
    const shortPath = path.length <= 2;
    if (hasCritical && shortPath)
        return "high";
    if (hasHigh || (hasCritical && !shortPath))
        return "medium";
    return "low";
}
/**
 * Generate attack steps from path
 */
function generateAttackSteps(graph, path) {
    return path.map((id) => {
        const node = graph.get(id);
        if (!node) {
            return {
                phase: "execution",
                findingId: id,
                description: "Unknown step",
                confidence: 50,
            };
        }
        return {
            phase: node.phase,
            findingId: id,
            description: node.finding.description,
            technique: getMitreTechnique(node.finding.category),
            confidence: node.finding.confidence,
        };
    });
}
/**
 * Get MITRE technique ID from category
 */
function getMitreTechnique(category) {
    const mapping = {
        "sql-injection": "T1190",
        "command-injection": "T1059",
        xss: "T1059.007",
        "auth-bypass": "T1078",
        ssrf: "T1090",
        "path-traversal": "T1083",
        "hardcoded-secret": "T1552",
        "prompt-injection": "T1059",
    };
    return mapping[category];
}
/**
 * Generate narrative text from path
 */
function generateNarrativeText(graph, path, targetAsset) {
    const steps = [];
    for (let i = 0; i < path.length; i++) {
        const node = graph.get(path[i]);
        if (!node)
            continue;
        const prefix = i === 0 ? "First, an attacker" : i === path.length - 1 ? "Finally, the attacker" : "Then, the attacker";
        const action = describeAction(node.finding.category);
        steps.push(`${prefix} ${action} (${node.finding.file || "unknown location"}:${node.finding.line || "?"}).`);
    }
    steps.push(`This chain allows the attacker to compromise ${targetAsset}.`);
    return steps.join(" ");
}
/**
 * Describe action for category
 */
function describeAction(category) {
    const actions = {
        "sql-injection": "exploits a SQL injection vulnerability to access the database",
        "command-injection": "injects commands to execute arbitrary code",
        xss: "injects malicious scripts via cross-site scripting",
        "auth-bypass": "bypasses authentication controls",
        "broken-access-control": "escalates privileges via broken access control",
        ssrf: "exploits SSRF to access internal services",
        "path-traversal": "reads arbitrary files via path traversal",
        "hardcoded-secret": "extracts hardcoded credentials",
        "prompt-injection": "manipulates LLM behavior via prompt injection",
        "exfil-path": "exfiltrates data through the agent tool chain",
    };
    return actions[category] || `exploits ${category} vulnerability`;
}
/**
 * Synthesize attack narratives without LLM
 */
export function synthesizeNarrativesDeterministic(findings, chains, paths, config) {
    const graph = buildAttackGraph(findings, chains, paths);
    const entryPoints = findEntryPoints(graph);
    const narratives = [];
    for (const entry of entryPoints) {
        const attackPaths = findAttackPaths(graph, entry.findingId);
        for (const path of attackPaths.slice(0, 2)) {
            if (path.length < 1)
                continue;
            const findingIds = path;
            const difficulty = calculatePathDifficulty(findings, path);
            const likelihood = calculateLikelihood(findings, path);
            const steps = generateAttackSteps(graph, path);
            const targetAsset = determineTargetAsset(graph, path);
            const narrative = generateNarrativeText(graph, path, targetAsset);
            const techniques = steps
                .map((s) => s.technique)
                .filter((t) => !!t);
            const confidence = Math.round(steps.reduce((sum, s) => sum + s.confidence, 0) / steps.length);
            if (confidence < config.minConfidence)
                continue;
            narratives.push({
                id: `nar-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`,
                name: generateNarrativeName(graph, path),
                phases: steps,
                entryPoint: entry.findingId,
                targetAsset,
                impact: determineImpact(graph, path),
                difficulty,
                likelihood,
                findingIds,
                mitreTechniques: [...new Set(techniques)],
                narrative,
                recommendations: [],
                confidence,
            });
        }
    }
    for (const chain of chains) {
        const findingIds = chain.steps.map((s) => s.findingId);
        const steps = chain.steps.map((s) => ({
            phase: CATEGORY_TO_PHASE[s.finding.category] || "execution",
            findingId: s.findingId,
            description: s.finding.description,
            technique: getMitreTechnique(s.finding.category),
            confidence: s.finding.confidence,
        }));
        const confidence = Math.round(steps.reduce((sum, s) => sum + s.confidence, 0) / steps.length);
        if (confidence < config.minConfidence)
            continue;
        narratives.push({
            id: `nar-chain-${chain.id}`,
            name: chain.name,
            phases: steps,
            entryPoint: chain.steps[0]?.findingId || "",
            targetAsset: chain.impact,
            impact: chain.impact,
            difficulty: chain.difficulty,
            likelihood: chain.totalSeverity === "critical" ? "high" : "medium",
            findingIds,
            mitreTechniques: chain.mitreAttackIds,
            narrative: chain.attackScenario,
            recommendations: [],
            confidence,
        });
    }
    const uniqueNarratives = deduplicateNarratives(narratives);
    return uniqueNarratives
        .sort((a, b) => {
        const likelihoodOrder = { high: 0, medium: 1, low: 2 };
        return likelihoodOrder[a.likelihood] - likelihoodOrder[b.likelihood];
    })
        .slice(0, config.maxNarratives);
}
/**
 * Determine target asset from path
 */
function determineTargetAsset(graph, path) {
    const lastNode = graph.get(path[path.length - 1]);
    if (!lastNode)
        return "system";
    const category = lastNode.finding.category;
    const targets = {
        "sql-injection": "database",
        "command-injection": "server",
        "auth-bypass": "user accounts",
        "hardcoded-secret": "credentials",
        "exfil-path": "sensitive data",
        "prompt-injection": "AI system",
    };
    return targets[category] || "application";
}
/**
 * Determine impact from path
 */
function determineImpact(graph, path) {
    const severities = path.map((id) => graph.get(id)?.finding.severity);
    if (severities.includes("critical"))
        return "Complete system compromise";
    if (severities.includes("high"))
        return "Significant data breach";
    if (severities.includes("medium"))
        return "Partial data exposure";
    return "Limited impact";
}
/**
 * Generate narrative name from path
 */
function generateNarrativeName(graph, path) {
    const firstNode = graph.get(path[0]);
    const lastNode = graph.get(path[path.length - 1]);
    const entryType = firstNode?.finding.category || "vulnerability";
    const targetType = lastNode?.finding.category || "compromise";
    const names = {
        "sql-injection": "SQL Injection",
        xss: "XSS",
        "auth-bypass": "Auth Bypass",
        "command-injection": "RCE",
        ssrf: "SSRF",
        "prompt-injection": "Prompt Injection",
        "exfil-path": "Data Exfil",
        "hardcoded-secret": "Credential Theft",
    };
    const entry = names[entryType] || entryType;
    const target = names[targetType] || targetType;
    return `${entry} to ${target} Chain`;
}
/**
 * Deduplicate narratives by finding overlap
 */
function deduplicateNarratives(narratives) {
    const unique = [];
    for (const narrative of narratives) {
        const isDuplicate = unique.some((existing) => {
            const overlap = narrative.findingIds.filter((id) => existing.findingIds.includes(id));
            return overlap.length > narrative.findingIds.length * 0.7;
        });
        if (!isDuplicate) {
            unique.push(narrative);
        }
    }
    return unique;
}
/**
 * Synthesize attack narratives using LLM
 */
export async function synthesizeNarrativesLlm(findings, chains, paths, config) {
    const client = new Anthropic();
    const findingsStr = findings
        .map((f) => `- ${f.id}: [${f.severity}] ${f.category} at ${f.file}:${f.line} - ${f.description}`)
        .join("\n");
    const chainsStr = chains
        .map((c) => `- ${c.name}: ${c.steps.map((s) => s.findingId).join(" → ")} (${c.totalSeverity})`)
        .join("\n");
    const pathsStr = paths
        .map((p) => `- ${p.source} → ${p.sink}: ${p.description}`)
        .join("\n");
    const prompt = buildSynthesisPrompt(findingsStr, chainsStr, pathsStr);
    try {
        const response = await client.messages.create({
            model: config.llmModel || "claude-sonnet-4-20250514",
            max_tokens: 4096,
            system: SYNTHESIS_SYSTEM_PROMPT,
            messages: [{ role: "user", content: prompt }],
        });
        const content = response.content[0];
        if (content.type !== "text") {
            throw new Error("Unexpected response type");
        }
        const jsonMatch = content.text.match(/\{[\s\S]*\}/);
        if (!jsonMatch) {
            throw new Error("No JSON in response");
        }
        const parsed = JSON.parse(jsonMatch[0]);
        const narratives = parsed.narratives.map((n, i) => ({
            id: `nar-llm-${Date.now().toString(36)}-${i}`,
            name: n.name,
            phases: n.phases.map((p) => ({ ...p, confidence: n.confidence })),
            entryPoint: n.entryPoint,
            targetAsset: n.targetAsset,
            impact: n.impact,
            difficulty: n.difficulty,
            likelihood: n.likelihood,
            findingIds: n.findingIds,
            mitreTechniques: n.mitreTechniques,
            narrative: n.narrative,
            recommendations: [],
            confidence: n.confidence,
        }));
        const tokensUsed = (response.usage?.input_tokens || 0) +
            (response.usage?.output_tokens || 0);
        return { narratives, tokensUsed };
    }
    catch (error) {
        logger.warn("antagonist.llm_synthesis_failed", {
            error: error instanceof Error ? error.message : String(error),
        });
        return { narratives: [], tokensUsed: 0 };
    }
}
/**
 * Main synthesis function - combines deterministic and LLM approaches
 */
export async function synthesizeNarratives(findings, chains, paths, config) {
    const deterministicNarratives = synthesizeNarrativesDeterministic(findings, chains, paths, config);
    if (!config.useLlm || findings.length < 3) {
        return { narratives: deterministicNarratives, tokensUsed: 0 };
    }
    const { narratives: llmNarratives, tokensUsed } = await synthesizeNarrativesLlm(findings, chains, paths, config);
    const combined = deduplicateNarratives([
        ...llmNarratives,
        ...deterministicNarratives,
    ]);
    return {
        narratives: combined.slice(0, config.maxNarratives),
        tokensUsed,
    };
}
//# sourceMappingURL=synthesizer.js.map