// toxic-flow-analyzer.mjs — TFA scanner: Lethal Trifecta Detection
// Post-processing correlator that detects when tool/permission combinations
// create exfiltration chains. Runs LAST in scan-orchestrator — receives
// output from all 7 prior scanners as priorResults.
// Zero external dependencies.
//
// "Lethal trifecta" (Willison / Invariant Labs):
//   1. Agent exposed to UNTRUSTED INPUT (prompt injection surface)
//   2. Agent has access to SENSITIVE DATA via tools
//   3. An EXFILTRATION SINK exists (HTTP, email, file write to public paths)
//
// Three phases:
//   Phase 1 — Component Inventory: build capability matrix from plugin frontmatter
//   Phase 2 — Trifecta Classification: classify each component's 3 legs
//   Phase 3 — Trifecta Detection: find dangerous combinations, apply mitigations
//
// OWASP mappings: ASI01, ASI02, ASI05, MCP1, MCP3, LLM01, LLM02, LLM06

import { join } from 'node:path';
import { existsSync } from 'node:fs';
import { readdir, readFile } from 'node:fs/promises';
import { readTextFile } from './lib/file-discovery.mjs';
import { parseFrontmatter, classifyPluginFile } from './lib/yaml-frontmatter.mjs';
import { finding, scannerResult } from './lib/output.mjs';
import { SEVERITY } from './lib/severity.mjs';

const SCANNER = 'TFA';

// ---------------------------------------------------------------------------
// Tool classification sets
// ---------------------------------------------------------------------------

/** Tools that expose the component to untrusted/external input. */
const INPUT_SURFACE_TOOLS = new Set(['Bash']);

/** Tools that grant read access to potentially sensitive data. */
const DATA_ACCESS_TOOLS = new Set(['Read', 'Glob', 'Grep']);

/** Tools that can send data outside the process boundary. */
const EXFIL_SINK_TOOLS = new Set(['Bash', 'WebFetch', 'WebSearch']);

/** Tools that allow spawning sub-agents (indirect capability escalation). */
const DELEGATION_TOOLS = new Set(['Agent', 'Task']);

// ---------------------------------------------------------------------------
// Keyword classification sets
// ---------------------------------------------------------------------------

/** Body/description keywords indicating untrusted input exposure. */
const INPUT_KEYWORDS = [
  '$arguments', 'user input', 'user-provided', 'untrusted',
  'tool_input', 'user_input', 'remote', 'url', 'github url',
];

/** Body/description keywords indicating sensitive data handling. */
const SENSITIVE_KEYWORDS = [
  'secret', 'credential', 'token', 'key', 'password', 'auth',
  '.env', '.ssh', '.aws', 'keychain', 'vault', 'certificate',
];

/** Body/description keywords indicating network/exfil operations. */
const EXFIL_KEYWORDS = [
  'fetch', 'http', 'webhook', 'upload', 'send', 'curl',
  'network', 'api', 'endpoint', 'transfer', 'exfil',
];

// ---------------------------------------------------------------------------
// Hook guard patterns — known hooks that mitigate exfil paths
// ---------------------------------------------------------------------------

const EXFIL_GUARD_HOOKS = [
  'pre-bash-destructive',
  'post-mcp-verify',
  'pre-install-supply-chain',
];

const INPUT_GUARD_HOOKS = [
  'pre-prompt-inject-scan',
];

// ---------------------------------------------------------------------------
// Phase 1: Component Inventory
// ---------------------------------------------------------------------------

/**
 * @typedef {Object} ComponentNode
 * @property {string} name
 * @property {'command'|'agent'|'skill'|'unknown'} type
 * @property {string[]} tools
 * @property {string} description - lowercase
 * @property {string} body - lowercase full content
 * @property {string} file - relative path
 * @property {boolean} hasInputSurface
 * @property {boolean} hasDataAccess
 * @property {boolean} hasExfilSink
 * @property {string[]} inputEvidence
 * @property {string[]} accessEvidence
 * @property {string[]} exfilEvidence
 */

/**
 * Build component inventory from plugin frontmatter.
 * @param {string} targetPath
 * @returns {Promise<ComponentNode[]>}
 */
async function buildComponentInventory(targetPath) {
  const components = [];
  const COMPONENT_DIRS = ['commands', 'agents', 'skills'];

  for (const dir of COMPONENT_DIRS) {
    const absDir = join(targetPath, dir);
    if (!existsSync(absDir)) continue;

    let entries;
    try {
      entries = await readdir(absDir, { withFileTypes: true });
    } catch { continue; }

    for (const entry of entries) {
      if (!entry.isFile() || !entry.name.endsWith('.md')) continue;

      const absFile = join(absDir, entry.name);
      const relFile = `${dir}/${entry.name}`;
      const content = await readTextFile(absFile);
      if (!content) continue;

      const fm = parseFrontmatter(content);
      if (!fm) continue;

      const type = classifyPluginFile(relFile, fm);
      const rawTools = fm.tools || fm.allowed_tools || fm['allowed-tools'] || [];
      const tools = Array.isArray(rawTools)
        ? rawTools.map(t => String(t).trim()).filter(Boolean)
        : String(rawTools).split(',').map(t => t.trim()).filter(Boolean);

      const description = typeof fm.description === 'string' ? fm.description.toLowerCase() : '';

      components.push({
        name: fm.name || entry.name.replace(/\.md$/, ''),
        type,
        tools,
        description,
        body: content.toLowerCase(),
        file: relFile,
        hasInputSurface: false,
        hasDataAccess: false,
        hasExfilSink: false,
        inputEvidence: [],
        accessEvidence: [],
        exfilEvidence: [],
      });
    }
  }

  return components;
}

/**
 * Detect active hook guards from hooks.json.
 * @param {string} targetPath
 * @returns {Promise<string[]>} List of active guard script basenames (lowercase)
 */
async function detectActiveGuards(targetPath) {
  const guards = [];
  const hooksPath = join(targetPath, 'hooks', 'hooks.json');
  if (!existsSync(hooksPath)) return guards;

  try {
    const raw = await readFile(hooksPath, 'utf-8');
    const config = JSON.parse(raw);
    const hooksRoot = config.hooks || config;
    if (typeof hooksRoot !== 'object' || Array.isArray(hooksRoot)) return guards;

    for (const descriptors of Object.values(hooksRoot)) {
      if (!Array.isArray(descriptors)) continue;
      for (const descriptor of descriptors) {
        const innerHooks = descriptor.hooks;
        if (!Array.isArray(innerHooks)) continue;
        for (const hookEntry of innerHooks) {
          if (hookEntry.type !== 'command' || typeof hookEntry.command !== 'string') continue;
          const cmd = hookEntry.command.toLowerCase();
          guards.push(cmd);
        }
      }
    }
  } catch {
    // Malformed hooks.json — no guards detected
  }

  return guards;
}

/**
 * Check if MCP servers are configured (presence = additional tool surface).
 * @param {string} targetPath
 * @returns {Promise<boolean>}
 */
async function hasMcpServers(targetPath) {
  const mcpJsonPath = join(targetPath, '.mcp.json');
  if (existsSync(mcpJsonPath)) {
    try {
      const raw = await readFile(mcpJsonPath, 'utf-8');
      const config = JSON.parse(raw);
      return Object.keys(config.mcpServers || config.servers || {}).length > 0;
    } catch { /* ignore */ }
  }
  return false;
}

// ---------------------------------------------------------------------------
// Phase 2: Trifecta Classification
// ---------------------------------------------------------------------------

/**
 * Classify each component's 3 trifecta legs using tools, keywords, and scanner findings.
 * Mutates components in place.
 * @param {ComponentNode[]} components
 * @param {Record<string, object>} priorResults - keyed by scanner short name
 * @param {boolean} mcpPresent
 */
function classifyTrifectaLegs(components, priorResults, mcpPresent) {
  for (const comp of components) {
    // --- Leg 1: Untrusted Input Surface ---

    // Tool-based: Bash can read stdin, env vars, pipe input
    if (comp.tools.some(t => INPUT_SURFACE_TOOLS.has(t))) {
      comp.hasInputSurface = true;
      comp.inputEvidence.push('Bash tool (stdin/env access)');
    }

    // Commands with $ARGUMENTS = direct user input
    if (comp.type === 'command' && comp.body.includes('$arguments')) {
      comp.hasInputSurface = true;
      comp.inputEvidence.push('$ARGUMENTS in command body');
    }

    // Keyword-based
    for (const kw of INPUT_KEYWORDS) {
      if (comp.description.includes(kw) || comp.body.includes(kw)) {
        comp.hasInputSurface = true;
        comp.inputEvidence.push(`keyword "${kw}"`);
        break;
      }
    }

    // MCP presence = additional input surface for all components
    if (mcpPresent) {
      comp.hasInputSurface = true;
      if (!comp.inputEvidence.some(e => e.includes('MCP'))) {
        comp.inputEvidence.push('MCP servers configured (tool input surface)');
      }
    }

    // --- Leg 2: Sensitive Data Access ---

    if (comp.tools.some(t => DATA_ACCESS_TOOLS.has(t))) {
      comp.hasDataAccess = true;
      const matched = comp.tools.filter(t => DATA_ACCESS_TOOLS.has(t));
      comp.accessEvidence.push(`tools: ${matched.join(', ')}`);
    }

    // Bash can cat/find/grep files
    if (comp.tools.includes('Bash')) {
      comp.hasDataAccess = true;
      if (!comp.accessEvidence.some(e => e.includes('Bash'))) {
        comp.accessEvidence.push('Bash tool (cat/find/grep capable)');
      }
    }

    for (const kw of SENSITIVE_KEYWORDS) {
      if (comp.description.includes(kw) || comp.body.includes(kw)) {
        comp.hasDataAccess = true;
        comp.accessEvidence.push(`keyword "${kw}"`);
        break;
      }
    }

    // --- Leg 3: Exfiltration Sink ---

    if (comp.tools.some(t => EXFIL_SINK_TOOLS.has(t))) {
      comp.hasExfilSink = true;
      const matched = comp.tools.filter(t => EXFIL_SINK_TOOLS.has(t));
      comp.exfilEvidence.push(`tools: ${matched.join(', ')}`);
    }

    // Delegation tools = indirect exfil (can spawn agents with Bash)
    if (comp.tools.some(t => DELEGATION_TOOLS.has(t))) {
      comp.hasExfilSink = true;
      const matched = comp.tools.filter(t => DELEGATION_TOOLS.has(t));
      comp.exfilEvidence.push(`delegation: ${matched.join(', ')} (can spawn capable sub-agents)`);
    }

    for (const kw of EXFIL_KEYWORDS) {
      if (comp.description.includes(kw) || comp.body.includes(kw)) {
        comp.hasExfilSink = true;
        comp.exfilEvidence.push(`keyword "${kw}"`);
        break;
      }
    }
  }

  // --- Enrich from prior scanner results ---
  enrichFromPriorResults(components, priorResults);
}

/**
 * Enrich component classifications using prior scanner findings.
 * Maps findings back to components by file path containment.
 * @param {ComponentNode[]} components
 * @param {Record<string, object>} priorResults
 */
function enrichFromPriorResults(components, priorResults) {
  if (!priorResults) return;

  // TNT (taint-tracer): LLM01 findings confirm injection surfaces, LLM02 confirm exfil
  const taintFindings = priorResults.taint?.findings || [];
  for (const f of taintFindings) {
    if (!f.file) continue;
    for (const comp of components) {
      if (!fileMatchesComponent(f.file, comp)) continue;
      if (f.owasp === 'LLM01') {
        comp.hasInputSurface = true;
        addUniqueEvidence(comp.inputEvidence, `TNT: ${f.title}`);
      }
      if (f.owasp === 'LLM02') {
        comp.hasExfilSink = true;
        addUniqueEvidence(comp.exfilEvidence, `TNT: ${f.title}`);
      }
    }
  }

  // NET (network-mapper): suspicious domains confirm exfil endpoints
  const netFindings = priorResults.network?.findings || [];
  for (const f of netFindings) {
    if (f.severity !== 'high' && f.severity !== 'critical') continue;
    if (!f.file) continue;
    for (const comp of components) {
      if (!fileMatchesComponent(f.file, comp)) continue;
      comp.hasExfilSink = true;
      addUniqueEvidence(comp.exfilEvidence, `NET: ${f.title}`);
    }
  }

  // ENT (entropy): high-entropy strings may indicate hardcoded secrets
  const entFindings = priorResults.entropy?.findings || [];
  for (const f of entFindings) {
    if (f.severity !== 'high' && f.severity !== 'critical') continue;
    if (!f.file) continue;
    for (const comp of components) {
      if (!fileMatchesComponent(f.file, comp)) continue;
      comp.hasDataAccess = true;
      addUniqueEvidence(comp.accessEvidence, `ENT: ${f.title}`);
    }
  }

  // UNI (unicode): hidden Unicode confirms injection payloads
  const uniFindings = priorResults.unicode?.findings || [];
  for (const f of uniFindings) {
    if (!f.file) continue;
    for (const comp of components) {
      if (!fileMatchesComponent(f.file, comp)) continue;
      comp.hasInputSurface = true;
      addUniqueEvidence(comp.inputEvidence, `UNI: ${f.title}`);
    }
  }

  // PRM (permission-mapper): excessive agency findings strengthen classification
  const prmFindings = priorResults.permission?.findings || [];
  for (const f of prmFindings) {
    if (!f.file) continue;
    for (const comp of components) {
      if (!fileMatchesComponent(f.file, comp)) continue;
      // PRM findings about dangerous tool combos strengthen all legs
      if (f.severity === 'high') {
        comp.hasExfilSink = true;
        addUniqueEvidence(comp.exfilEvidence, `PRM: ${f.title}`);
      }
    }
  }
}

/**
 * Check if a finding's file path belongs to a component's directory.
 * @param {string} findingFile - Relative path from finding
 * @param {ComponentNode} comp
 * @returns {boolean}
 */
function fileMatchesComponent(findingFile, comp) {
  // Direct match
  if (findingFile === comp.file) return true;
  // Same directory (e.g., finding in commands/scan.md matches component commands/scan)
  const compDir = comp.file.replace(/\/[^/]+$/, '/');
  return findingFile.startsWith(compDir);
}

/**
 * Add evidence string only if not already present (avoid duplicates).
 * @param {string[]} arr
 * @param {string} evidence
 */
function addUniqueEvidence(arr, evidence) {
  if (!arr.some(e => e === evidence)) {
    arr.push(evidence);
  }
}

// ---------------------------------------------------------------------------
// Phase 3: Trifecta Detection & Scoring
// ---------------------------------------------------------------------------

/**
 * Detect trifecta patterns and generate findings.
 * @param {ComponentNode[]} components
 * @param {string[]} activeGuards - Hook command strings
 * @returns {object[]} Array of finding objects
 */
function detectTrifectas(components, activeGuards) {
  const findings = [];
  const hasExfilGuard = activeGuards.some(g =>
    EXFIL_GUARD_HOOKS.some(h => g.includes(h))
  );
  const hasInputGuard = activeGuards.some(g =>
    INPUT_GUARD_HOOKS.some(h => g.includes(h))
  );

  // --- Direct trifectas: all 3 legs in one component ---
  for (const comp of components) {
    if (!comp.hasInputSurface || !comp.hasDataAccess || !comp.hasExfilSink) continue;

    let severity = SEVERITY.CRITICAL;

    // Mitigation: if hooks guard the exfil or input path, downgrade
    if (hasExfilGuard && hasInputGuard) {
      severity = SEVERITY.MEDIUM;
    } else if (hasExfilGuard || hasInputGuard) {
      severity = SEVERITY.HIGH;
    }

    findings.push(finding({
      scanner: SCANNER,
      severity,
      title: `Lethal trifecta: ${comp.name} (${comp.type})`,
      description:
        `Component "${comp.name}" has all three legs of the lethal trifecta: ` +
        `untrusted input surface, sensitive data access, and an exfiltration sink. ` +
        `A successful prompt injection targeting this component could read sensitive ` +
        `data and exfiltrate it in a single chain.` +
        (hasExfilGuard || hasInputGuard
          ? ` Mitigated by active hook guards (severity reduced).`
          : ` No hook guards detected for this chain.`),
      file: comp.file,
      evidence: formatTrifectaEvidence(comp),
      owasp: 'ASI01, ASI02, ASI05',
      recommendation:
        'Apply principle of least privilege: separate read-only analysis from ' +
        'write/network capabilities into distinct components. Add hook guards ' +
        '(pre-bash-destructive, pre-prompt-inject-scan) to mitigate injection + exfil paths.',
    }));
  }

  // --- Cross-component trifectas: 2 legs in one, 3rd in another ---
  const twoLeg = components.filter(c =>
    (c.hasInputSurface && c.hasDataAccess && !c.hasExfilSink) ||
    (c.hasInputSurface && !c.hasDataAccess && c.hasExfilSink) ||
    (!c.hasInputSurface && c.hasDataAccess && c.hasExfilSink)
  );

  // Components that complete the missing leg
  const inputSources = components.filter(c => c.hasInputSurface);
  const dataAccessors = components.filter(c => c.hasDataAccess);
  const exfilSinks = components.filter(c => c.hasExfilSink);

  const reportedCrossPairs = new Set();

  for (const comp of twoLeg) {
    // Already reported as direct trifecta?
    if (comp.hasInputSurface && comp.hasDataAccess && comp.hasExfilSink) continue;

    let complementary = [];
    let missingLeg = '';

    if (!comp.hasInputSurface) {
      complementary = inputSources.filter(c => c !== comp);
      missingLeg = 'input surface';
    } else if (!comp.hasDataAccess) {
      complementary = dataAccessors.filter(c => c !== comp);
      missingLeg = 'data access';
    } else {
      complementary = exfilSinks.filter(c => c !== comp);
      missingLeg = 'exfil sink';
    }

    if (complementary.length === 0) continue;

    // Only report the most significant complementary component (avoid finding flood)
    const bestMatch = complementary[0];
    const pairKey = [comp.name, bestMatch.name].sort().join('|');
    if (reportedCrossPairs.has(pairKey)) continue;
    reportedCrossPairs.add(pairKey);

    let severity = SEVERITY.HIGH;
    if (hasExfilGuard && hasInputGuard) {
      severity = SEVERITY.LOW;
    } else if (hasExfilGuard || hasInputGuard) {
      severity = SEVERITY.MEDIUM;
    }

    findings.push(finding({
      scanner: SCANNER,
      severity,
      title: `Cross-component trifecta: ${comp.name} + ${bestMatch.name}`,
      description:
        `Components "${comp.name}" and "${bestMatch.name}" together complete the lethal trifecta. ` +
        `"${comp.name}" provides ${describeLegsCovered(comp)} while "${bestMatch.name}" ` +
        `provides the missing ${missingLeg}. If an attacker can influence both components ` +
        `(e.g., via prompt injection propagating through delegation), this chain enables ` +
        `data exfiltration.` +
        (hasExfilGuard || hasInputGuard
          ? ` Mitigated by active hook guards.`
          : ''),
      file: comp.file,
      evidence:
        `${comp.name}: ${describeLegsCovered(comp)} | ` +
        `${bestMatch.name}: ${missingLeg} via ${describeLegsEvidence(bestMatch, missingLeg)}`,
      owasp: 'ASI01, ASI02, ASI05',
      recommendation:
        'Reduce tool surface on components that complete trifecta chains. ' +
        'Ensure hook guards cover all exfiltration and injection paths. ' +
        'Consider whether delegation between these components can be restricted.',
    }));
  }

  // --- Project-level trifecta: all 3 legs exist somewhere ---
  const hasAnyInput = components.some(c => c.hasInputSurface);
  const hasAnyAccess = components.some(c => c.hasDataAccess);
  const hasAnyExfil = components.some(c => c.hasExfilSink);

  if (hasAnyInput && hasAnyAccess && hasAnyExfil) {
    // Only emit this if we haven't already reported direct or cross-component trifectas
    const directCount = findings.filter(f => f.title.startsWith('Lethal trifecta:')).length;
    const crossCount = findings.filter(f => f.title.startsWith('Cross-component')).length;

    if (directCount === 0 && crossCount === 0) {
      let severity = SEVERITY.MEDIUM;
      if (hasExfilGuard && hasInputGuard) {
        severity = SEVERITY.LOW;
      }

      findings.push(finding({
        scanner: SCANNER,
        severity,
        title: 'Project-level trifecta: all three legs present',
        description:
          'This project contains components with untrusted input surfaces, ' +
          'sensitive data access, and exfiltration sinks — but no single component ' +
          'or pair completes the full chain. The trifecta exists at the project level, ' +
          'which is a lower risk but still worth monitoring. A multi-hop attack chain ' +
          'through delegation or shared state could connect these legs.',
        file: null,
        evidence:
          `Input: ${inputSources.map(c => c.name).slice(0, 3).join(', ')} | ` +
          `Access: ${dataAccessors.map(c => c.name).slice(0, 3).join(', ')} | ` +
          `Exfil: ${exfilSinks.map(c => c.name).slice(0, 3).join(', ')}`,
        owasp: 'ASI01, ASI02',
        recommendation:
          'Monitor for capability escalation through delegation chains. ' +
          'Ensure hook guards are active for injection and exfiltration paths.',
      }));
    }
  }

  return findings;
}

// ---------------------------------------------------------------------------
// Evidence formatting helpers
// ---------------------------------------------------------------------------

/**
 * Format all 3 legs of evidence for a component into a compact string.
 * @param {ComponentNode} comp
 * @returns {string}
 */
function formatTrifectaEvidence(comp) {
  const parts = [];
  if (comp.inputEvidence.length > 0) {
    parts.push(`Input: ${comp.inputEvidence.slice(0, 2).join('; ')}`);
  }
  if (comp.accessEvidence.length > 0) {
    parts.push(`Access: ${comp.accessEvidence.slice(0, 2).join('; ')}`);
  }
  if (comp.exfilEvidence.length > 0) {
    parts.push(`Exfil: ${comp.exfilEvidence.slice(0, 2).join('; ')}`);
  }
  return parts.join(' | ');
}

/**
 * Describe which legs a component covers.
 * @param {ComponentNode} comp
 * @returns {string}
 */
function describeLegsCovered(comp) {
  const legs = [];
  if (comp.hasInputSurface) legs.push('input surface');
  if (comp.hasDataAccess) legs.push('data access');
  if (comp.hasExfilSink) legs.push('exfil sink');
  return legs.join(' + ');
}

/**
 * Describe evidence for a specific leg on a component.
 * @param {ComponentNode} comp
 * @param {string} leg - 'input surface' | 'data access' | 'exfil sink'
 * @returns {string}
 */
function describeLegsEvidence(comp, leg) {
  switch (leg) {
    case 'input surface': return comp.inputEvidence[0] || 'inferred';
    case 'data access': return comp.accessEvidence[0] || 'inferred';
    case 'exfil sink': return comp.exfilEvidence[0] || 'inferred';
    default: return 'unknown';
  }
}

// ---------------------------------------------------------------------------
// Plugin detection (reused from permission-mapper pattern)
// ---------------------------------------------------------------------------

/**
 * Check if targetPath is a Claude Code plugin.
 * @param {string} targetPath
 * @returns {boolean}
 */
function isPlugin(targetPath) {
  if (existsSync(join(targetPath, '.claude-plugin', 'plugin.json'))) return true;
  if (existsSync(join(targetPath, 'plugin.json'))) return true;
  if (existsSync(join(targetPath, 'plugin.fixture.json'))) return true;
  if (existsSync(join(targetPath, 'commands'))) return true;
  return false;
}

// ---------------------------------------------------------------------------
// Public scanner entry point
// ---------------------------------------------------------------------------

/**
 * Scan a target path for lethal trifecta patterns.
 *
 * This scanner is a post-processing correlator: it reads plugin component
 * frontmatter to build a capability inventory, then uses prior scanner
 * findings to detect dangerous tool/permission combinations.
 *
 * @param {string} targetPath - Absolute path to scan
 * @param {object} discovery - Pre-computed file discovery (used for file count only)
 * @param {Record<string, object>} [priorResults] - Output from prior scanners
 * @returns {Promise<object>} Scanner result envelope
 */
export async function scan(targetPath, discovery, priorResults) {
  const start = Date.now();

  // Skip non-plugin targets — TFA analyzes plugin structure
  if (!isPlugin(targetPath)) {
    return scannerResult('toxic-flow', 'skipped', [], 0, Date.now() - start);
  }

  try {
    // Phase 1: Component Inventory
    const components = await buildComponentInventory(targetPath);
    if (components.length === 0) {
      return scannerResult('toxic-flow', 'ok', [], 0, Date.now() - start);
    }

    const activeGuards = await detectActiveGuards(targetPath);
    const mcpPresent = await hasMcpServers(targetPath);

    // Phase 2: Trifecta Classification
    classifyTrifectaLegs(components, priorResults || {}, mcpPresent);

    // Phase 3: Trifecta Detection
    const findings = detectTrifectas(components, activeGuards);

    const filesScanned = components.length;
    return scannerResult('toxic-flow', 'ok', findings, filesScanned, Date.now() - start);
  } catch (err) {
    return scannerResult(
      'toxic-flow', 'error', [], 0, Date.now() - start, err.message
    );
  }
}
