// memory-poisoning-scanner.mjs — Detects cognitive state poisoning in CLAUDE.md,
// memory files, .claude/rules, and other agent configuration files.
//
// "Cognitive State Traps" (Franklin et al., Google DeepMind, 2025):
// Latent Memory Poisoning + RAG Knowledge Poisoning. CLAUDE.md and memory/*.md
// are Claude Code's equivalent of RAG corpora — loaded automatically into context,
// potentially containing instructions the agent follows uncritically.
//
// Zero external dependencies — Node.js builtins only.
// OWASP coverage: LLM01 (Prompt Injection), ASI02 (Excessive Agency)

import { readTextFile } from './lib/file-discovery.mjs';
import { finding, scannerResult } from './lib/output.mjs';
import { SEVERITY } from './lib/severity.mjs';
import { scanForInjection } from './lib/injection-patterns.mjs';
import { isBase64Like, isHexBlob } from './lib/string-utils.mjs';

// ---------------------------------------------------------------------------
// Target file patterns — files that influence agent cognition
// ---------------------------------------------------------------------------

/** Glob-like path matchers for memory/config files */
const MEMORY_FILE_PATTERNS = [
  /(?:^|\/)CLAUDE\.md$/i,
  /(?:^|\/)\.claude\/rules\/[^/]+\.md$/,
  /(?:^|\/)\.claude\/agents\/[^/]+\.md$/,  // E15 (v7.2.0) — agent files are loaded as Claude Code subagent system prompts and are a direct memory-poisoning surface
  /(?:^|\/)memory\/[^/]+\.md$/,
  /(?:^|\/)REMEMBER\.md$/i,
  /\.local\.md$/,
  /(?:^|\/)\.claude-plugin\/plugin\.json$/,
];

/** Files that are CLAUDE.md (may legitimately contain shell examples) */
const CLAUDE_MD_PATTERN = /(?:^|\/)CLAUDE\.md$/i;

/** Files that should NOT contain shell commands (memory, rules, REMEMBER) */
const STRICT_FILES_PATTERN = /(?:^|\/)(?:memory\/[^/]+\.md|REMEMBER\.md|\.claude\/rules\/[^/]+\.md)$/i;

// ---------------------------------------------------------------------------
// Detection patterns
// ---------------------------------------------------------------------------

/** Shell commands that should not appear in memory/rules files */
const SHELL_COMMAND_RE = /(?:^|[`'";\s|&])(?:curl|wget|bash|sh|eval|exec|chmod\s+[+0-7]|npm\s+install|pip3?\s+install|gem\s+install|cargo\s+install)\b/i;

/** Credential path references */
const CREDENTIAL_PATH_RE = /(?:~\/|\/home\/|\/root\/)?\.(?:ssh|aws|gnupg|config\/gcloud)\/|(?:^|[\s/"'`])(?:id_rsa|id_ed25519|id_ecdsa|wallet\.dat|keystore|credentials\.json|\.env(?:\.\w+)?|\.netrc|\.pgpass|kubeconfig|service[_-]account[_-]key)(?:[\s/"'`]|$)/i;

/** Permission expansion directives */
const PERMISSION_EXPANSION_RE = /(?:allowed-tools\s*[=:]\s*.*(?:Bash|Write|Edit|all)|bypassPermissions\s*[=:]\s*true|dangerouslySkipPermissions|--dangerously-skip-permissions|dangerouslyAllowArbitraryPaths\s*[=:]\s*true)/i;

/** Suspicious exfiltration domains (subset of network-mapper) */
const SUSPICIOUS_DOMAINS = new Set([
  'webhook.site', 'webhookinbox.com',
  'requestbin.com', 'requestbin.net',
  'pipedream.net', 'hookbin.com',
  'beeceptor.com', 'requestcatcher.com',
  'ngrok.io', 'ngrok.app', 'ngrok-free.app',
  'serveo.net', 'localtunnel.me',
  'pastebin.com', 'paste.ee',
  'transfer.sh', 'file.io',
  'temp.sh', '0x0.st',
]);

/** URL extraction regex */
const URL_RE = /https?:\/\/([a-zA-Z0-9][-a-zA-Z0-9]*(?:\.[a-zA-Z0-9][-a-zA-Z0-9]*)+)(?:\/[^\s"'`)\]]*)?/g;

/** Base64 token regex (40+ chars) */
const BASE64_TOKEN_RE = /[A-Za-z0-9+/]{40,}={0,3}/g;

/** Hex blob regex (64+ chars) */
const HEX_TOKEN_RE = /(?:0x)?[0-9a-fA-F]{64,}/g;

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

function isMemoryFile(relPath) {
  return MEMORY_FILE_PATTERNS.some(p => p.test(relPath));
}

function isStrictFile(relPath) {
  return STRICT_FILES_PATTERN.test(relPath);
}

function isClaudeMd(relPath) {
  return CLAUDE_MD_PATTERN.test(relPath);
}

/**
 * Extract domain from a URL match.
 * @param {string} host - hostname from URL regex
 * @returns {string} - domain to check against suspicious set
 */
function getDomainForCheck(host) {
  const parts = host.toLowerCase().split('.');
  // Return last two parts (e.g., "webhook.site" from "abc.webhook.site")
  if (parts.length >= 2) {
    return parts.slice(-2).join('.');
  }
  return host.toLowerCase();
}

// ---------------------------------------------------------------------------
// Detection functions — each returns an array of findings
// ---------------------------------------------------------------------------

/**
 * Check 1: Injection patterns via shared injection-patterns.mjs
 */
function detectInjection(content, relPath) {
  const results = [];
  const scan = scanForInjection(content);

  if (!scan.found) return results;

  for (const { label, severity } of scan.patterns) {
    let sev;
    if (severity === 'critical') sev = SEVERITY.CRITICAL;
    else if (severity === 'high') sev = SEVERITY.HIGH;
    else sev = SEVERITY.MEDIUM;

    results.push(finding({
      scanner: 'MEM',
      severity: sev,
      title: `Injection pattern in cognitive state file: ${label}`,
      description:
        `Memory/config file "${relPath}" contains a prompt injection pattern: "${label}". ` +
        'These files are loaded into agent context automatically and can manipulate agent behavior.',
      file: relPath,
      evidence: label,
      owasp: 'LLM01',
      recommendation:
        'Remove the injection pattern. Review the file for unauthorized modifications. ' +
        'Consider adding integrity checks for CLAUDE.md and memory files.',
    }));
  }

  return results;
}

/**
 * Check 2: Shell commands in files that shouldn't have them
 */
function detectShellCommands(content, relPath) {
  const results = [];
  const lines = content.split('\n');
  const strict = isStrictFile(relPath);
  const claudeMd = isClaudeMd(relPath);

  for (let i = 0; i < lines.length; i++) {
    const line = lines[i];
    if (!SHELL_COMMAND_RE.test(line)) continue;

    // In CLAUDE.md, shell commands in code blocks are legitimate
    if (claudeMd) {
      // Check if we're inside a code block (simple heuristic: track ``` state)
      let inCodeBlock = false;
      for (let j = 0; j <= i; j++) {
        if (lines[j].trim().startsWith('```')) inCodeBlock = !inCodeBlock;
      }
      if (inCodeBlock) continue; // legitimate code example

      results.push(finding({
        scanner: 'MEM',
        severity: SEVERITY.LOW,
        title: 'Shell command outside code block in CLAUDE.md',
        description:
          `CLAUDE.md line ${i + 1} contains a shell command outside a code block. ` +
          'This may be a legitimate instruction or an attempt to get the agent to execute commands.',
        file: relPath,
        line: i + 1,
        evidence: line.trim().slice(0, 120),
        owasp: 'LLM01',
        recommendation:
          'Verify this shell command is intentional. If it is an instruction for the agent, ' +
          'consider whether it could be exploited via CLAUDE.md poisoning.',
      }));
    } else if (strict) {
      results.push(finding({
        scanner: 'MEM',
        severity: SEVERITY.HIGH,
        title: 'Shell command in memory/rules file',
        description:
          `Memory/rules file "${relPath}" line ${i + 1} contains a shell command. ` +
          'Memory and rules files should not contain executable commands — this is ' +
          'a strong indicator of cognitive state poisoning.',
        file: relPath,
        line: i + 1,
        evidence: line.trim().slice(0, 120),
        owasp: 'LLM01',
        recommendation:
          'Remove the shell command from this memory file. Memory files should contain ' +
          'state and context only, never executable instructions.',
      }));
    }
  }

  return results;
}

/**
 * Check 3: Suspicious URLs (exfiltration domains)
 */
function detectSuspiciousUrls(content, relPath) {
  const results = [];
  const lines = content.split('\n');

  for (let i = 0; i < lines.length; i++) {
    const line = lines[i];
    URL_RE.lastIndex = 0;
    let match;
    while ((match = URL_RE.exec(line)) !== null) {
      const host = match[1];
      const domain = getDomainForCheck(host);
      if (SUSPICIOUS_DOMAINS.has(domain)) {
        results.push(finding({
          scanner: 'MEM',
          severity: SEVERITY.HIGH,
          title: 'Suspicious exfiltration URL in cognitive state file',
          description:
            `File "${relPath}" line ${i + 1} references "${domain}" — a known ` +
            'data exfiltration / webhook interception service. In a memory or CLAUDE.md file, ' +
            'this could instruct the agent to send data to an attacker-controlled endpoint.',
          file: relPath,
          line: i + 1,
          evidence: match[0].slice(0, 100),
          owasp: 'LLM01',
          recommendation:
            'Remove the suspicious URL. Investigate how it was introduced. ' +
            'Memory and config files should never reference webhook/tunnel services.',
        }));
      }
    }
  }

  return results;
}

/**
 * Check 4: Credential path references
 */
function detectCredentialPaths(content, relPath) {
  const results = [];
  const lines = content.split('\n');

  for (let i = 0; i < lines.length; i++) {
    const line = lines[i];
    const match = line.match(CREDENTIAL_PATH_RE);
    if (!match) continue;

    // In CLAUDE.md inside code blocks, credential path refs may be legitimate documentation
    if (isClaudeMd(relPath)) {
      let inCodeBlock = false;
      for (let j = 0; j <= i; j++) {
        if (lines[j].trim().startsWith('```')) inCodeBlock = !inCodeBlock;
      }
      if (inCodeBlock) continue;
    }

    results.push(finding({
      scanner: 'MEM',
      severity: SEVERITY.HIGH,
      title: 'Credential path reference in cognitive state file',
      description:
        `File "${relPath}" line ${i + 1} references a credential path (${match[0].trim()}). ` +
        'Memory files that reference credential locations could be used to instruct the agent ' +
        'to access sensitive key material.',
      file: relPath,
      line: i + 1,
      evidence: match[0].trim().slice(0, 80),
      owasp: 'ASI02',
      recommendation:
        'Remove credential path references from memory/config files. ' +
        'If credential paths need documentation, use a separate secured document.',
    }));
  }

  return results;
}

/**
 * Check 5: Permission expansion directives
 */
function detectPermissionExpansion(content, relPath) {
  const results = [];
  const lines = content.split('\n');

  for (let i = 0; i < lines.length; i++) {
    const line = lines[i];
    const match = line.match(PERMISSION_EXPANSION_RE);
    if (!match) continue;

    results.push(finding({
      scanner: 'MEM',
      severity: SEVERITY.CRITICAL,
      title: 'Permission expansion directive in cognitive state file',
      description:
        `File "${relPath}" line ${i + 1} contains a permission expansion directive: ` +
        `"${match[0].trim()}". This could grant the agent excessive capabilities through ` +
        'configuration poisoning.',
      file: relPath,
      line: i + 1,
      evidence: match[0].trim().slice(0, 100),
      owasp: 'ASI02',
      recommendation:
        'Remove the permission expansion directive. Agent permissions should be configured ' +
        'in settings.json with deny-first approach, not in memory or rules files.',
    }));
  }

  return results;
}

/**
 * Check 6: Encoded payloads (base64 / hex blobs)
 */
function detectEncodedPayloads(content, relPath) {
  const results = [];
  const lines = content.split('\n');

  for (let i = 0; i < lines.length; i++) {
    const line = lines[i];

    // Check base64
    BASE64_TOKEN_RE.lastIndex = 0;
    let match;
    while ((match = BASE64_TOKEN_RE.exec(line)) !== null) {
      if (isBase64Like(match[0])) {
        results.push(finding({
          scanner: 'MEM',
          severity: SEVERITY.MEDIUM,
          title: 'Base64-encoded payload in cognitive state file',
          description:
            `File "${relPath}" line ${i + 1} contains a base64-encoded string (${match[0].length} chars). ` +
            'Encoded payloads in memory files can hide injection instructions or exfiltration commands ' +
            'that evade text-based detection.',
          file: relPath,
          line: i + 1,
          evidence: `${match[0].slice(0, 40)}... (${match[0].length} chars)`,
          owasp: 'LLM01',
          recommendation:
            'Decode and inspect the base64 content. Remove if it contains instructions or commands. ' +
            'Memory files should contain plain text only.',
        }));
      }
    }

    // Check hex blobs
    HEX_TOKEN_RE.lastIndex = 0;
    while ((match = HEX_TOKEN_RE.exec(line)) !== null) {
      if (isHexBlob(match[0])) {
        results.push(finding({
          scanner: 'MEM',
          severity: SEVERITY.MEDIUM,
          title: 'Hex-encoded blob in cognitive state file',
          description:
            `File "${relPath}" line ${i + 1} contains a hex-encoded blob (${match[0].length} chars). ` +
            'Hex-encoded data in memory files can conceal malicious payloads.',
          file: relPath,
          line: i + 1,
          evidence: `${match[0].slice(0, 40)}... (${match[0].length} chars)`,
          owasp: 'LLM01',
          recommendation:
            'Decode and inspect the hex content. Remove if it contains instructions or commands.',
        }));
      }
    }
  }

  return results;
}

// ---------------------------------------------------------------------------
// Main scanner export
// ---------------------------------------------------------------------------

/**
 * Scan all discovered files for memory/cognitive state poisoning.
 * Only processes files matching MEMORY_FILE_PATTERNS.
 *
 * @param {string} targetPath        - Absolute root path being scanned
 * @param {{ files: import('./lib/file-discovery.mjs').FileInfo[] }} discovery
 * @returns {Promise<object>}        - scannerResult envelope
 */
export async function scan(targetPath, discovery) {
  const startMs = Date.now();
  const findings = [];
  let filesScanned = 0;

  try {
    for (const fileInfo of discovery.files) {
      // Only scan memory/config files
      if (!isMemoryFile(fileInfo.relPath)) continue;

      const content = await readTextFile(fileInfo.absPath);
      if (content === null) continue;

      filesScanned++;

      // Run all 6 detectors
      findings.push(...detectInjection(content, fileInfo.relPath));
      findings.push(...detectShellCommands(content, fileInfo.relPath));
      findings.push(...detectSuspiciousUrls(content, fileInfo.relPath));
      findings.push(...detectCredentialPaths(content, fileInfo.relPath));
      findings.push(...detectPermissionExpansion(content, fileInfo.relPath));
      findings.push(...detectEncodedPayloads(content, fileInfo.relPath));
    }

    const durationMs = Date.now() - startMs;
    return scannerResult('memory-poisoning-scanner', 'ok', findings, filesScanned, durationMs);

  } catch (err) {
    const durationMs = Date.now() - startMs;
    return scannerResult(
      'memory-poisoning-scanner',
      'error',
      findings,
      filesScanned,
      durationMs,
      err.message,
    );
  }
}
