#!/bin/sh
# NAME
#   scanWordPressBackdoors.sh - find+grep prefilter wrapper for scanWordPressBackdoors
#
# USAGE
#   scanWordPressBackdoors.sh [options] /path/to/clients
#   scanWordPressBackdoors.sh --help
#
#   Any --flag is forwarded as-is to scanWordPressBackdoors - e.g.
#   --whitelist=FILE, --max-bytes=N, --threshold=N, --max-per-site=N (run
#   `scanWordPressBackdoors.sh --help` for the full, current list). Order
#   relative to the root argument doesn't matter. --help works on its own,
#   without a root argument - it's short-circuited straight to the scanner
#   before the root directory is even checked.
#
#   Intended to be run nightly on the backup server, after replication from
#   the live server completes, e.g. from cron:
#     0 4 * * * /path/to/scanWordPressBackdoors.sh /storage/backup/nfs/jenny/www/clients >>/var/log/wp-scan.log 2>&1
#
# WHAT IT DOES
#   Locates candidate PHP files under a clients/client#/web#/web tree and
#   hands them to scanWordPressBackdoors for scoring, instead of letting the
#   Perl script walk (and open/read) every file itself:
#
#   1. find(1) enumerates *.php/*.phtml/*.phps files, pruning .git/.svn/
#      node_modules/vendor *before* descending into them (unlike the Perl
#      script's own directory walk, which can only skip scoring those files
#      after already having traversed into the directory).
#   2. grep -l prefilters that list down to files containing at least one
#      suspicious token, so the Perl script's full multi-regex scoring only
#      runs against real candidates. wp-config.php and files under
#      uploads/cache are always kept regardless of grep hit, since those get
#      special-cased scoring in the Perl script.
#   3. The final candidate list is streamed (NUL-delimited) into
#      scanWordPressBackdoors in list mode ("-").
#
# REQUIREMENTS
#   A POSIX sh, plus find/grep/xargs/sort/tr from a standard FreeBSD (or
#   Linux) base install, and perl for scanWordPressBackdoors itself. No
#   non-core Perl modules or GNU-only flags are used, so this should run
#   as-is on the FreeBSD NAS this is deployed to.
#
# OUTPUT / EXIT STATUS
#   Prints the same report as scanWordPressBackdoors (see its own docs via
#   `perldoc scanWordPressBackdoors`, or its internal POD block). Exits
#   non-zero only if $ROOT is missing/not a directory; a clean scan with no
#   findings still exits 0.
#
# See ../testing/ for real-world backdoor samples used to regression-test
# both this wrapper and scanWordPressBackdoors' detection logic.

set -eu

SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
SCANNER="$SCRIPT_DIR/scanWordPressBackdoors"
[ -x "$SCANNER" ] || SCANNER="perl $SCANNER"

WHITELIST_ARG=""
EXTRA_ARGS=""
SHOW_HELP=0
ROOT=""
for arg in "$@"; do
  case "$arg" in
    --help)
      SHOW_HELP=1
      ;;
    --whitelist=*)
      WHITELIST_ARG=$arg
      ;;
    --*)
      # Any other scanner flag (--max-bytes=N, --threshold=N,
      # --max-per-site=N, and anything added later) - forwarded verbatim.
      # Getopt::Long on the scanner side rejects anything it doesn't
      # recognize, so a typo here still errors instead of being ignored.
      EXTRA_ARGS="$EXTRA_ARGS $arg"
      ;;
    *)
      if [ -z "$ROOT" ]; then
        ROOT=$arg
      else
        echo "Usage: $0 [options] /full/path/to/clients" >&2
        exit 1
      fi
      ;;
  esac
done

if [ "$SHOW_HELP" -eq 1 ]; then
  exec $SCANNER --help
fi

[ -n "$ROOT" ] || { echo "Usage: $0 [options] /full/path/to/clients" >&2; exit 1; }
[ -d "$ROOT" ] || { echo "Not a directory: $ROOT" >&2; exit 1; }

# Broad set of obfuscation/execution signals - a file must match at least
# one to be scored. Keep this in rough sync with the detections in
# scanWordPressBackdoors itself.
GREP_PATTERN='eval|assert|create_function|shell_exec|system\(|exec\(|passthru|proc_open|popen\(|curl_exec|curl_init|base64_decode|gzinflate|gzuncompress|str_rot13|hex2bin|unserialize|goto[[:space:]]|chr\(|pack\('

ALL=$(mktemp)
ALWAYS=$(mktemp)
GREPPED=$(mktemp)
trap 'rm -f "$ALL" "$ALWAYS" "$GREPPED"' EXIT

find "$ROOT" \
  -type d \( -name .git -o -name .svn -o -name node_modules -o -name vendor \) -prune -o \
  -type f \( -iname '*.php' -o -iname '*.phtml' -o -iname '*.phps' \) -print0 > "$ALL"

# Always-include: wp-config.php, and anything sitting where executable php
# has no business being (uploads/, cache/) - kept regardless of content.
tr '\0' '\n' < "$ALL" | grep -iE '/(wp-config\.php|wp-content/(uploads|cache)/)' > "$ALWAYS" || true

# Content-based candidates. -a forces text mode so grep doesn't skip files
# that look "binary" due to embedded base64/hex blobs.
xargs -0 grep -a -l -E "$GREP_PATTERN" < "$ALL" > "$GREPPED" 2>/dev/null || true

cat "$ALWAYS" "$GREPPED" | sort -u | tr '\n' '\0' | $SCANNER ${WHITELIST_ARG:+"$WHITELIST_ARG"} $EXTRA_ARGS - "$ROOT"
