#!/usr/bin/env bash
# elf-static.sh - what an ELF *can* touch, without running it.
#
#   elf-static.sh ./binary [> STATIC.md]
#
# Answers, from the file alone:
#   * is it dynamic or static, PIE, stripped?
#   * which loader and which shared libraries does it need (DT_NEEDED, RUNPATH)?
#   * which libc entry points does it import - grouped into file / process /
#     network / exec capability buckets?
#   * which path-like strings are baked into it (candidate files it opens)?
#   * which hardening flags are on?
set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BIN="${1:?usage: elf-static.sh /path/to/binary}"
[[ -r "$BIN" ]] || { echo "cannot read $BIN" >&2; exit 1; }
ABS="$(readlink -f "$BIN")"

have() { command -v "$1" >/dev/null 2>&1; }

# binutils is not guaranteed on a locked-down RHEL 9.6 host, and installing it
# needs elfutils-debuginfod-client. pyelftools (one noarch RPM, no awkward deps)
# covers everything this report needs, so fall back to it rather than failing.
PYFALLBACK="$HERE/elf_static_py.py"
PY="${PYTHON:-/usr/bin/python3}"
have_pyelftools() {
  [[ -x "$PYFALLBACK" ]] && "$PY" -c 'import elftools' >/dev/null 2>&1
}
DEGRADED=0
if ! have readelf && have_pyelftools; then DEGRADED=1; fi

# readelf/strings wrappers: use the real tool when present, pyelftools otherwise
re_header()    { if have readelf; then readelf -h "$1"; else "$PY" "$PYFALLBACK" header "$1"; fi; }
re_dynamic()   {
  if have readelf; then
    readelf -l "$1" 2>/dev/null | grep -A1 INTERP | sed 's/^ *//'
    readelf -d "$1" 2>/dev/null | grep -E 'NEEDED|RUNPATH|RPATH|SONAME' \
      || echo "(no dynamic section - static binary?)"
  else
    "$PY" "$PYFALLBACK" dynamic "$1"
  fi
}
re_undef()     {
  if have readelf; then
    readelf -W --dyn-syms "$1" 2>/dev/null | awk '$7=="UND"{print $8}' | sed 's/@.*//' | sort -u
  else
    "$PY" "$PYFALLBACK" undef-syms "$1" 2>/dev/null
  fi
}
do_strings()   { if have strings; then strings -a -n 6 "$1" 2>/dev/null; else "$PY" "$PYFALLBACK" strings "$1" 2>/dev/null; fi; }

echo "# Static ELF report: $(basename "$ABS")"
echo
echo '```'
echo "path   : $ABS"
echo "size   : $(stat -c %s "$ABS") bytes"
echo "sha256 : $(sha256sum "$ABS" | cut -d' ' -f1)"
have file && echo "file   : $(file -b "$ABS")"
if have rpm; then
  owner="$(rpm -qf "$ABS" 2>/dev/null || true)"
  echo "rpm    : ${owner:-not owned by any package}"
  if [[ -n "$owner" && "$owner" != *"not owned"* ]]; then
    echo "verify : $(rpm -V "$owner" 2>/dev/null | grep -F "$ABS" || echo 'unmodified vs package')"
  fi
fi
echo '```'

echo
if [[ $DEGRADED -eq 1 ]]; then
  echo "> NOTE: binutils is not installed here, so this report was produced with"
  echo "> pyelftools instead of readelf. Everything below is equivalent except"
  echo "> section 4, which needs objdump and is skipped."
  echo
fi

echo "## 1. ELF header"
echo '```'
re_header "$ABS"
echo '```'

echo
echo "## 2. Interpreter, RPATH/RUNPATH and required libraries"
echo '```'
re_dynamic "$ABS" || true
echo '```'
echo
echo "Resolved at load time (\`ldd\`):"
echo '```'
ldd "$ABS" 2>&1 || true
echo '```'
echo "> Libraries opened later with \`dlopen()\` do NOT appear above."
echo "> Only a runtime trace (elf-trace.sh) reveals those."

echo
echo "## 3. Imported symbols by capability"
SYMS="$(re_undef "$ABS")"
if [[ -z "$SYMS" ]]; then
  echo "_no dynamic symbol table (static or stripped-dynamic binary); fall back to section 5_"
else
  bucket() {
    local title="$1" pat="$2" hits
    hits="$(printf '%s\n' "$SYMS" | grep -E "$pat" || true)"
    printf '\n**%s**\n\n' "$title"
    if [[ -z "$hits" ]]; then echo '_none_'; else echo '```'; echo "$hits" | tr '\n' ' ' | fold -s -w 100; echo; echo '```'; fi
  }
  bucket "File access"      '^(open|open64|openat|creat|fopen|fopen64|freopen|read|pread|fread|write|pwrite|fwrite|close|fclose|stat|stat64|lstat|fstat|__xstat|__lxstat|__fxstat|access|faccessat|unlink|remove|rename|mkdir|rmdir|chmod|chown|truncate|ftruncate|lseek|mmap|mmap64|readlink|realpath|opendir|readdir|scandir|glob|tmpfile|mkstemp|mkdtemp|fcntl|flock|fsync|sendfile|statx|utimensat|link|symlink)'
  bucket "Process / exec"   '^(fork|vfork|clone|posix_spawn|execl|execle|execlp|execv|execve|execvp|execvpe|fexecve|system|popen|pclose|wait|waitpid|wait4|kill|raise|getpid|getppid|setuid|setgid|seteuid|setegid|setsid|daemon|prctl|ptrace|personality)'
  bucket "Dynamic loading" '^(dlopen|dlsym|dlclose|dlerror|dladdr|dlmopen)'
  bucket "Network"          '^(socket|socketpair|connect|bind|listen|accept|accept4|send|sendto|sendmsg|recv|recvfrom|recvmsg|getaddrinfo|gethostbyname|getnameinfo|inet_addr|inet_ntop|inet_pton|setsockopt|shutdown|res_)'
  bucket "Users / identity" '^(getpwnam|getpwuid|getgrnam|getgrgid|getlogin|getenv|secure_getenv|crypt|pam_)'
  bucket "Time / random"    '^(time|clock_gettime|gettimeofday|rand|srand|random|getrandom|arc4random)'
  echo
  if have readelf; then
    echo "Full undefined-symbol list: \`readelf -W --dyn-syms '$ABS' | awk '\$7==\"UND\"'\`"
  else
    echo "Full undefined-symbol list: \`$PY $PYFALLBACK undef-syms '$ABS'\`"
  fi
fi

echo
echo "## 4. Direct syscall sites (binaries that bypass libc)"
if have objdump; then
  N="$(objdump -d "$ABS" 2>/dev/null | grep -cE '\ssyscall\s*$|\sint\s+\$0x80' || true)"
  echo "\`syscall\` instructions found: ${N:-0}"
  [[ "${N:-0}" -gt 0 ]] && echo "> A high count on a small binary suggests hand-written syscalls (Go, asm, packer). Trace it."
else
  echo "_objdump is not installed (binutils), so this check was skipped._"
  echo "_Install binutils, or treat a small binary with few imports as suspicious on principle._"
fi

echo
echo "## 5. Path-like strings embedded in the binary"
echo "Candidate files/dirs/commands. Not proof of access - confirm with a trace."
echo
echo '```'
do_strings "$ABS" \
  | grep -E '^(/[A-Za-z0-9._%+-]+){1,}/?$|^[A-Za-z0-9._-]+\.(conf|cfg|ini|json|ya?ml|log|so|so\.[0-9]+|key|pem|crt|db|sql|sh|py)$' \
  | sort -u | head -80
echo '```'
echo
echo "Environment variables it looks at:"
echo '```'
do_strings "$ABS" | grep -E '^[A-Z][A-Z0-9_]{2,}$' | sort -u | head -40
echo '```'

echo
echo "## 6. Hardening / packing indicators"
echo '```'
if have readelf; then
  hdr="$(readelf -h "$ABS")"
  type="$(echo "$hdr" | awk '/Type:/{print $2}')"
  echo "PIE        : $([[ "$type" == DYN ]] && echo yes || echo "no ($type)")"
  echo "NX (stack) : $(readelf -lW "$ABS" 2>/dev/null | awk '/GNU_STACK/{print ($0 ~ /RWE/) ? "NO - executable stack" : "yes"}' | head -1)"
  echo "RELRO      : $(readelf -lW "$ABS" 2>/dev/null | grep -q GNU_RELRO && { readelf -dW "$ABS" | grep -q BIND_NOW && echo "full" || echo "partial"; } || echo "none")"
  echo "Canary     : $(readelf -W --dyn-syms "$ABS" 2>/dev/null | grep -q __stack_chk && echo yes || echo "not visible")"
  echo "Stripped   : $(file -b "$ABS" | grep -q 'not stripped' && echo no || echo yes)"
  echo "Sections   : $(readelf -S "$ABS" 2>/dev/null | grep -c '^\s*\[') (very low count + high entropy = packed)"
else
  "$PY" "$PYFALLBACK" hardening "$ABS"
fi
echo "setuid/gid : $(find "$ABS" -perm /6000 -printf 'YES %M\n' 2>/dev/null || echo no)"
echo "Capabilities: $(getcap "$ABS" 2>/dev/null || echo none)"
echo '```'

echo
echo "## 7. Next step"
echo "\`\`\`"
echo "tools/elf-trace.sh -- $ABS            # see what it actually touches"
echo "\`\`\`"
