Last active
May 14, 2026 06:46
-
-
Save BlueSkyXN/4777513302311bfac05a65a710f4e25f to your computer and use it in GitHub Desktop.
nginx-cve42945-audit.sh
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/sh | |
| # nginx-cve42945-audit-v7.sh | |
| # Read-only NGINX / BaoTa / SafeLine audit collector for CVE-2026-42945-style rewrite risk patterns. | |
| # It does NOT modify config, reload/restart services, or send HTTP requests. | |
| # | |
| # Optional environment variables: | |
| # OUT_BASE=/root # output root directory; fallback to /tmp if not writable | |
| # COLLECT_FULL_CONFIG=1 # include sanitized full scanned config copies | |
| # COLLECT_RAW_CONFIG=1 # include raw scanned config copies; not recommended | |
| # LOG_SCAN=0 # disable nginx log suspicious excerpt scanning | |
| # COLLECT_FULL_LOGS=1 # collect full nginx error logs only; access logs are never fully copied by default | |
| # MAX_CONF_SIZE_KB=5120 # skip config-like files larger than this | |
| # LOG_MAX_LINES=300 # max suspicious log lines per log file | |
| # SINGLE_LOG=1 # write one combined .log artifact; default enabled | |
| # PACKAGE_TGZ=1 # also build .tgz package; default disabled | |
| # KEEP_WORK_DIR=1 # keep intermediate output directory; default disabled when using single log | |
| # INCLUDE_NGINX_T_IN_LOG=1 # include sanitized nginx -T dumps in the single log; default disabled | |
| # SAFELINE_NGINX_ROOT=/data/safeline/resources/nginx | |
| set -u | |
| SCRIPT_VERSION="v7-single-log-nginx-baota-safeline" | |
| TS="$(date +%Y%m%d-%H%M%S 2>/dev/null || echo unknown-time)" | |
| HOST="$(hostname 2>/dev/null || uname -n 2>/dev/null || echo unknown-host)" | |
| HOST_SAFE="$(printf '%s' "$HOST" | sed 's/[^A-Za-z0-9_.-]/_/g')" | |
| OUT_BASE="${OUT_BASE:-/root}" | |
| if [ ! -d "$OUT_BASE" ] || [ ! -w "$OUT_BASE" ]; then | |
| OUT_BASE="/tmp" | |
| fi | |
| OUT_DIR="$OUT_BASE/nginx-cve42945-audit-$HOST_SAFE-$TS" | |
| mkdir -p "$OUT_DIR" 2>/dev/null || { | |
| OUT_DIR="/tmp/nginx-cve42945-audit-$HOST_SAFE-$TS" | |
| mkdir -p "$OUT_DIR" || exit 1 | |
| } | |
| REPORT="$OUT_DIR/console-report.txt" | |
| SUMMARY="$OUT_DIR/final-summary.txt" | |
| SINGLE_LOG="${SINGLE_LOG:-1}" | |
| PACKAGE_TGZ="${PACKAGE_TGZ:-0}" | |
| KEEP_WORK_DIR="${KEEP_WORK_DIR:-0}" | |
| INCLUDE_NGINX_T_IN_LOG="${INCLUDE_NGINX_T_IN_LOG:-0}" | |
| SINGLE_LOG_FILE="${SINGLE_LOG_FILE:-$OUT_BASE/nginx-cve42945-audit-$HOST_SAFE-$TS.log}" | |
| SAFELINE_NGINX_ROOT="${SAFELINE_NGINX_ROOT:-/data/safeline/resources/nginx}" | |
| mkdir -p \ | |
| "$OUT_DIR/system" \ | |
| "$OUT_DIR/process" \ | |
| "$OUT_DIR/nginx" \ | |
| "$OUT_DIR/nginx/nginxT" \ | |
| "$OUT_DIR/nginx/config-context" \ | |
| "$OUT_DIR/nginx/config-full-sanitized" \ | |
| "$OUT_DIR/nginx/config-full-raw" \ | |
| "$OUT_DIR/baota" \ | |
| "$OUT_DIR/safeline" \ | |
| "$OUT_DIR/logs" \ | |
| "$OUT_DIR/tmp" | |
| say() { | |
| printf '%s\n' "$*" | tee -a "$REPORT" | |
| } | |
| run_capture() { | |
| # usage: run_capture output_file command args... | |
| out="$1" | |
| shift | |
| { | |
| printf '### command:' | |
| for a in "$@"; do printf ' %s' "$a"; done | |
| printf '\n' | |
| "$@" | |
| rc=$? | |
| printf '\n### exit_code: %s\n' "$rc" | |
| } > "$out" 2>&1 | |
| } | |
| append_unique() { | |
| file="$1" | |
| val="$2" | |
| [ -n "$val" ] || return 0 | |
| grep -Fx -- "$val" "$file" >/dev/null 2>&1 || printf '%s\n' "$val" >> "$file" | |
| } | |
| sanitize_stream() { | |
| # Conservative text sanitizer. Keeps rewrite/$1/?/location structure intact. | |
| sed -E \ | |
| -e 's/(Authorization:[[:space:]]*)[^[:space:]]+/\1<REDACTED>/Ig' \ | |
| -e 's/(Bearer[[:space:]]+)[A-Za-z0-9._~+\/-=]+/\1<REDACTED>/Ig' \ | |
| -e 's/([Pp]ass(word)?|[Ss]ecret|[Tt]oken|[Aa]pi[_-]?[Kk]ey|[Aa]ccess[_-]?[Kk]ey|[Ss]ignature|[Cc]redential)([[:space:]]*[:=][[:space:]]*)[^;&[:space:]"'"'"']+/\1\3<REDACTED>/g' \ | |
| -e 's/(auth_basic_user_file[[:space:]]+)[^;]+;/\1<REDACTED>;/Ig' \ | |
| -e 's#(/[^[:space:];]*\.(key|pem|p12|pfx))#<REDACTED_PRIVATE_KEY_PATH>#Ig' | |
| } | |
| size_kb() { | |
| f="$1" | |
| if command -v stat >/dev/null 2>&1; then | |
| # GNU stat on most Linux systems | |
| bytes="$(stat -c '%s' "$f" 2>/dev/null || wc -c < "$f" 2>/dev/null || echo 0)" | |
| else | |
| bytes="$(wc -c < "$f" 2>/dev/null || echo 0)" | |
| fi | |
| echo $(( (bytes + 1023) / 1024 )) | |
| } | |
| is_text_file() { | |
| f="$1" | |
| [ -r "$f" ] || return 1 | |
| # GNU grep -Iq handles binary. If unavailable, this usually still works on Linux. | |
| grep -Iq . "$f" 2>/dev/null | |
| } | |
| # ------------------------------- | |
| # 0. Header | |
| # ------------------------------- | |
| say "============================================================" | |
| say "NGINX CVE-2026-42945 rewrite-risk audit collector $SCRIPT_VERSION" | |
| say "host: $HOST" | |
| say "time: $(date 2>/dev/null || echo unknown)" | |
| say "output_dir: $OUT_DIR" | |
| say "mode: read-only; no config change; no reload/restart; no HTTP requests" | |
| say "artifact_mode: single_log=$SINGLE_LOG package_tgz=$PACKAGE_TGZ keep_work_dir=$KEEP_WORK_DIR" | |
| if [ "$SINGLE_LOG" = "1" ]; then | |
| say "single_log_file: $SINGLE_LOG_FILE" | |
| fi | |
| say "============================================================" | |
| # ------------------------------- | |
| # 1. System and software inventory | |
| # ------------------------------- | |
| say "" | |
| say "[1/8] Collecting system and software information" | |
| { | |
| echo "===== hostname =====" | |
| hostname 2>/dev/null || true | |
| echo | |
| echo "===== date =====" | |
| date 2>/dev/null || true | |
| echo | |
| echo "===== uname =====" | |
| uname -a 2>/dev/null || true | |
| echo | |
| echo "===== architecture =====" | |
| uname -m 2>/dev/null || true | |
| getconf LONG_BIT 2>/dev/null || true | |
| echo | |
| echo "===== os-release =====" | |
| cat /etc/os-release 2>/dev/null || true | |
| echo | |
| echo "===== redhat-release/debian-version/alpine-release =====" | |
| cat /etc/redhat-release 2>/dev/null || true | |
| cat /etc/debian_version 2>/dev/null || true | |
| cat /etc/alpine-release 2>/dev/null || true | |
| echo | |
| echo "===== kernel ASLR =====" | |
| printf '%s' "/proc/sys/kernel/randomize_va_space=" | |
| cat /proc/sys/kernel/randomize_va_space 2>/dev/null || true | |
| echo | |
| echo "===== core pattern =====" | |
| cat /proc/sys/kernel/core_pattern 2>/dev/null || true | |
| echo | |
| echo "===== ulimit -c =====" | |
| (ulimit -c) 2>/dev/null || true | |
| echo | |
| echo "===== package inventory: nginx/openresty/tengine =====" | |
| if command -v dpkg >/dev/null 2>&1; then | |
| dpkg -l 2>/dev/null | grep -Ei 'nginx|openresty|tengine|libnginx' || true | |
| fi | |
| if command -v rpm >/dev/null 2>&1; then | |
| rpm -qa 2>/dev/null | grep -Ei 'nginx|openresty|tengine' || true | |
| fi | |
| if command -v apk >/dev/null 2>&1; then | |
| apk info -v 2>/dev/null | grep -Ei 'nginx|openresty|tengine' || true | |
| fi | |
| } > "$OUT_DIR/system/system-info.txt" 2>&1 | |
| # ------------------------------- | |
| # 2. Process and network exposure | |
| # ------------------------------- | |
| say "[2/8] Collecting NGINX process and listening-port information" | |
| { | |
| echo "===== nginx/openresty/tengine processes =====" | |
| ps -eo pid,ppid,user,etime,args 2>/dev/null | grep -Ei 'nginx: master|nginx: worker|openresty|tengine' | grep -v grep || true | |
| echo | |
| echo "===== systemctl status snippets =====" | |
| if command -v systemctl >/dev/null 2>&1; then | |
| systemctl status nginx --no-pager 2>/dev/null | sed -n '1,80p' || true | |
| systemctl status openresty --no-pager 2>/dev/null | sed -n '1,80p' || true | |
| systemctl status tengine --no-pager 2>/dev/null | sed -n '1,80p' || true | |
| fi | |
| echo | |
| echo "===== init scripts =====" | |
| ls -l /etc/init.d/nginx /etc/init.d/openresty /etc/init.d/tengine 2>/dev/null || true | |
| echo | |
| echo "===== listening ports =====" | |
| if command -v ss >/dev/null 2>&1; then | |
| ss -lntp 2>/dev/null || ss -lnt 2>/dev/null || true | |
| elif command -v netstat >/dev/null 2>&1; then | |
| netstat -lntp 2>/dev/null || netstat -lnt 2>/dev/null || true | |
| fi | |
| echo | |
| echo "===== ip addresses =====" | |
| ip addr 2>/dev/null | sed -n '1,160p' || ifconfig 2>/dev/null || true | |
| } > "$OUT_DIR/process/process-network.txt" 2>&1 | |
| # ------------------------------- | |
| # 3. Locate NGINX-like binaries and active config hints | |
| # ------------------------------- | |
| say "[3/8] Locating nginx/openresty/tengine binaries and active config hints" | |
| BIN_LIST="$OUT_DIR/nginx/bin-candidates.txt" | |
| HINT_LIST="$OUT_DIR/nginx/active-config-hints.txt" | |
| : > "$BIN_LIST" | |
| : > "$HINT_LIST" | |
| # Running master process hints. | |
| ps -eo pid,args 2>/dev/null | grep -E 'nginx: master|openresty: master|tengine' | grep -v grep > "$OUT_DIR/process/master-processes.txt" 2>/dev/null || true | |
| while IFS= read -r line; do | |
| pid="$(printf '%s\n' "$line" | awk '{print $1}')" | |
| [ -n "$pid" ] || continue | |
| exe="" | |
| [ -e "/proc/$pid/exe" ] && exe="$(readlink -f "/proc/$pid/exe" 2>/dev/null || true)" | |
| [ -n "$exe" ] && append_unique "$BIN_LIST" "$exe" | |
| cmd="" | |
| [ -r "/proc/$pid/cmdline" ] && cmd="$(tr '\000' ' ' < "/proc/$pid/cmdline" 2>/dev/null || true)" | |
| [ -n "$cmd" ] || cmd="$line" | |
| prefix="" | |
| conf="" | |
| prev="" | |
| # shell word splitting is acceptable for typical nginx cmdline flags. | |
| for arg in $cmd; do | |
| if [ "$prev" = "-c" ]; then conf="$arg"; prev=""; continue; fi | |
| if [ "$prev" = "-p" ]; then prefix="$arg"; prev=""; continue; fi | |
| case "$arg" in | |
| -c) prev="-c" ;; | |
| -p) prev="-p" ;; | |
| -c*) conf="$(printf '%s' "$arg" | sed 's/^-c//')" ;; | |
| -p*) prefix="$(printf '%s' "$arg" | sed 's/^-p//')" ;; | |
| esac | |
| done | |
| printf 'pid=%s|bin=%s|prefix=%s|conf=%s|cmd=%s\n' "$pid" "$exe" "$prefix" "$conf" "$cmd" >> "$HINT_LIST" | |
| done < "$OUT_DIR/process/master-processes.txt" | |
| # Common locations and PATH. | |
| for b in \ | |
| /www/server/nginx/sbin/nginx \ | |
| /usr/sbin/nginx \ | |
| /sbin/nginx \ | |
| /usr/local/nginx/sbin/nginx \ | |
| /usr/local/openresty/nginx/sbin/nginx \ | |
| /openresty/nginx/sbin/nginx \ | |
| /usr/local/tengine/sbin/nginx \ | |
| /opt/nginx/sbin/nginx \ | |
| /opt/openresty/nginx/sbin/nginx | |
| do | |
| [ -x "$b" ] && append_unique "$BIN_LIST" "$(readlink -f "$b" 2>/dev/null || echo "$b")" | |
| done | |
| for c in nginx openresty tengine; do | |
| if command -v "$c" >/dev/null 2>&1; then | |
| b="$(command -v "$c" 2>/dev/null || true)" | |
| [ -n "$b" ] && append_unique "$BIN_LIST" "$(readlink -f "$b" 2>/dev/null || echo "$b")" | |
| fi | |
| done | |
| { | |
| echo "===== binary candidates =====" | |
| cat "$BIN_LIST" 2>/dev/null || true | |
| echo | |
| echo "===== active config hints =====" | |
| cat "$HINT_LIST" 2>/dev/null || true | |
| } > "$OUT_DIR/nginx/detection-summary.txt" | |
| # ------------------------------- | |
| # 4. NGINX version/build and nginx -T dumps | |
| # ------------------------------- | |
| say "[4/8] Collecting nginx -v/-V and nginx -T outputs" | |
| : > "$OUT_DIR/nginx/version-all.txt" | |
| : > "$OUT_DIR/nginx/nginxT-files.txt" | |
| T_INDEX=0 | |
| while IFS= read -r bin; do | |
| [ -x "$bin" ] || continue | |
| safe_bin="$(printf '%s' "$bin" | sed 's#[/ ]#_#g; s#[^A-Za-z0-9_.-]#_#g')" | |
| { | |
| echo "===== binary: $bin =====" | |
| echo "--- -v ---" | |
| "$bin" -v 2>&1 || true | |
| echo | |
| echo "--- -V ---" | |
| "$bin" -V 2>&1 || true | |
| echo | |
| } >> "$OUT_DIR/nginx/version-all.txt" 2>&1 | |
| run_one_T() { | |
| _tag="$1" | |
| shift | |
| T_INDEX=$((T_INDEX + 1)) | |
| out="$OUT_DIR/nginx/nginxT/nginxT-${T_INDEX}-${safe_bin}-${_tag}.txt" | |
| { | |
| echo "===== binary: $bin =====" | |
| echo "===== args: $* =====" | |
| "$bin" "$@" -T | |
| rc=$? | |
| echo "===== exit_code: $rc =====" | |
| } > "$out" 2>&1 | |
| printf '%s\n' "$out" >> "$OUT_DIR/nginx/nginxT-files.txt" | |
| } | |
| # default config | |
| run_one_T "default" >/dev/null 2>&1 || true | |
| # active hints for this binary or generic hints | |
| while IFS= read -r hint; do | |
| hbin="$(printf '%s' "$hint" | sed -n 's/^.*|bin=\([^|]*\)|.*$/\1/p')" | |
| hpfx="$(printf '%s' "$hint" | sed -n 's/^.*|prefix=\([^|]*\)|conf=.*$/\1/p')" | |
| hconf="$(printf '%s' "$hint" | sed -n 's/^.*|conf=\([^|]*\)|cmd=.*$/\1/p')" | |
| [ -z "$hbin" ] || [ "$hbin" = "$bin" ] || continue | |
| if [ -n "$hpfx" ] && [ -n "$hconf" ]; then | |
| run_one_T "active-p-c" -p "$hpfx" -c "$hconf" >/dev/null 2>&1 || true | |
| elif [ -n "$hconf" ]; then | |
| run_one_T "active-c" -c "$hconf" >/dev/null 2>&1 || true | |
| elif [ -n "$hpfx" ]; then | |
| run_one_T "active-p" -p "$hpfx" >/dev/null 2>&1 || true | |
| fi | |
| done < "$HINT_LIST" | |
| done < "$BIN_LIST" | |
| # Extract config files from nginx -T output. | |
| LOADED_CONF_LIST="$OUT_DIR/nginx/loaded-config-files-from-nginxT.txt" | |
| : > "$LOADED_CONF_LIST" | |
| while IFS= read -r tf; do | |
| [ -r "$tf" ] || continue | |
| sed -n 's/^# configuration file \(.*\):$/\1/p' "$tf" 2>/dev/null | while IFS= read -r p; do | |
| [ -n "$p" ] && append_unique "$LOADED_CONF_LIST" "$p" | |
| done | |
| done < "$OUT_DIR/nginx/nginxT-files.txt" | |
| # ------------------------------- | |
| # 5. BaoTa/SafeLine and config file discovery | |
| # ------------------------------- | |
| say "[5/8] Discovering BaoTa/SafeLine/multi-site and NGINX-related config files" | |
| CONF_ROOTS="$OUT_DIR/nginx/config-roots.txt" | |
| CONF_ALL="$OUT_DIR/nginx/conf-files-all.txt" | |
| CONF_SCAN="$OUT_DIR/nginx/conf-files-scanned.txt" | |
| CONF_SKIP="$OUT_DIR/nginx/conf-files-skipped.txt" | |
| : > "$CONF_ROOTS" | |
| : > "$CONF_ALL" | |
| : > "$CONF_SCAN" | |
| : > "$CONF_SKIP" | |
| for d in \ | |
| /www/server/nginx/conf \ | |
| /www/server/panel/vhost \ | |
| /etc/nginx \ | |
| /usr/local/nginx/conf \ | |
| /usr/local/openresty/nginx/conf \ | |
| /openresty/nginx/conf \ | |
| /usr/local/tengine/conf \ | |
| /opt/nginx/conf \ | |
| /opt/openresty/nginx/conf \ | |
| "$SAFELINE_NGINX_ROOT" | |
| do | |
| [ -d "$d" ] && append_unique "$CONF_ROOTS" "$d" | |
| done | |
| # Also include directories of files reported by nginx -T. | |
| while IFS= read -r f; do | |
| [ -n "$f" ] || continue | |
| if [ -f "$f" ]; then | |
| append_unique "$CONF_ROOTS" "$(dirname "$f")" | |
| append_unique "$CONF_ALL" "$f" | |
| fi | |
| done < "$LOADED_CONF_LIST" | |
| # Find config-like files under roots. Intentionally broad for BaoTa and SafeLine multi-site layouts. | |
| while IFS= read -r d; do | |
| [ -d "$d" ] || continue | |
| find "$d" -maxdepth 8 -type f \ | |
| ! -name '*.log' ! -name '*.pid' ! -name '*.sock' \ | |
| ! -name '*.key' ! -name '*.pem' ! -name '*.p12' ! -name '*.pfx' ! -name '*.jks' ! -name '*.crt' ! -name '*.csr' \ | |
| 2>/dev/null | while IFS= read -r f; do | |
| # Keep nginx-related files. In BaoTa/SafeLine dirs, extension can vary, so scan text files too. | |
| case "$f" in | |
| *.conf|*.inc|*.include|*.rules|*.rule|*.vhost|*/vhost/*|"$SAFELINE_NGINX_ROOT"/*|*/nginx/conf/*|*/openresty/nginx/conf/*|*/tengine/conf/*) | |
| append_unique "$CONF_ALL" "$f" | |
| ;; | |
| esac | |
| done | |
| done < "$CONF_ROOTS" | |
| MAX_CONF_SIZE_KB="${MAX_CONF_SIZE_KB:-5120}" | |
| while IFS= read -r f; do | |
| [ -f "$f" ] || { printf '%s | missing\n' "$f" >> "$CONF_SKIP"; continue; } | |
| kb="$(size_kb "$f")" | |
| if [ "$kb" -gt "$MAX_CONF_SIZE_KB" ]; then | |
| printf '%s | skipped_large_%sKB\n' "$f" "$kb" >> "$CONF_SKIP" | |
| continue | |
| fi | |
| if ! is_text_file "$f"; then | |
| printf '%s | skipped_non_text_or_unreadable\n' "$f" >> "$CONF_SKIP" | |
| continue | |
| fi | |
| append_unique "$CONF_SCAN" "$f" | |
| done < "$CONF_ALL" | |
| # BaoTa site inventory. | |
| { | |
| echo "===== baota path check =====" | |
| ls -ld /www/server/panel /www/server/panel/vhost /www/server/nginx 2>/dev/null || true | |
| echo | |
| echo "===== baota nginx version files if present =====" | |
| cat /www/server/nginx/version.pl 2>/dev/null || true | |
| cat /www/server/nginx/src/version 2>/dev/null || true | |
| echo | |
| echo "===== baota vhost tree summary =====" | |
| find /www/server/panel/vhost -maxdepth 4 -type f 2>/dev/null | sed -n '1,1000p' || true | |
| echo | |
| echo "===== baota site configs =====" | |
| for f in /www/server/panel/vhost/nginx/*.conf; do | |
| [ -f "$f" ] || continue | |
| site="$(basename "$f" .conf)" | |
| loaded="no" | |
| grep -Fx -- "$f" "$LOADED_CONF_LIST" >/dev/null 2>&1 && loaded="yes" | |
| listen="$(grep -nE '^[[:space:]]*listen[[:space:]]+' "$f" 2>/dev/null | head -5 | sanitize_stream | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g')" | |
| server_name="$(grep -nE '^[[:space:]]*server_name[[:space:]]+' "$f" 2>/dev/null | head -5 | sanitize_stream | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g')" | |
| rewrite_file="/www/server/panel/vhost/rewrite/${site}.conf" | |
| [ -f "$rewrite_file" ] || rewrite_file="" | |
| printf 'site=%s|loaded_by_nginx_T=%s|conf=%s|rewrite=%s|listen=%s|server_name=%s\n' "$site" "$loaded" "$f" "$rewrite_file" "$listen" "$server_name" | |
| done | |
| } > "$OUT_DIR/baota/baota-inventory.txt" 2>&1 | |
| # SafeLine mapped NGINX config inventory. | |
| { | |
| echo "===== safeline path check =====" | |
| echo "safeline_nginx_root=$SAFELINE_NGINX_ROOT" | |
| ls -ld "$SAFELINE_NGINX_ROOT" 2>/dev/null || true | |
| echo | |
| echo "===== safeline nginx tree summary =====" | |
| find "$SAFELINE_NGINX_ROOT" -maxdepth 6 -type f 2>/dev/null | sed -n '1,1000p' || true | |
| echo | |
| echo "===== safeline files selected for scan =====" | |
| grep -F -- "$SAFELINE_NGINX_ROOT" "$CONF_SCAN" 2>/dev/null | sed -n '1,1000p' || true | |
| echo | |
| echo "===== safeline core nginx directives =====" | |
| while IFS= read -r f; do | |
| [ -r "$f" ] || continue | |
| case "$f" in | |
| "$SAFELINE_NGINX_ROOT"/*) | |
| grep -nE '^[[:space:]]*(server[[:space:]]*\{|listen[[:space:]]+|server_name[[:space:]]+|location[[:space:]]|rewrite[[:space:]]+|set[[:space:]]+|if[[:space:]]*\(|try_files[[:space:]]+|proxy_pass[[:space:]]+|include[[:space:]]+)' "$f" 2>/dev/null | sanitize_stream | sed "s#^#$f:#" | |
| ;; | |
| esac | |
| done < "$CONF_SCAN" | |
| } > "$OUT_DIR/safeline/safeline-inventory.txt" 2>&1 | |
| # Generic website/server block inventory from scanned config files. | |
| { | |
| echo "===== scanned config files =====" | |
| cat "$CONF_SCAN" 2>/dev/null || true | |
| echo | |
| echo "===== core nginx directives: server/listen/server_name/location/rewrite/set/if/try_files/proxy_pass/include =====" | |
| while IFS= read -r f; do | |
| [ -r "$f" ] || continue | |
| grep -nE '^[[:space:]]*(server[[:space:]]*\{|listen[[:space:]]+|server_name[[:space:]]+|location[[:space:]]|rewrite[[:space:]]+|set[[:space:]]+|if[[:space:]]*\(|try_files[[:space:]]+|proxy_pass[[:space:]]+|include[[:space:]]+)' "$f" 2>/dev/null | sanitize_stream | sed "s#^#$f:#" | |
| done < "$CONF_SCAN" | |
| } > "$OUT_DIR/nginx/core-config-lines.txt" 2>&1 | |
| # ------------------------------- | |
| # 6. Risk scan: grep fallback + Python block heuristic if available | |
| # ------------------------------- | |
| say "[6/8] Scanning config files for rewrite + ? + numbered-capture risk patterns" | |
| RISK_DIRECT="$OUT_DIR/nginx/risk-direct-high.txt" | |
| RISK_REWRITE_Q="$OUT_DIR/nginx/rewrite-with-question.txt" | |
| RISK_NUMBERED="$OUT_DIR/nginx/numbered-captures.txt" | |
| RISK_BLOCK="$OUT_DIR/nginx/risk-block-check.txt" | |
| RISK_CONTEXT_DIR="$OUT_DIR/nginx/config-context" | |
| : > "$RISK_DIRECT" | |
| : > "$RISK_REWRITE_Q" | |
| : > "$RISK_NUMBERED" | |
| : > "$RISK_BLOCK" | |
| while IFS= read -r f; do | |
| [ -r "$f" ] || continue | |
| grep -nE '^[[:space:]]*rewrite[[:space:]].*\?.*\$[1-9][0-9]*|^[[:space:]]*rewrite[[:space:]].*\$[1-9][0-9]*.*\?' "$f" 2>/dev/null | sanitize_stream | sed "s#^#$f:#" >> "$RISK_DIRECT" || true | |
| grep -nE '^[[:space:]]*rewrite[[:space:]].*\?' "$f" 2>/dev/null | sanitize_stream | sed "s#^#$f:#" >> "$RISK_REWRITE_Q" || true | |
| grep -nE '\$[1-9][0-9]*' "$f" 2>/dev/null | sanitize_stream | sed "s#^#$f:#" >> "$RISK_NUMBERED" || true | |
| done < "$CONF_SCAN" | |
| PY_BIN="" | |
| if command -v python3 >/dev/null 2>&1; then | |
| PY_BIN="$(command -v python3)" | |
| elif command -v python >/dev/null 2>&1; then | |
| PY_BIN="$(command -v python)" | |
| fi | |
| if [ -n "$PY_BIN" ]; then | |
| "$PY_BIN" - "$CONF_SCAN" "$RISK_BLOCK" "$RISK_CONTEXT_DIR" "$SAFELINE_NGINX_ROOT" <<'PY' 2> "$OUT_DIR/nginx/python-scan-stderr.txt" | |
| from __future__ import print_function | |
| import io, os, re, sys, hashlib | |
| conf_list = sys.argv[1] | |
| out_path = sys.argv[2] | |
| ctx_dir = sys.argv[3] | |
| safeline_root = sys.argv[4].rstrip('/') if len(sys.argv) > 4 else '/data/safeline/resources/nginx' | |
| rewrite_q = re.compile(r'^\s*rewrite\b.*\?', re.I) | |
| rewrite_direct = re.compile(r'^\s*rewrite\b', re.I) | |
| numbered = re.compile(r'\$[1-9][0-9]*') | |
| follow = re.compile(r'^\s*(rewrite|set|if)\b', re.I) | |
| sensitive = re.compile(r'(?i)(password|passwd|secret|token|api[_-]?key|access[_-]?key|authorization|credential|signature)(\s*[:=]\s*)[^;&\s"\']+') | |
| private_path = re.compile(r'(?i)/[^\s;]*\.(key|pem|p12|pfx)') | |
| def sanitize(s): | |
| s = sensitive.sub(lambda m: m.group(1) + m.group(2) + '<REDACTED>', s) | |
| s = private_path.sub('<REDACTED_PRIVATE_KEY_PATH>', s) | |
| s = re.sub(r'(?i)(Authorization:\s*)\S+', r'\1<REDACTED>', s) | |
| s = re.sub(r'(?i)(Bearer\s+)[A-Za-z0-9._~+/=-]+', r'\1<REDACTED>', s) | |
| return s | |
| def strip_comment(s): | |
| out = [] | |
| in_s = False | |
| in_d = False | |
| esc = False | |
| for ch in s: | |
| if esc: | |
| out.append(ch); esc = False; continue | |
| if ch == '\\': | |
| out.append(ch); esc = True; continue | |
| if ch == "'" and not in_d: | |
| in_s = not in_s; out.append(ch); continue | |
| if ch == '"' and not in_s: | |
| in_d = not in_d; out.append(ch); continue | |
| if ch == '#' and not in_s and not in_d: | |
| break | |
| out.append(ch) | |
| return ''.join(out) | |
| def site_kind(path): | |
| p = path | |
| site = '-' | |
| kind = 'generic' | |
| if safeline_root and p.startswith(safeline_root + '/'): | |
| kind = 'safeline-nginx' | |
| rel = p[len(safeline_root)+1:] | |
| site = rel.split('/')[0] if rel else '-' | |
| elif '/www/server/panel/vhost/rewrite/' in p: | |
| kind = 'bt-rewrite'; site = os.path.basename(p).replace('.conf', '') | |
| elif '/www/server/panel/vhost/nginx/redirect/' in p or '/www/server/panel/vhost/redirect/' in p: | |
| kind = 'bt-redirect' | |
| parts = p.split('/') | |
| if 'redirect' in parts: | |
| idx = parts.index('redirect') | |
| if idx + 1 < len(parts): site = parts[idx+1] | |
| elif '/www/server/panel/vhost/nginx/proxy/' in p or '/www/server/panel/vhost/proxy/' in p: | |
| kind = 'bt-proxy' | |
| parts = p.split('/') | |
| if 'proxy' in parts: | |
| idx = parts.index('proxy') | |
| if idx + 1 < len(parts): site = parts[idx+1] | |
| elif '/www/server/panel/vhost/nginx/' in p: | |
| kind = 'bt-site-conf'; site = os.path.basename(p).replace('.conf', '') | |
| return site, kind | |
| def safe_name(path, no): | |
| h = hashlib.sha1((path + ':' + str(no)).encode('utf-8', 'ignore')).hexdigest()[:12] | |
| base = re.sub(r'[^A-Za-z0-9_.-]+', '_', os.path.basename(path))[:80] | |
| return '%s-line%s-%s.txt' % (base, no, h) | |
| def write_context(path, lines, no, reason): | |
| try: | |
| if not os.path.isdir(ctx_dir): | |
| os.makedirs(ctx_dir) | |
| start = max(1, no - 8) | |
| end = min(len(lines), no + 8) | |
| name = safe_name(path, no) | |
| cp = os.path.join(ctx_dir, name) | |
| with io.open(cp, 'w', encoding='utf-8', errors='ignore') as w: | |
| w.write(u'file: %s\nline: %s\nreason: %s\n--- context ---\n' % (path, no, reason)) | |
| for i in range(start, end + 1): | |
| mark = '>>' if i == no else ' ' | |
| w.write(u'%s %6d: %s\n' % (mark, i, sanitize(lines[i-1].rstrip('\n')))) | |
| return cp | |
| except Exception: | |
| return '' | |
| paths = [] | |
| with io.open(conf_list, 'r', encoding='utf-8', errors='ignore') as f: | |
| for line in f: | |
| p = line.strip() | |
| if p and os.path.isfile(p): | |
| paths.append(p) | |
| records = [] | |
| for path in paths: | |
| try: | |
| with io.open(path, 'r', encoding='utf-8', errors='ignore') as f: | |
| raw_lines = f.readlines() | |
| except Exception as e: | |
| continue | |
| depth = 0 | |
| last_rewrite_q = {} | |
| for idx, raw in enumerate(raw_lines, 1): | |
| line = strip_comment(raw).strip() | |
| if not line: | |
| # still track braces conservatively from raw? no | |
| continue | |
| site, kind = site_kind(path) | |
| if rewrite_direct.search(line) and '?' in line and numbered.search(line): | |
| ctx = write_context(path, raw_lines, idx, 'HIGH same-line rewrite + ? + $N') | |
| records.append(('HIGH', path, idx, site, kind, 'same-line rewrite contains ? and $1/$2 numbered capture', sanitize(line), ctx)) | |
| if rewrite_q.search(line): | |
| last_rewrite_q[depth] = (idx, line) | |
| if follow.search(line) and numbered.search(line): | |
| for d in range(depth, -1, -1): | |
| if d in last_rewrite_q: | |
| prev_no, prev_line = last_rewrite_q[d] | |
| if prev_no != idx: | |
| ctx = write_context(path, raw_lines, idx, 'CHECK block has earlier rewrite + ? and later $N') | |
| reason = 'same block has earlier rewrite+? at line %s; current rewrite/set/if uses $1/$2' % prev_no | |
| records.append(('CHECK', path, idx, site, kind, reason, sanitize(line), ctx)) | |
| break | |
| # Update depth after checking current line. It is an approximation, but useful for nginx blocks. | |
| depth += line.count('{') - line.count('}') | |
| for d in list(last_rewrite_q.keys()): | |
| if d > depth: | |
| del last_rewrite_q[d] | |
| with io.open(out_path, 'w', encoding='utf-8') as out: | |
| for rec in records: | |
| sev, path, no, site, kind, reason, line, ctx = rec | |
| out.write(u'[%s] file=%s line=%s site=%s kind=%s reason=%s text=%s context=%s\n' % (sev, path, no, site, kind, reason, line, ctx)) | |
| PY | |
| else | |
| echo "python3/python not found; block-level heuristic scan skipped. Grep direct scan still available." > "$RISK_BLOCK" | |
| fi | |
| # Optional full config copies. | |
| if [ "${COLLECT_FULL_CONFIG:-0}" = "1" ]; then | |
| while IFS= read -r f; do | |
| [ -r "$f" ] || continue | |
| rel="$(printf '%s' "$f" | sed 's#^/##; s#[/ ]#_#g; s#[^A-Za-z0-9_.-]#_#g')" | |
| sanitize_stream < "$f" > "$OUT_DIR/nginx/config-full-sanitized/$rel" 2>/dev/null || true | |
| done < "$CONF_SCAN" | |
| fi | |
| if [ "${COLLECT_RAW_CONFIG:-0}" = "1" ]; then | |
| while IFS= read -r f; do | |
| [ -r "$f" ] || continue | |
| rel="$(printf '%s' "$f" | sed 's#^/##; s#[/ ]#_#g; s#[^A-Za-z0-9_.-]#_#g')" | |
| cp "$f" "$OUT_DIR/nginx/config-full-raw/$rel" 2>/dev/null || true | |
| done < "$CONF_SCAN" | |
| fi | |
| # ------------------------------- | |
| # 7. Log suspicious excerpts | |
| # ------------------------------- | |
| say "[7/8] Collecting limited NGINX log/crash clues" | |
| LOG_SCAN="${LOG_SCAN:-1}" | |
| LOG_MAX_LINES="${LOG_MAX_LINES:-300}" | |
| LOG_FILES="$OUT_DIR/logs/nginx-log-files.txt" | |
| : > "$LOG_FILES" | |
| if [ "$LOG_SCAN" = "1" ]; then | |
| for d in /var/log/nginx /www/wwwlogs /www/server/nginx/logs /usr/local/nginx/logs /usr/local/openresty/nginx/logs /usr/local/tengine/logs; do | |
| [ -d "$d" ] || continue | |
| find "$d" -maxdepth 2 -type f \( -name '*.log' -o -name '*access*' -o -name '*error*' \) 2>/dev/null | while IFS= read -r lf; do | |
| append_unique "$LOG_FILES" "$lf" | |
| done | |
| done | |
| { | |
| echo "===== system nginx crash clues since 2026-05-13 where available =====" | |
| if command -v journalctl >/dev/null 2>&1; then | |
| journalctl -u nginx --since '2026-05-13' --no-pager 2>/dev/null | grep -Ei 'segfault|core|worker process|exited|signal|trap|abort' | tail -n "$LOG_MAX_LINES" || true | |
| journalctl --since '2026-05-13' --no-pager 2>/dev/null | grep -Ei 'nginx|openresty|tengine' | grep -Ei 'segfault|core|worker process|exited|signal|trap|abort' | tail -n "$LOG_MAX_LINES" || true | |
| fi | |
| echo | |
| echo "===== dmesg crash clues =====" | |
| dmesg -T 2>/dev/null | grep -Ei 'nginx|openresty|tengine|segfault|trap|core' | tail -n "$LOG_MAX_LINES" || true | |
| } | sanitize_stream > "$OUT_DIR/logs/system-crash-clues.txt" 2>&1 | |
| while IFS= read -r lf; do | |
| [ -r "$lf" ] || continue | |
| rel="$(printf '%s' "$lf" | sed 's#^/##; s#[/ ]#_#g; s#[^A-Za-z0-9_.-]#_#g')" | |
| { | |
| echo "file: $lf" | |
| echo "size_kb: $(size_kb "$lf")" | |
| echo "--- suspicious lines: crash/error/url-encoding/long request ---" | |
| if printf '%s' "$lf" | grep -Eiq 'error'; then | |
| grep -Ei 'segfault|core dumped|worker process|exited on signal|signal|malloc|free\(|corrupt|overflow|abort|rewrite' "$lf" 2>/dev/null | tail -n "$LOG_MAX_LINES" || true | |
| else | |
| awk 'length($0) > 3000 || /%2[bB]|%25|%26|%3[fF]|\+/' "$lf" 2>/dev/null | tail -n "$LOG_MAX_LINES" || true | |
| fi | |
| } | sanitize_stream > "$OUT_DIR/logs/suspicious-$rel.txt" 2>&1 | |
| if [ "${COLLECT_FULL_LOGS:-0}" = "1" ] && printf '%s' "$lf" | grep -Eiq 'error'; then | |
| sanitize_stream < "$lf" > "$OUT_DIR/logs/full-error-$rel.txt" 2>/dev/null || true | |
| fi | |
| done < "$LOG_FILES" | |
| else | |
| echo "LOG_SCAN disabled by LOG_SCAN=0" > "$OUT_DIR/logs/log-scan-disabled.txt" | |
| fi | |
| # ------------------------------- | |
| # 8. Final summary and package | |
| # ------------------------------- | |
| say "[8/8] Building final summary and collection package" | |
| count_lines() { | |
| _f="$1" | |
| if [ -f "$_f" ]; then | |
| wc -l < "$_f" 2>/dev/null | tr -d ' ' | tr -d '\n' | |
| else | |
| printf '0' | |
| fi | |
| } | |
| count_matching_lines() { | |
| _pat="$1" | |
| _f="$2" | |
| if [ -f "$_f" ]; then | |
| grep "$_pat" "$_f" 2>/dev/null | wc -l | tr -d ' ' | tr -d '\n' | |
| else | |
| printf '0' | |
| fi | |
| } | |
| append_file_section() { | |
| _title="$1" | |
| _file="$2" | |
| echo | |
| echo "============================================================" | |
| echo "$_title" | |
| echo "============================================================" | |
| if [ -s "$_file" ]; then | |
| cat "$_file" 2>/dev/null || true | |
| else | |
| echo "EMPTY_OR_MISSING: $_file" | |
| fi | |
| } | |
| build_single_log() { | |
| _out="$1" | |
| _parent="$(dirname "$_out" 2>/dev/null || echo ".")" | |
| mkdir -p "$_parent" 2>/dev/null || true | |
| { | |
| echo "============================================================" | |
| echo "NGINX CVE-2026-42945 AUDIT SINGLE LOG" | |
| echo "============================================================" | |
| echo "host: $HOST" | |
| echo "time: $(date 2>/dev/null || echo unknown)" | |
| echo "script: $SCRIPT_VERSION" | |
| echo "mode: read-only; no config change; no reload/restart; no HTTP requests" | |
| echo "work_dir_used_during_scan: $OUT_DIR" | |
| echo | |
| echo "NOTE:" | |
| echo "- This single log includes the key audit evidence needed for review." | |
| echo "- Raw nginx -T dumps are not included by default. Set INCLUDE_NGINX_T_IN_LOG=1 to include sanitized nginx -T dumps." | |
| append_file_section "FINAL SUMMARY" "$SUMMARY" | |
| append_file_section "SYSTEM INFO" "$OUT_DIR/system/system-info.txt" | |
| append_file_section "PROCESS AND NETWORK" "$OUT_DIR/process/process-network.txt" | |
| append_file_section "NGINX DETECTION SUMMARY" "$OUT_DIR/nginx/detection-summary.txt" | |
| append_file_section "NGINX VERSIONS AND BUILDS" "$OUT_DIR/nginx/version-all.txt" | |
| append_file_section "NGINX LOADED CONFIG FILES FROM nginx -T" "$OUT_DIR/nginx/loaded-config-files-from-nginxT.txt" | |
| append_file_section "CONFIG ROOTS" "$OUT_DIR/nginx/config-roots.txt" | |
| append_file_section "CONFIG FILES SCANNED" "$OUT_DIR/nginx/conf-files-scanned.txt" | |
| append_file_section "CONFIG FILES SKIPPED" "$OUT_DIR/nginx/conf-files-skipped.txt" | |
| append_file_section "BAOTA INVENTORY" "$OUT_DIR/baota/baota-inventory.txt" | |
| append_file_section "SAFELINE INVENTORY" "$OUT_DIR/safeline/safeline-inventory.txt" | |
| append_file_section "CORE CONFIG LINES" "$OUT_DIR/nginx/core-config-lines.txt" | |
| append_file_section "RISK DIRECT HIGH" "$RISK_DIRECT" | |
| append_file_section "RISK BLOCK CHECK" "$RISK_BLOCK" | |
| append_file_section "REWRITE WITH QUESTION" "$RISK_REWRITE_Q" | |
| append_file_section "NUMBERED CAPTURES" "$RISK_NUMBERED" | |
| echo | |
| echo "============================================================" | |
| echo "RISK CONTEXTS" | |
| echo "============================================================" | |
| _ctx_found=0 | |
| for _ctx in "$RISK_CONTEXT_DIR"/*.txt; do | |
| [ -f "$_ctx" ] || continue | |
| _ctx_found=1 | |
| append_file_section "CONTEXT: $_ctx" "$_ctx" | |
| done | |
| [ "$_ctx_found" = "1" ] || echo "NONE" | |
| append_file_section "SYSTEM CRASH CLUES" "$OUT_DIR/logs/system-crash-clues.txt" | |
| echo | |
| echo "============================================================" | |
| echo "SUSPICIOUS LOG EXCERPTS" | |
| echo "============================================================" | |
| _log_found=0 | |
| for _lf in "$OUT_DIR/logs"/suspicious-*.txt; do | |
| [ -f "$_lf" ] || continue | |
| _log_found=1 | |
| append_file_section "LOG EXCERPT: $_lf" "$_lf" | |
| done | |
| [ "$_log_found" = "1" ] || echo "NONE" | |
| if [ "$INCLUDE_NGINX_T_IN_LOG" = "1" ]; then | |
| echo | |
| echo "============================================================" | |
| echo "SANITIZED NGINX -T DUMPS" | |
| echo "============================================================" | |
| _t_found=0 | |
| while IFS= read -r _tf; do | |
| [ -f "$_tf" ] || continue | |
| _t_found=1 | |
| echo | |
| echo "------------------------------------------------------------" | |
| echo "nginx -T file: $_tf" | |
| echo "------------------------------------------------------------" | |
| sanitize_stream < "$_tf" 2>/dev/null || true | |
| done < "$OUT_DIR/nginx/nginxT-files.txt" | |
| [ "$_t_found" = "1" ] || echo "NONE" | |
| fi | |
| } > "$_out" 2>&1 | |
| } | |
| HIGH_COUNT="$(count_lines "$RISK_DIRECT")" | |
| PY_HIGH_COUNT="$(count_matching_lines '^\[HIGH\]' "$RISK_BLOCK")" | |
| CHECK_COUNT="$(count_matching_lines '^\[CHECK\]' "$RISK_BLOCK")" | |
| REWRITE_Q_COUNT="$(count_lines "$RISK_REWRITE_Q")" | |
| NUMBERED_COUNT="$(count_lines "$RISK_NUMBERED")" | |
| CONF_SCAN_COUNT="$(count_lines "$CONF_SCAN")" | |
| BIN_COUNT="$(count_lines "$BIN_LIST")" | |
| BT_SITE_COUNT="$(count_matching_lines '^site=' "$OUT_DIR/baota/baota-inventory.txt")" | |
| SAFELINE_CONF_COUNT="$(grep -F -- "$SAFELINE_NGINX_ROOT" "$CONF_SCAN" 2>/dev/null | wc -l | tr -d ' ' | tr -d '\n')" | |
| VERSION_WARN="unknown" | |
| if grep -Eiq 'nginx version: nginx/(0\.|1\.([0-9]|1[0-9]|2[0-9])\.|1\.30\.0)' "$OUT_DIR/nginx/version-all.txt" 2>/dev/null; then | |
| VERSION_WARN="yes-nginx-version-appears-within-0.x-or-1.0-through-1.30.0" | |
| elif grep -Eiq 'nginx version: nginx/(1\.30\.[1-9]|1\.3[1-9]\.|1\.[4-9][0-9]\.|[2-9]\.)' "$OUT_DIR/nginx/version-all.txt" 2>/dev/null; then | |
| VERSION_WARN="appears-patched-version-line-or-newer" | |
| fi | |
| REWRITE_MODULE="unknown" | |
| if grep -q -- '--without-http_rewrite_module' "$OUT_DIR/nginx/version-all.txt" 2>/dev/null; then | |
| REWRITE_MODULE="some-build-shows-without-http_rewrite_module" | |
| else | |
| REWRITE_MODULE="rewrite-module-likely-present-or-not-explicitly-disabled" | |
| fi | |
| RESULT="LOW/NO OBVIOUS CONFIG TRIGGER" | |
| if [ "${HIGH_COUNT:-0}" -gt 0 ] || [ "${PY_HIGH_COUNT:-0}" -gt 0 ]; then | |
| RESULT="HIGH RISK CANDIDATE" | |
| elif [ "${CHECK_COUNT:-0}" -gt 0 ]; then | |
| RESULT="CANDIDATE RISK - NEED CONTEXT REVIEW" | |
| elif [ "${REWRITE_Q_COUNT:-0}" -gt 0 ] && [ "${NUMBERED_COUNT:-0}" -gt 0 ]; then | |
| RESULT="POSSIBLE RISK - rewrite-with-question and numbered captures both exist" | |
| elif [ "$VERSION_WARN" = "yes-nginx-version-appears-within-0.x-or-1.0-through-1.30.0" ]; then | |
| RESULT="VERSION/ENVIRONMENT WARNING - old nginx but no obvious trigger found" | |
| fi | |
| { | |
| echo "============================================================" | |
| echo "FINAL SUMMARY" | |
| echo "============================================================" | |
| echo "host: $HOST" | |
| echo "time: $(date 2>/dev/null || echo unknown)" | |
| echo "script: $SCRIPT_VERSION" | |
| echo "result: $RESULT" | |
| echo "version_assessment: $VERSION_WARN" | |
| echo "rewrite_module_assessment: $REWRITE_MODULE" | |
| echo "binaries_found: $BIN_COUNT" | |
| echo "configs_scanned: $CONF_SCAN_COUNT" | |
| echo "baota_sites_found: $BT_SITE_COUNT" | |
| echo "safeline_configs_scanned: $SAFELINE_CONF_COUNT" | |
| echo "same_line_high_grep_count: $HIGH_COUNT" | |
| echo "python_high_count: $PY_HIGH_COUNT" | |
| echo "python_block_check_count: $CHECK_COUNT" | |
| echo "rewrite_with_question_count: $REWRITE_Q_COUNT" | |
| echo "numbered_capture_count: $NUMBERED_COUNT" | |
| echo | |
| echo "===== binaries =====" | |
| cat "$BIN_LIST" 2>/dev/null || true | |
| echo | |
| echo "===== nginx versions/builds =====" | |
| sed -n '1,180p' "$OUT_DIR/nginx/version-all.txt" 2>/dev/null || true | |
| echo | |
| echo "===== BaoTa sites summary =====" | |
| grep '^site=' "$OUT_DIR/baota/baota-inventory.txt" 2>/dev/null | sed -n '1,120p' || true | |
| echo | |
| echo "===== SafeLine config summary =====" | |
| sed -n '1,160p' "$OUT_DIR/safeline/safeline-inventory.txt" 2>/dev/null || true | |
| echo | |
| echo "===== HIGH direct same-line candidates =====" | |
| if [ -s "$RISK_DIRECT" ]; then sed -n '1,200p' "$RISK_DIRECT"; else echo "NONE"; fi | |
| echo | |
| echo "===== Block-level HIGH/CHECK candidates =====" | |
| if [ -s "$RISK_BLOCK" ]; then sed -n '1,240p' "$RISK_BLOCK"; else echo "NONE"; fi | |
| echo | |
| echo "===== rewrite with ? sample =====" | |
| if [ -s "$RISK_REWRITE_Q" ]; then sed -n '1,120p' "$RISK_REWRITE_Q"; else echo "NONE"; fi | |
| echo | |
| echo "===== numbered capture sample =====" | |
| if [ -s "$RISK_NUMBERED" ]; then sed -n '1,120p' "$RISK_NUMBERED"; else echo "NONE"; fi | |
| echo | |
| echo "===== listening ports sample =====" | |
| sed -n '/===== listening ports =====/,$p' "$OUT_DIR/process/process-network.txt" 2>/dev/null | sed -n '1,120p' || true | |
| echo | |
| echo "===== crash/log clues sample =====" | |
| sed -n '1,160p' "$OUT_DIR/logs/system-crash-clues.txt" 2>/dev/null || true | |
| echo | |
| echo "===== artifact summary =====" | |
| if [ "$SINGLE_LOG" = "1" ]; then | |
| echo "single combined log: $SINGLE_LOG_FILE" | |
| fi | |
| if [ "$PACKAGE_TGZ" = "1" ]; then | |
| echo "tgz package: $OUT_DIR.tgz" | |
| else | |
| echo "tgz package: disabled by PACKAGE_TGZ=0" | |
| fi | |
| if [ "$KEEP_WORK_DIR" = "1" ] || [ "$SINGLE_LOG" != "1" ]; then | |
| echo "work directory: $OUT_DIR" | |
| echo "console report: console-report.txt" | |
| echo "system info: system/system-info.txt" | |
| echo "process/network: process/process-network.txt" | |
| echo "nginx versions: nginx/version-all.txt" | |
| echo "nginx -T outputs: nginx/nginxT/" | |
| echo "loaded config file list: nginx/loaded-config-files-from-nginxT.txt" | |
| echo "scanned config file list: nginx/conf-files-scanned.txt" | |
| echo "core config lines: nginx/core-config-lines.txt" | |
| echo "risk direct: nginx/risk-direct-high.txt" | |
| echo "risk block: nginx/risk-block-check.txt" | |
| echo "risk contexts: nginx/config-context/" | |
| echo "baota inventory: baota/baota-inventory.txt" | |
| echo "safeline inventory: safeline/safeline-inventory.txt" | |
| echo "log clues: logs/" | |
| else | |
| echo "work directory: temporary; removed after single log is built unless KEEP_WORK_DIR=1" | |
| fi | |
| } > "$SUMMARY" 2>&1 | |
| cat "$SUMMARY" | tee -a "$REPORT" | |
| if [ "$SINGLE_LOG" = "1" ]; then | |
| build_single_log "$SINGLE_LOG_FILE" | |
| fi | |
| PKG="" | |
| if [ "$PACKAGE_TGZ" = "1" ]; then | |
| PKG="$OUT_DIR.tgz" | |
| # Exclude raw configs unless explicitly collected. Directories may be empty. | |
| tar czf "$PKG" -C "$(dirname "$OUT_DIR")" "$(basename "$OUT_DIR")" 2>/dev/null || true | |
| fi | |
| say "" | |
| say "============================================================" | |
| say "COLLECTION ARTIFACTS" | |
| say "============================================================" | |
| if [ "$SINGLE_LOG" = "1" ]; then | |
| say "single_log: $SINGLE_LOG_FILE" | |
| fi | |
| if [ "$PACKAGE_TGZ" = "1" ]; then | |
| say "package: $PKG" | |
| else | |
| say "package: disabled" | |
| fi | |
| if [ "$KEEP_WORK_DIR" = "1" ] || [ "$SINGLE_LOG" != "1" ]; then | |
| say "output_dir: $OUT_DIR" | |
| say "report: $REPORT" | |
| else | |
| say "output_dir: $OUT_DIR (temporary; removing after successful single log build)" | |
| fi | |
| say "" | |
| if [ "$SINGLE_LOG" = "1" ]; then | |
| say "Send me the single .log file above." | |
| else | |
| say "Send me either the console output above or the output directory/package." | |
| fi | |
| say "To also create a .tgz package: PACKAGE_TGZ=1 sh $0" | |
| say "To keep intermediate files: KEEP_WORK_DIR=1 sh $0" | |
| say "To include sanitized nginx -T dumps in the single log: INCLUDE_NGINX_T_IN_LOG=1 sh $0" | |
| if [ "$KEEP_WORK_DIR" != "1" ] && [ "$SINGLE_LOG" = "1" ] && [ -s "$SINGLE_LOG_FILE" ]; then | |
| rm -rf "$OUT_DIR" 2>/dev/null || true | |
| fi | |
| exit 0 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment