#!/bin/bash
# root_pwd_trace.sh
# RHEL 系（CentOS / Rocky / AlmaLinux 7-9）root 密码变更与入侵线索排查脚本
# 依赖 /var/log/secure*；Debian/Ubuntu 请改查 /var/log/auth.log*
# 只读脚本，不修改系统配置

set -u

# 中途中断时清理临时文件，避免在报告目录留下 .run_* / .psp / .prp 残留
cleanup() { rm -f "${OUT_DIR}"/.run_* "${OUT_DIR}"/.psp "${OUT_DIR}"/.prp 2>/dev/null; }
trap cleanup EXIT INT TERM

TIME_NOW="$(date '+%Y%m%d_%H%M%S')"
HOSTNAME_NOW="$(hostname 2>/dev/null || echo unknown)"
OUT_DIR="./root_pwd_trace_${HOSTNAME_NOW}_${TIME_NOW}"
REPORT="${OUT_DIR}/report.txt"
HTML_REPORT="${OUT_DIR}/report.html"
RAW_DIR="${OUT_DIR}/raw"

mkdir -p "$RAW_DIR"

show_spinner() {
    local pid="$1"
    local text="$2"
    local frames=("|" "/" "-" "\\")
    local i=0

    if [ ! -t 1 ]; then
        echo "[RUN] ${text}"
        return
    fi

    while kill -0 "$pid" >/dev/null 2>&1; do
        printf "\r[RUN] %s %s" "$text" "${frames[$((i % 4))]}"
        i=$((i + 1))
        sleep 0.12
    done
}

finish_spinner() {
    local status="$1"
    local text="$2"
    local detail="${3:-}"

    if [ -t 1 ]; then
        printf "\r%-120s\r" " "
    fi

    if [ -n "$detail" ]; then
        echo "[${status}] ${text} ${detail}"
    else
        echo "[${status}] ${text}"
    fi
}

run_cmd() {
    local title="$1"
    local cmd="$2"
    local pid rc tmp status detail

    {
        echo
        echo "============================================================"
        echo "${title}"
        echo "============================================================"
        echo "[CMD] ${cmd}"
        echo
    } >> "$REPORT"

    tmp="${OUT_DIR}/.run_${RANDOM}_$$.log"
    (
        bash -c "$cmd" > "$tmp" 2>&1
    ) &
    pid=$!

    show_spinner "$pid" "采集中: ${title}"
    wait "$pid"
    rc=$?

    if [ -s "$tmp" ]; then
        cat "$tmp" >> "$REPORT"
    else
        echo "(无输出，可能无匹配数据或当前系统不适用)" >> "$REPORT"
    fi

    status="OK"
    detail=""
    if [ ! -s "$tmp" ]; then
        status="SKIP"
        detail="(无数据/不适用)"
    elif [ "$rc" -ne 0 ]; then
        if grep -Eiq 'command not found|Permission denied|Operation not permitted|syntax error|invalid option|bad option' "$tmp"; then
            status="WARN"
            detail="(命令异常，退出码: ${rc})"
        else
            status="SKIP"
            detail="(无匹配数据，退出码: ${rc})"
        fi
    fi

    rm -f "$tmp"
    finish_spinner "$status" "${title}" "$detail"
    return 0
}

copy_if_exists() {
    local src="$1"
    local dst_dir="$2"

    if ls $src >/dev/null 2>&1; then
        cp -a $src "$dst_dir/" 2>/dev/null
    fi
}

html_escape() {
    sed \
        -e 's/&/\&amp;/g' \
        -e 's/</\&lt;/g' \
        -e 's/>/\&gt;/g' \
        -e 's/"/\&quot;/g'
}

count_cmd_lines() {
    local cmd="$1"
    bash -c "$cmd" 2>/dev/null | wc -l | awk '{print $1}'
}

# ── 认证日志统一来源 ───────────────────────────────────────────────
# 优先 /var/log/secure*；为空(minimal 安装无 rsyslog)则回退 journald；
# 再留 Debian /var/log/auth.log* 钩子。所有登录/改密/失败判定都走这里，
# 保证 txt 采集与 HTML 判定口径一致，避免双正则打架。
AUTH_SOURCE=""          # secure | journald | authlog | none （供置信度判定）
detect_auth_source() {
    if zgrep -aqiE 'sshd|pam_unix' /var/log/secure* 2>/dev/null; then
        AUTH_SOURCE="secure"
    elif command -v journalctl >/dev/null 2>&1 && journalctl SYSLOG_FACILITY=10 -n1 >/dev/null 2>&1; then
        AUTH_SOURCE="journald"
    elif zgrep -aqiE 'sshd|pam_unix' /var/log/auth.log* 2>/dev/null; then
        AUTH_SOURCE="authlog"
    else
        AUTH_SOURCE="none"
    fi
}
# 输出全部认证日志正文到 stdout（按 AUTH_SOURCE 选源）
read_auth() {
    case "$AUTH_SOURCE" in
        secure)   zcat -f /var/log/secure* 2>/dev/null ;;
        authlog)  zcat -f /var/log/auth.log* 2>/dev/null ;;
        journald) journalctl SYSLOG_FACILITY=10 --no-pager -o short 2>/dev/null
                  journalctl -u sshd --no-pager -o short 2>/dev/null ;;
        *)        : ;;
    esac
}
auth_source_label() {
    case "$AUTH_SOURCE" in
        secure)   echo "/var/log/secure*" ;;
        authlog)  echo "/var/log/auth.log*" ;;
        journald) echo "journald (无 secure，已回退)" ;;
        *)        echo "无可用认证日志" ;;
    esac
}
# html_cmd_block 用 bash -c 跑子shell，需导出函数与来源变量，子shell 才能调用 read_auth
export -f read_auth auth_source_label
export AUTH_SOURCE

html_cmd_block() {
    local cmd="$1"
    local fallback="${2:-未发现相关记录}"
    local output

    output="$(bash -c "$cmd" 2>&1 | tail -200)"
    if [ -n "$output" ]; then
        printf '%s\n' "$output" | html_escape
    else
        printf '%s\n' "$fallback" | html_escape
    fi
}

generate_html_report() {
    local generated_at system_info host_html system_html
    local root_pwd_days root_pwd_date shadow_mtime passwd_mtime shadow_epoch now_epoch shadow_recent
    local root_login_events failed_login_count passwd_change_count audit_shadow_count
    local login_ip_count top_login_ips secret_count suid_suspect_count
    local rpm_tamper_count preload_status sudoers_nopasswd_count hidden_proc_count wtmp_gap
    local shadow_change_session
    local uid0_count uid0_accounts empty_pwd_count auth_keys_status auth_keys_mtime auth_keys_count
    local permit_root password_auth audit_status secure_status messages_status auth_label
    local secure_window shadow_login_match
    local score signal_level signal_class signal_text confidence_level confidence_text
    local finding_html recommendation_html summary_text
    local shadow_stat_html passwd_change_html root_login_html failed_login_html ssh_html audit_html history_html tamper_html raw_list_html timeline_html failed_ip_html account_html
    local login_ip_html secret_html authkeys_html ports_html persistence_html
    local rpm_tamper_html preload_html hidden_proc_html
    local findings_count

    generated_at="$(date '+%F %T %z')"
    system_info="$( (cat /etc/redhat-release 2>/dev/null || awk -F= '/^PRETTY_NAME=/{gsub(/"/, "", $2); print $2}' /etc/os-release 2>/dev/null || uname -a) | head -1 )"
    host_html="$(printf '%s' "$HOSTNAME_NOW" | html_escape)"
    system_html="$(printf '%s' "$system_info" | html_escape)"

    root_pwd_days="$(awk -F: '/^root:/{print $3}' /etc/shadow 2>/dev/null)"
    if [[ "$root_pwd_days" =~ ^[0-9]+$ ]]; then
        root_pwd_date="$(date -d "1970-01-01 +${root_pwd_days} days" '+%F' 2>/dev/null || echo "无法换算")"
    else
        root_pwd_date="未获取"
    fi

    shadow_mtime="$(stat -c '%y' /etc/shadow 2>/dev/null | cut -d. -f1)"
    passwd_mtime="$(stat -c '%y' /etc/passwd 2>/dev/null | cut -d. -f1)"
    shadow_epoch="$(stat -c '%Y' /etc/shadow 2>/dev/null || echo 0)"
    now_epoch="$(date '+%s')"
    [ -n "$shadow_mtime" ] || shadow_mtime="未获取"
    [ -n "$passwd_mtime" ] || passwd_mtime="未获取"

    shadow_recent="否"
    if [[ "$shadow_epoch" =~ ^[0-9]+$ ]] && [ "$shadow_epoch" -gt 0 ] && [ $((now_epoch - shadow_epoch)) -le 604800 ]; then
        shadow_recent="是"
    fi

    auth_label="$(auth_source_label)"

    # 所有登录/改密/失败判定统一走 read_auth（secure 为空时已自动回退 journald），口径一致
    # 按 "Accepted ... for root" 事件计数，而不是行数（session opened 会重复计数导致虚高）
    root_login_events="$(read_auth | grep -aihE 'Accepted (password|publickey|keyboard-interactive).* for root ' | wc -l | tr -d ' ')"
    passwd_change_count="$(read_auth | grep -aihE 'password changed for root|password for root changed|pam_unix\(passwd:chauthtok\).*root|chage.*root' | wc -l | tr -d ' ')"
    failed_login_count="$(read_auth | grep -aihE 'Failed password|Invalid user|authentication failure' | wc -l | tr -d ' ')"
    audit_shadow_count="$(count_cmd_lines "ausearch -f /etc/shadow 2>/dev/null")"
    uid0_count="$(awk -F: '($3==0){c++} END{print c+0}' /etc/passwd 2>/dev/null)"
    uid0_accounts="$(awk -F: '($3==0){printf "%s ", $1}' /etc/passwd 2>/dev/null)"
    empty_pwd_count="$(awk -F: '($2==""){c++} END{print c+0}' /etc/shadow 2>/dev/null)"

    # 成功登录来源 IP 去重数量（用于判断是否单一可信来源）
    login_ip_count="$(read_auth | grep -aihE 'Accepted .* for root ' | grep -oE 'from [0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | awk '{print $2}' | sort -u | wc -l | tr -d ' ')"
    top_login_ips="$(read_auth | grep -aihE 'Accepted .* for root ' | grep -oE 'from [0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | awk '{print $2}' | sort | uniq -c | sort -nr | head -3 | awk '{printf "%s(%s次) ", $2, $1}')"
    [ -n "$login_ip_count" ] || login_ip_count=0

    # 明文密钥/口令泄露扫描：history(root+所有用户) + mysql/python history + 存活进程命令行(/proc/cmdline) + .env/.netrc
    secret_count="$(count_cmd_lines "{ grep -rahoE '(sk-[A-Za-z0-9]{20,}|sshpass -p [^ ]+|AKIA[0-9A-Z]{16}|password=[^ ]+|passwd=[^ ]+|token=[A-Za-z0-9]{16,})' /root/.bash_history /root/.mysql_history /root/.python_history /home/*/.bash_history /root/.netrc /home/*/.netrc 2>/dev/null; tr '\\0' ' ' < /dev/null; for c in /proc/[0-9]*/cmdline; do tr '\\0' ' ' < \"\$c\" 2>/dev/null | grep -aoE '(-p[ =]?[^ ]+|password=[^ ]+|sk-[A-Za-z0-9]{20,})'; echo; done; } 2>/dev/null | grep -avE '^$'")"

    # ── 完整性基线：rpm -Va 找被篡改的系统二进制（仅二进制路径，排除配置/文档噪音，降假阳性）
    if command -v rpm >/dev/null 2>&1; then
        rpm_tamper_count="$(rpm -Va 2>/dev/null | awk '$1 ~ /5/ || $1 ~ /M/ {print $NF}' | grep -E '^/(usr/)?(s?bin|lib(64)?|libexec)/' | grep -vE '\.conf$|\.cfg$' | sort -u | wc -l | tr -d ' ')"
    else
        rpm_tamper_count="N/A"
    fi

    # ── SUID：用 rpm -Vf 校验，只报"不属于任何包"或"校验失败"的（mtime 会被 touch -r 伪造，不作为判据）
    suid_suspect_count=0
    if command -v rpm >/dev/null 2>&1; then
        while IFS= read -r sf; do
            [ -n "$sf" ] || continue
            if ! rpm -qf "$sf" >/dev/null 2>&1; then
                suid_suspect_count=$((suid_suspect_count + 1))           # 不属于任何包 = 可疑
            elif rpm -Vf "$sf" 2>/dev/null | grep -q '^..5'; then
                suid_suspect_count=$((suid_suspect_count + 1))           # 属于包但校验和变了 = 被替换
            fi
        done < <(find / -xdev -perm -4000 -type f 2>/dev/null)
    else
        suid_suspect_count="N/A"
    fi

    # ── 后门常驻点：ld.so.preload（存在即高危劫持点）、sudoers NOPASSWD
    if [ -s /etc/ld.so.preload ]; then
        preload_status="存在"
    else
        preload_status="无"
    fi
    sudoers_nopasswd_count="$(grep -rhE '^[^#]*NOPASSWD' /etc/sudoers /etc/sudoers.d/ 2>/dev/null | wc -l | tr -d ' ')"

    # ── 隐藏进程：/proc 里有 PID 但 ps 看不到（rootkit 迹象）
    hidden_proc_count=0
    {
        ps -eo pid= 2>/dev/null | tr -d ' ' | sort -u > "${OUT_DIR}/.ps_pids" 2>/dev/null
        for p in /proc/[0-9]*; do basename "$p"; done | sort -u > "${OUT_DIR}/.proc_pids" 2>/dev/null
        hidden_proc_count="$(comm -13 "${OUT_DIR}/.ps_pids" "${OUT_DIR}/.proc_pids" 2>/dev/null | wc -l | tr -d ' ')"
        rm -f "${OUT_DIR}/.ps_pids" "${OUT_DIR}/.proc_pids"
    } 2>/dev/null
    [ -n "$hidden_proc_count" ] || hidden_proc_count=0

    # ── wtmp 断档校验：改密-登录关联依赖 wtmp，被清会导致假阴性，需显式标注
    wtmp_gap="否"
    if [ ! -s /var/log/wtmp ]; then
        wtmp_gap="是(wtmp 为空)"
    elif last -F 2>/dev/null | grep -q 'wtmp begins'; then
        wtmp_gap="否"
    fi

    # 认证日志覆盖的时间窗口（最早一条），用于判断是否覆盖到改密时间
    secure_window="$(read_auth | grep -oE '^[A-Z][a-z]{2} +[0-9]+' | head -1)"
    [ -n "$secure_window" ] || secure_window="未知"

    if ls /var/log/secure* >/dev/null 2>&1; then
        secure_status="存在"
    else
        secure_status="未发现"
    fi
    if ls /var/log/messages* >/dev/null 2>&1; then
        messages_status="存在"
    else
        messages_status="未发现"
    fi

    if [ -f /root/.ssh/authorized_keys ]; then
        auth_keys_status="存在"
        auth_keys_mtime="$(stat -c '%y' /root/.ssh/authorized_keys 2>/dev/null | cut -d. -f1)"
        auth_keys_count="$(grep -cve '^[[:space:]]*$' /root/.ssh/authorized_keys 2>/dev/null)"
        [ -n "$auth_keys_mtime" ] || auth_keys_mtime="未获取"
        [ -n "$auth_keys_count" ] || auth_keys_count="0"
    else
        auth_keys_status="未发现"
        auth_keys_mtime="-"
        auth_keys_count="0"
    fi

    permit_root="$(sshd -T 2>/dev/null | awk '/^permitrootlogin /{print $2; exit}')"
    password_auth="$(sshd -T 2>/dev/null | awk '/^passwordauthentication /{print $2; exit}')"
    # systemctl is-active 在 inactive 时返回非零会触发 || 分支，需吞掉其 stdout 避免出现 "inactive\nunknown"
    audit_status="$(systemctl is-active auditd 2>/dev/null)"
    [ -n "$audit_status" ] || audit_status="unknown"
    [ -n "$permit_root" ] || permit_root="未获取"
    [ -n "$password_auth" ] || password_auth="未获取"

    score=0
    finding_html=""
    findings_count=0

    add_finding() {
        local level="$1"
        local title="$2"
        local reason="$3"
        local advice="$4"
        findings_count=$((findings_count + 1))
        finding_html="${finding_html}
        <article class=\"finding ${level}\">
          <div class=\"finding-head\">
            <span class=\"tag ${level}\">${level}</span>
            <span class=\"finding-title\">${title}</span>
          </div>
          <p class=\"finding-desc\">${reason}</p>
          <div class=\"advice\"><strong>排查建议：</strong>${advice}</div>
        </article>"
    }

    # 关联：改密时刻是否落在某个 root 登录会话内（精确到会话，并带出来源 IP，降误判）
    shadow_login_match="未知"
    shadow_change_session=""
    if [ -n "$shadow_mtime" ] && [ "$shadow_mtime" != "未获取" ] && [ "$wtmp_gap" = "否" ]; then
        local shadow_day shadow_hm
        # 强制 C locale，使月份为英文（Oct/Jun…），与 last 输出一致，否则中文 locale 下永远匹配不上
        shadow_day="$(LC_ALL=C date -d "$shadow_mtime" '+%b %e' 2>/dev/null)"
        shadow_hm="$(date -d "$shadow_mtime" '+%H:%M' 2>/dev/null)"
        # 取改密当天的 root 会话行（含登录-登出时间窗与来源 IP），供会话级核对
        shadow_change_session="$(LC_ALL=C last -F root 2>/dev/null | grep "$shadow_day" | head -5)"
        if [ -n "$shadow_change_session" ]; then
            shadow_login_match="是"
        else
            shadow_login_match="否"
        fi
    elif [ "$wtmp_gap" != "否" ]; then
        shadow_login_match="不可信"   # wtmp 被清/为空，关联结论不可采信，避免假阴性
    fi

    if [ "${uid0_count:-0}" -gt 1 ]; then
        score=$((score + 5))
        add_finding "high" "发现多个 UID 0 账号" "UID 0 账号：${uid0_accounts:-未获取}。除 root 外的 UID 0 账号通常需要重点核实。" "立即确认账号来源，必要时禁用异常账号并保留现场。"
    fi
    if [ "${empty_pwd_count:-0}" -gt 0 ]; then
        score=$((score + 5))
        add_finding "high" "发现空密码 shadow 账号" "空密码账号数量：${empty_pwd_count}。这类账号可能允许无密码登录或被滥用。" "立刻锁定相关账号并核对登录日志。"
    fi
    if [ "$preload_status" = "存在" ]; then
        score=$((score + 6))
        add_finding "high" "/etc/ld.so.preload 存在（预加载劫持点）" "检测到 /etc/ld.so.preload 非空。该文件会强制所有动态链接程序预载指定 .so，是常见的 rootkit / 进程隐藏 / 凭据窃取手法，正常系统默认不存在。" "立即查看其内容指向的 .so 文件来源；非业务明确需要应视为高度可疑，保全现场后排查。"
    fi
    if [ "$rpm_tamper_count" != "N/A" ] && [ "${rpm_tamper_count:-0}" -gt 0 ]; then
        score=$((score + 5))
        add_finding "high" "系统二进制被篡改（rpm 校验失败）" "rpm -Va 发现 ${rpm_tamper_count} 个系统二进制（/usr/bin、/usr/sbin、/lib 等）校验和或权限与安装包不一致，可能被替换或植入后门。" "对照下方清单逐个核实；确认被篡改的应重装对应 rpm 包并深入排查。"
    fi
    if [ "${hidden_proc_count:-0}" -gt 0 ]; then
        score=$((score + 5))
        add_finding "high" "发现隐藏进程迹象" "/proc 中存在 ${hidden_proc_count} 个 PID 但 ps 无法列出，是内核级 rootkit 隐藏进程的典型特征。" "用 ls /proc/<pid> 直接核对这些 PID 的 cmdline/exe；强烈建议离线取证。"
    fi
    if [ "${secret_count:-0}" -gt 0 ]; then
        score=$((score + 4))
        add_finding "high" "存在明文密钥/口令泄露" "在 history / .netrc / 存活进程命令行中匹配到 ${secret_count} 处疑似明文凭据（API key、sshpass 密码、access key 等）。这类凭据一旦泄露可被直接利用。" "立即轮换相关密钥与密码，并清理 history；排查这些凭据是否已被异常调用。"
    fi
    if [ "$suid_suspect_count" != "N/A" ] && [ "${suid_suspect_count:-0}" -gt 0 ]; then
        score=$((score + 3))
        add_finding "high" "发现可疑 SUID 文件（非系统包/校验失败）" "有 ${suid_suspect_count} 个 SUID 文件不属于任何 rpm 包或校验和与包不一致（mtime 可被 touch -r 伪造，此处不依赖 mtime）。" "逐个核对来源，非系统自带的 SUID 提权文件需重点排查。"
    fi
    if [ "${sudoers_nopasswd_count:-0}" -gt 0 ]; then
        add_finding "medium" "sudoers 中存在 NOPASSWD 授权" "/etc/sudoers* 中有 ${sudoers_nopasswd_count} 条 NOPASSWD 规则，可被用于免密提权。" "确认每条 NOPASSWD 是否为业务必需，警惕被新增的免密提权后门。"
    fi
    # root 登录来源：仅当出现多个来源 IP 时才提示，单一可信来源不算异常
    if [ "${login_ip_count:-0}" -gt 1 ]; then
        score=$((score + 2))
        add_finding "medium" "root 登录存在多个来源 IP" "root 成功登录共 ${root_login_events} 次，来自 ${login_ip_count} 个不同 IP。Top 来源：${top_login_ips:-未提取}。" "确认每个来源 IP 是否都属于堡垒机、VPN、办公网或云控制台，陌生 IP 重点核查。"
    elif [ "${root_login_events:-0}" -gt 0 ]; then
        add_finding "low" "root 登录来源单一" "root 成功登录 ${root_login_events} 次，均来自同一来源：${top_login_ips:-未提取}。来源单一通常说明是管理员日常登录。" "确认该 IP 确属可信来源即可，无需特别处置。"
    fi
    if [ "${failed_login_count:-0}" -gt 50 ]; then
        score=$((score + 2))
        add_finding "medium" "发现较多认证失败线索" "认证失败或异常用户记录 ${failed_login_count} 条，可能存在爆破或扫描。" "查看失败来源 IP 排名，必要时封禁来源并加固 SSH。"
    fi
    # 改密研判：优先报告已确定的改密日期，再结合日志覆盖窗口与当天登录情况
    if [ "${passwd_change_count:-0}" -gt 0 ]; then
        score=$((score + 2))
        add_finding "medium" "认证日志中存在 root 改密记录" "在认证日志（${auth_label}）中匹配到 root 改密或 PAM chauthtok 记录 ${passwd_change_count} 条。" "核对改密时间是否属于计划操作，并关联执行人。"
    elif [ "$shadow_recent" = "是" ]; then
        score=$((score + 3))
        add_finding "medium" "shadow 最近有修改但未找到改密日志" "/etc/shadow 最近 7 天内被修改，但 secure 改密关键词未匹配到记录。" "重点排查是否直接编辑 shadow、日志缺失、日志被清理或系统日志路径不适配。"
    elif [ "$shadow_login_match" = "是" ]; then
        add_finding "low" "改密时间已确定且落在某次 root 会话内" "root 密码最后修改于 ${root_pwd_date}（shadow 修改时间 ${shadow_mtime}），该时刻匹配到 root 登录会话（见下方会话明细及来源 IP），倾向于正常授权改密。认证日志（${auth_label}）最早仅回溯到 ${secure_window}，未覆盖该时间点，因此查不到改密日志属正常现象，而非被清理。" "核对该会话来源 IP 是否可信、操作人是否本人即可。"
    elif [ "$shadow_login_match" = "不可信" ]; then
        score=$((score + 2))
        add_finding "medium" "改密关联因 wtmp 异常无法采信" "root 密码最后修改于 ${root_pwd_date}，但 wtmp 状态异常（${wtmp_gap}），无法用登录记录核对改密会话。wtmp 被清是入侵后常见的痕迹清理手法。" "改用 journald/云审计核对改密时刻的登录来源；wtmp 被清本身需作为可疑信号排查。"
    else
        add_finding "low" "改密时间已确定" "root 密码最后修改于 ${root_pwd_date}（shadow 修改时间 ${shadow_mtime}）。认证日志（${auth_label}）最早仅回溯到 ${secure_window}，未必覆盖该时间点。" "结合堡垒机/云审计核对改密当天是谁登录、是否计划操作。"
    fi
    if [ "${audit_shadow_count:-0}" -gt 0 ]; then
        score=$((score + 2))
        add_finding "medium" "auditd 记录到 shadow 访问" "auditd 中匹配到 /etc/shadow 相关记录 ${audit_shadow_count} 条。" "优先查看 auditd 事件中的进程、UID、时间和执行路径。"
    fi
    if [ "$auth_keys_status" = "存在" ]; then
        add_finding "medium" "root authorized_keys 存在多把公钥" "root authorized_keys 有 ${auth_keys_count} 条非空记录，修改时间：${auth_keys_mtime}。详见下方逐条列表。" "逐条核对公钥归属（尤其是以个人邮箱/外部命名的 key），删除未知密钥并检查其他用户 authorized_keys。"
    fi
    if [ "$permit_root" = "yes" ] || [ "$permit_root" = "prohibit-password" ]; then
        score=$((score + 1))
        add_finding "medium" "SSH 允许 root 登录" "sshd 有效配置 permitrootlogin=${permit_root}。" "如无必要，建议禁止 root 直接登录，改用普通用户加 sudo。"
    fi
    if [ "$AUTH_SOURCE" = "none" ]; then
        score=$((score + 2))
        add_finding "medium" "无任何可用认证日志" "secure*、auth.log* 与 journald 均未取到认证记录。可能是日志被清理，也可能是日志服务从未运行——两种都会导致登录类检测大面积漏报。" "确认 rsyslog/journald 状态与磁盘；日志缺失时不能据此判定安全，必须结合云厂商登录审计。"
    elif [ "$AUTH_SOURCE" = "journald" ]; then
        add_finding "low" "认证日志来自 journald（无 secure 文件）" "未发现 /var/log/secure*，已自动回退 journald 读取认证记录。注意 journald 默认可能不持久化（重启丢失），覆盖窗口有限。" "如需长期留痕，建议开启 journald 持久化或部署 rsyslog；本次结论基于现有 journald 数据。"
    fi
    if [ "$audit_status" != "active" ]; then
        add_finding "low" "auditd 未处于 active 状态" "auditd 当前状态：${audit_status}，审计证据可能不完整。" "如果服务器有合规或溯源要求，建议启用 auditd 并配置关键文件审计。"
    fi
    if [ "$findings_count" -eq 0 ]; then
        add_finding "low" "未发现明显异常线索" "当前规则判断未命中高危项，但这不等于可以排除入侵。" "继续结合堡垒机、云审计、业务变更记录和人工复核。"
    fi

    signal_level="低"
    signal_class="low"
    signal_text="未发现明显高危线索，仍需结合业务变更记录人工确认。"
    if [ "$score" -ge 5 ]; then
        signal_level="高"
        signal_class="high"
        signal_text="命中高危线索，建议立即复核登录来源、账号、SSH 密钥和审计记录。"
    elif [ "$score" -ge 2 ]; then
        signal_level="中"
        signal_class="medium"
        signal_text="存在需要人工确认的安全线索，建议按时间线逐项核对。"
    fi

    confidence_level="低"
    confidence_text="关键日志或审计来源不足，结论只能作为初筛。"
    if [ "$AUTH_SOURCE" != "none" ]; then
        confidence_level="中"
        confidence_text="认证日志（${auth_label}）可用，结论具备基础参考价值。"
    fi
    if [ "$AUTH_SOURCE" != "none" ] && [ "$audit_status" = "active" ] && [ "$rpm_tamper_count" != "N/A" ]; then
        confidence_level="高"
        confidence_text="认证日志、auditd 与 rpm 完整性基线均可用，证据链相对完整。"
    fi

    if [ "${passwd_change_count:-0}" -gt 0 ] && [ "${root_login_events:-0}" -gt 0 ]; then
        summary_text="root 密码最后修改于 ${root_pwd_date}，且认证日志（${auth_label}）中存在 root 改密记录与登录记录，需确认改密是否由授权登录触发。"
    elif [ "$shadow_recent" = "是" ]; then
        summary_text="root 密码最后修改于 ${root_pwd_date}（最近 7 天内），但未找到明确改密日志，需重点关注是否被直接修改 shadow 或日志缺失。"
    elif [ "$shadow_login_match" = "是" ]; then
        summary_text="root 密码最后修改于 ${root_pwd_date}，改密时刻匹配到 root 登录会话（见证据），倾向于正常授权改密。认证日志（${auth_label}）最早仅到 ${secure_window}，未覆盖改密时间点，查不到改密日志属正常现象。"
    elif [ "$shadow_login_match" = "不可信" ]; then
        summary_text="root 密码最后修改于 ${root_pwd_date}，但 wtmp 状态异常（${wtmp_gap}），无法用登录记录核对改密会话，需借助 journald/云审计，且 wtmp 被清本身可疑。"
    else
        summary_text="root 密码最后修改于 ${root_pwd_date}。认证日志（${auth_label}）最早仅回溯到 ${secure_window}，未必覆盖该时间点，建议结合堡垒机/云审计核对改密当天的登录与操作。"
    fi

    recommendation_html="
      <ol>
        <li>先核对关键时间线中的 root 登录、改密、shadow 修改和 authorized_keys 修改时间。</li>
        <li>确认所有 root 登录来源 IP 是否来自堡垒机、VPN、云厂商控制台或可信办公网。</li>
        <li>核对 root authorized_keys 与所有 UID 0 账号，删除未知密钥和异常账号前先保留证据。</li>
        <li>如果日志缺失或 auditd 未启用，不要仅凭本报告排除入侵，继续查云审计和业务侧操作记录。</li>
      </ol>"

    shadow_stat_html="$(html_cmd_block "stat /etc/passwd /etc/shadow /etc/group /etc/gshadow 2>/dev/null; echo; chage -l root 2>/dev/null")"
    # 改密时刻匹配到的 root 会话单独拼接（含来源 IP），避免把 last 输出注入 bash -c 字符串
    if [ -n "$shadow_change_session" ]; then
        shadow_stat_html="${shadow_stat_html}
$(printf '\n改密时刻匹配到的 root 会话(来源 IP)：\n%s\n' "$shadow_change_session" | html_escape)"
    fi
    passwd_change_html="$(html_cmd_block "read_auth | grep -aihE 'passwd.*root|password changed for root|password for root changed|chauthtok|pam_unix\\(passwd:chauthtok\\)'" "未在认证日志中发现 root 改密记录（注意：日志窗口可能未覆盖改密时间）")"
    root_login_html="$(html_cmd_block "read_auth | grep -aihE 'Accepted (password|publickey|keyboard-interactive).* for root ' | tail -120" "未在认证日志中发现 root 成功登录记录")"
    login_ip_html="$(html_cmd_block "read_auth | grep -aihE 'Accepted .* for root ' | grep -oE 'from [0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+' | awk '{print \$2}' | sort | uniq -c | sort -nr" "未提取到 root 成功登录来源 IP")"
    failed_login_html="$(html_cmd_block "read_auth | grep -aihE 'Failed password|Invalid user|authentication failure' | tail -160" "未在认证日志中发现认证失败记录")"
    failed_ip_html="$(html_cmd_block "read_auth | grep -aihE 'Failed password|Invalid user|authentication failure' | awk '{for(i=1;i<=NF;i++){if(\$i==\"from\"){print \$(i+1)}}}' | sort | uniq -c | sort -nr | head -30" "未提取到失败登录来源 IP")"
    # 时间线只保留登录/改密/提权/账号/密钥这类高价值事件，剔除 logrotate/rsyslog HUP 噪音
    timeline_html="$(html_cmd_block "read_auth | grep -aihE 'password changed for root|chauthtok|Accepted .* for root |Failed password|Invalid user|sudo:|su\\[|useradd|usermod|chage' | grep -avE 'logrotate|was HUPed|imjournal|Deactivated successfully' | tail -180" "未生成关键时间线")"
    ssh_html="$(html_cmd_block "sshd -T 2>/dev/null | grep -Ei '^(permitrootlogin|passwordauthentication|pubkeyauthentication) '; echo; stat /root/.ssh /root/.ssh/authorized_keys 2>/dev/null" "未获取到 SSH 配置信息")"
    # authorized_keys 逐条列出（注释每把 key 的注释字段，便于核对归属）
    authkeys_html="$(html_cmd_block "for f in /root/.ssh/authorized_keys /home/*/.ssh/authorized_keys; do [ -f \"\$f\" ] || continue; echo \"===== \$f =====\"; awk 'NF{n++; print n\". 类型=\"\$1\"  注释/归属=\"\$NF} !NF{next}' \"\$f\"; echo; done" "未发现任何 authorized_keys")"
    # 密钥/口令泄露：history/.netrc/存活进程命令行，命中后脱敏，避免报告本身再次泄露
    secret_html="$(html_cmd_block "{ grep -rahoE '(sk-[A-Za-z0-9]{20,}|sshpass -p [^ ]+|AKIA[0-9A-Z]{16}|password=[^ ]+|passwd=[^ ]+|token=[A-Za-z0-9]{16,})' /root/.bash_history /root/.mysql_history /root/.python_history /home/*/.bash_history /root/.netrc /home/*/.netrc 2>/dev/null; for c in /proc/[0-9]*/cmdline; do tr '\\0' ' ' < \"\$c\" 2>/dev/null | grep -aoE '(sshpass -p [^ ]+|password=[^ ]+|sk-[A-Za-z0-9]{20,})'; done; } | sed -E 's/(sk-[A-Za-z0-9]{4})[A-Za-z0-9]+/\\1****【已脱敏】/g; s/(-p .)[^ ]+/\\1****【已脱敏】/g; s/(AKIA[0-9A-Z]{4})[0-9A-Z]+/\\1****【已脱敏】/g; s/(password=.)[^ ]+/\\1****【已脱敏】/g; s/(passwd=.)[^ ]+/\\1****【已脱敏】/g' | sort -u" "未在 history/.netrc/进程命令行中发现明文密钥/口令")"
    ports_html="$(html_cmd_block "ss -tunlp 2>/dev/null | head -60" "未获取到监听端口信息")"
    persistence_html="$(html_cmd_block "echo '----- 各用户 crontab (/var/spool/cron) -----'; for cf in /var/spool/cron/* /var/spool/cron/crontabs/*; do [ -f \"\$cf\" ] && echo \"# \$cf\" && cat \"\$cf\"; done; echo; echo '----- /etc/crontab + cron.d -----'; cat /etc/crontab 2>/dev/null; for f in /etc/cron.d/*; do [ -f \"\$f\" ] && echo \"# \$f\" && cat \"\$f\"; done; echo; echo '----- systemd timers -----'; systemctl list-timers --all --no-pager 2>/dev/null | head -30; echo; echo '----- 近30天修改的 systemd 服务 -----'; find /etc/systemd /usr/lib/systemd/system -type f -mtime -30 -printf '%TY-%Tm-%Td %p\\n' 2>/dev/null | sort; echo; echo '----- /etc/profile.d 近30天改动 -----'; find /etc/profile.d -type f -mtime -30 -printf '%TY-%Tm-%Td %p\\n' 2>/dev/null | sort" "未获取到持久化信息")"
    rpm_tamper_html="$(html_cmd_block "command -v rpm >/dev/null 2>&1 && rpm -Va 2>/dev/null | awk '\$1 ~ /5/ || \$1 ~ /M/ {print}' | grep -E '/(usr/)?(s?bin|lib(64)?|libexec)/' | head -100 || echo '系统无 rpm，跳过完整性比对'" "rpm -Va 未发现二进制被篡改（仅检查 /usr/bin 等二进制路径）")"
    preload_html="$(html_cmd_block "echo '----- /etc/ld.so.preload -----'; cat /etc/ld.so.preload 2>/dev/null; echo; echo '----- /etc/ld.so.conf.d -----'; ls -l /etc/ld.so.conf.d/ 2>/dev/null; echo; echo '----- sudoers NOPASSWD -----'; grep -rnE '^[^#]*NOPASSWD' /etc/sudoers /etc/sudoers.d/ 2>/dev/null" "未发现 ld.so.preload / 异常 NOPASSWD（preload 默认应为空）")"
    hidden_proc_html="$(html_cmd_block "ps -eo pid= 2>/dev/null | tr -d ' ' | sort -u > /tmp/.psp_\$\$; for p in /proc/[0-9]*; do basename \"\$p\"; done | sort -u > /tmp/.prp_\$\$; echo '/proc 中存在但 ps 看不到的 PID（隐藏进程嫌疑）：'; comm -13 /tmp/.psp_\$\$ /tmp/.prp_\$\$; for pid in \$(comm -13 /tmp/.psp_\$\$ /tmp/.prp_\$\$); do echo \"--- PID \$pid ---\"; tr '\\0' ' ' < /proc/\$pid/cmdline 2>/dev/null; echo; readlink /proc/\$pid/exe 2>/dev/null; done; rm -f /tmp/.psp_\$\$ /tmp/.prp_\$\$" "未发现隐藏进程（ps 与 /proc 一致）")"
    audit_html="$(html_cmd_block "systemctl status auditd --no-pager 2>/dev/null | head -40; echo; ausearch -f /etc/shadow 2>/dev/null | tail -160" "未发现 auditd 或 /etc/shadow 审计记录")"
    history_html="$(html_cmd_block "grep -aiE 'passwd|shadow|useradd|usermod|chage|ssh|curl|wget|nc |bash -i|chmod \\+s|authorized_keys|truncate|history' /root/.bash_history /home/*/.bash_history 2>/dev/null | tail -180" "未在常见 shell history 中发现关键词记录")"
    account_html="$(html_cmd_block "awk -F: 'BEGIN{print \"UID 0 accounts:\"} (\$3==0){print \$0}' /etc/passwd 2>/dev/null; echo; awk -F: '(\$2==\"\"){print \"empty password:\",\$1} (\$2!~/^!|^\\*|^\\\$|^!!/){print \"possible login account:\",\$1,\$2}' /etc/shadow 2>/dev/null" "未获取到账户异常信息")"
    tamper_html="$(html_cmd_block "ls -lh /var/log/secure* /var/log/messages* /var/log/wtmp /var/log/btmp /var/log/lastlog 2>/dev/null; echo; echo 'wtmp 起始（last 末行，可判断是否被截断）：'; LC_ALL=C last -F 2>/dev/null | tail -3; echo; grep -aihE 'rsyslog|auditd|logrotate|secure|wtmp|btmp|lastlog|shadow|truncate|deleted|removed' /var/log/messages* 2>/dev/null | tail -120" "未获取到明显日志清理或服务异常线索")"
    raw_list_html="$(html_cmd_block "find '$OUT_DIR' -maxdepth 3 -type f -printf '%p\\n' 2>/dev/null | sort" "未生成原始证据文件列表")"

    cat > "$HTML_REPORT" <<EOF_HTML
<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>root 密码变更与入侵线索排查报告</title>
  <style>
    :root {
      --bg: #f8fafc;
      --panel: #ffffff;
      --text: #0f172a;
      --muted: #64748b;
      --line: #e2e8f0;
      --soft: #f1f5f9;
      
      --primary: #2563eb;
      --primary-soft: #eff6ff;
      --primary-border: #bfdbfe;
      
      --red-text: #991b1b;
      --red-bg: #fef2f2;
      --red-border: #fca5a5;
      
      --orange-text: #92400e;
      --orange-bg: #fffbeb;
      --orange-border: #fcd34d;
      
      --green-text: #166534;
      --green-bg: #f0fdf4;
      --green-border: #bbf7d0;
      
      --code-bg: #0f172a;
      --code-text: #e2e8f0;
      --code-border: #1e293b;
      
      --shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.05), 0 1px 2px 0 rgba(0, 0, 0, 0.03);
      --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -2px rgba(0, 0, 0, 0.05);
    }

    * { box-sizing: border-box; }
    html { scroll-behavior: smooth; }

    body {
      margin: 0;
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Microsoft YaHei", sans-serif;
      background-color: var(--bg);
      color: var(--text);
      line-height: 1.6;
      -webkit-font-smoothing: antialiased;
    }

    .wrap {
      max-width: 1200px;
      margin: 0 auto;
      padding: 40px 24px 64px;
    }

    /* Hero Section */
    .hero {
      padding: 32px;
      border: 1px solid var(--line);
      border-radius: 12px;
      background: #ffffff;
      box-shadow: var(--shadow);
      margin-bottom: 24px;
      position: relative;
      overflow: hidden;
    }
    
    .hero::before {
      content: "";
      position: absolute;
      top: 0;
      left: 0;
      width: 6px;
      height: 100%;
      background: var(--primary);
    }

    h1 {
      margin: 0 0 12px;
      font-size: 26px;
      font-weight: 700;
      color: var(--text);
      letter-spacing: -0.02em;
    }

    .meta {
      color: var(--muted);
      font-size: 14px;
      display: flex;
      flex-wrap: wrap;
      gap: 16px;
      align-items: center;
      margin-bottom: 20px;
    }

    .notice {
      padding: 12px 16px;
      background-color: #fafaf9;
      border: 1px solid #f3f3f2;
      border-left: 4px solid #78716c;
      border-radius: 6px;
      color: #44403c;
      font-size: 13.5px;
      margin-bottom: 24px;
    }

    /* Grid layout for cards */
    .grid {
      display: grid;
      grid-template-columns: repeat(5, minmax(0, 1fr));
      gap: 16px;
    }

    .card {
      background: var(--panel);
      border: 1px solid var(--line);
      border-radius: 10px;
      padding: 16px;
      box-shadow: var(--shadow);
      display: flex;
      flex-direction: column;
      justify-content: space-between;
      transition: transform 0.2s, box-shadow 0.2s;
    }
    
    .card:hover {
      transform: translateY(-2px);
      box-shadow: var(--shadow-md);
    }

    .label {
      color: var(--muted);
      font-size: 12.5px;
      font-weight: 500;
      margin-bottom: 8px;
      text-transform: uppercase;
      letter-spacing: 0.05em;
    }

    .value {
      font-size: 20px;
      font-weight: 700;
      color: var(--text);
      word-break: break-all;
    }

    /* Badges */
    .badge {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      padding: 4px 12px;
      border-radius: 9999px;
      font-size: 13px;
      font-weight: 600;
      text-transform: uppercase;
      border: 1px solid transparent;
    }
    
    .card.risk-high {
      border-color: var(--red-border);
      background-color: var(--red-bg);
    }
    .card.risk-medium {
      border-color: var(--orange-border);
      background-color: var(--orange-bg);
    }
    .card.risk-low {
      border-color: var(--green-border);
      background-color: var(--green-bg);
    }

    .badge.low {
      background-color: var(--green-bg);
      color: var(--green-text);
      border-color: var(--green-border);
    }

    .badge.medium {
      background-color: var(--orange-bg);
      color: var(--orange-text);
      border-color: var(--orange-border);
    }

    .badge.high {
      background-color: var(--red-bg);
      color: var(--red-text);
      border-color: var(--red-border);
    }

    /* Navigation / TOC */
    .toc {
      position: sticky;
      top: 16px;
      z-index: 100;
      display: flex;
      gap: 6px;
      flex-wrap: wrap;
      margin: 24px 0;
      padding: 8px;
      border: 1px solid var(--line);
      border-radius: 10px;
      background: rgba(255, 255, 255, 0.85);
      backdrop-filter: blur(12px);
      box-shadow: var(--shadow-md);
      transition: background-color 0.3s;
    }

    .toc a {
      color: var(--muted);
      text-decoration: none;
      font-size: 13.5px;
      font-weight: 500;
      padding: 6px 12px;
      border-radius: 6px;
      transition: all 0.2s;
    }

    .toc a:hover, .toc a.active {
      background: var(--soft);
      color: var(--text);
    }

    /* Sections */
    h2 {
      margin: 40px 0 16px;
      font-size: 18px;
      font-weight: 700;
      color: var(--text);
      display: flex;
      align-items: center;
      gap: 8px;
      scroll-margin-top: 80px;
    }
    
    h2::before {
      content: "";
      display: inline-block;
      width: 4px;
      height: 16px;
      background: var(--primary);
      border-radius: 2px;
    }

    h3 {
      margin: 0 0 12px;
      font-size: 15px;
      font-weight: 600;
      color: var(--text);
      border-bottom: 1px solid var(--line);
      padding-bottom: 8px;
    }

    .section {
      background: var(--panel);
      border: 1px solid var(--line);
      border-radius: 10px;
      padding: 24px;
      margin-bottom: 16px;
      box-shadow: var(--shadow);
    }

    .summary {
      display: grid;
      grid-template-columns: 1.2fr 0.8fr;
      gap: 20px;
    }

    /* Tables */
    .table-container {
      border: 1px solid var(--line);
      border-radius: 10px;
      overflow: hidden;
      box-shadow: var(--shadow);
      background: var(--panel);
      margin-bottom: 24px;
    }

    table {
      width: 100%;
      border-collapse: collapse;
      text-align: left;
    }

    th, td {
      padding: 12px 16px;
      font-size: 13.5px;
      border-bottom: 1px solid var(--line);
    }

    th {
      background: var(--soft);
      color: var(--text);
      font-weight: 600;
      font-size: 13px;
      text-transform: uppercase;
      letter-spacing: 0.02em;
    }

    tr:last-child td {
      border-bottom: none;
    }

    tr:hover td {
      background-color: rgba(248, 250, 252, 0.7);
    }

    /* Findings / Risk Alerts */
    .findings {
      display: grid;
      grid-template-columns: repeat(2, minmax(0, 1fr));
      gap: 16px;
    }

    .finding {
      border: 1px solid var(--line);
      border-left: 4px solid var(--muted);
      border-radius: 8px;
      background: var(--panel);
      padding: 16px;
      box-shadow: var(--shadow);
      display: flex;
      flex-direction: column;
      gap: 8px;
    }

    .finding.high {
      border-left-color: var(--red-text);
      background: linear-gradient(to right, var(--red-bg), #ffffff);
      border-color: var(--red-border) var(--red-border) var(--red-border) var(--red-text);
    }

    .finding.medium {
      border-left-color: var(--orange-text);
      background: linear-gradient(to right, var(--orange-bg), #ffffff);
      border-color: var(--orange-border) var(--orange-border) var(--orange-border) var(--orange-text);
    }

    .finding.low {
      border-left-color: var(--green-text);
      background: linear-gradient(to right, var(--green-bg), #ffffff);
      border-color: var(--green-border) var(--green-border) var(--green-border) var(--green-text);
    }

    .finding-head {
      display: flex;
      gap: 8px;
      align-items: center;
    }
    
    .finding-title {
      font-weight: 600;
      font-size: 14.5px;
      color: var(--text);
    }
    
    .finding-desc {
      font-size: 13px;
      color: var(--muted);
      margin: 0;
    }

    .finding .advice {
      margin-top: 4px;
      padding-top: 8px;
      border-top: 1px dashed var(--line);
      font-size: 13px;
      color: var(--text);
      font-weight: 500;
    }
    
    .tag {
      padding: 2px 8px;
      border-radius: 4px;
      font-size: 11px;
      font-weight: 700;
      text-transform: uppercase;
    }
    
    .tag.high {
      background-color: var(--red-text);
      color: #ffffff;
    }
    
    .tag.medium {
      background-color: var(--orange-text);
      color: #ffffff;
    }
    
    .tag.low {
      background-color: var(--green-text);
      color: #ffffff;
    }

    /* Evidence accordion style */
    .evidence {
      border: 1px solid var(--line);
      border-radius: 10px;
      background: var(--panel);
      margin-bottom: 14px;
      box-shadow: var(--shadow);
      overflow: hidden;
      transition: border-color 0.2s;
    }
    
    .evidence[open] {
      border-color: #cbd5e1;
    }

    .evidence summary {
      cursor: pointer;
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 14px 20px;
      background: var(--soft);
      font-weight: 600;
      font-size: 14px;
      user-select: none;
      outline: none;
    }
    
    .evidence summary::-webkit-details-marker {
      display: none;
    }
    
    .evidence summary::after {
      content: "";
      width: 14px;
      height: 14px;
      border-right: 2px solid var(--muted);
      border-bottom: 2px solid var(--muted);
      transform: rotate(45deg);
      transition: transform 0.2s;
      margin-right: 4px;
      margin-top: -4px;
    }
    
    .evidence[open] summary::after {
      transform: rotate(-135deg);
      margin-top: 4px;
    }

    .evidence-summary-left {
      display: flex;
      align-items: center;
      gap: 10px;
    }
    
    .evidence-title {
      color: var(--text);
    }

    .source {
      background-color: rgba(255, 255, 255, 0.6);
      border: 1px solid var(--line);
      padding: 2px 8px;
      border-radius: 4px;
      color: var(--muted);
      font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
      font-size: 11.5px;
      font-weight: 500;
    }

    .code-wrapper {
      position: relative;
    }

    .code-title {
      padding: 8px 16px;
      border-top: 1px solid var(--line);
      border-bottom: 1px solid var(--code-border);
      background: #1e293b;
      color: #94a3b8;
      font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
      font-size: 12px;
      display: flex;
      justify-content: space-between;
      align-items: center;
    }
    
    .btn-copy {
      background: rgba(255, 255, 255, 0.1);
      border: 1px solid rgba(255, 255, 255, 0.15);
      color: #e2e8f0;
      padding: 3px 8px;
      font-size: 11px;
      border-radius: 4px;
      cursor: pointer;
      transition: all 0.2s;
    }
    
    .btn-copy:hover {
      background: rgba(255, 255, 255, 0.2);
      color: #ffffff;
    }

    pre {
      margin: 0;
      padding: 16px;
      overflow: auto;
      max-height: 400px;
      background: var(--code-bg);
      color: var(--code-text);
      font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Fira Code", "Liberation Mono", monospace;
      font-size: 12.5px;
      line-height: 1.6;
      white-space: pre-wrap;
      word-break: break-all;
    }
    
    pre::-webkit-scrollbar {
      width: 8px;
      height: 8px;
    }
    
    pre::-webkit-scrollbar-track {
      background: var(--code-bg);
    }
    
    pre::-webkit-scrollbar-thumb {
      background: #334155;
      border-radius: 4px;
    }
    
    pre::-webkit-scrollbar-thumb:hover {
      background: #475569;
    }

    ol {
      margin: 0;
      padding-left: 20px;
    }

    li {
      margin: 8px 0;
      font-size: 13.5px;
      color: var(--text);
    }

    .footer {
      margin-top: 40px;
      padding-top: 20px;
      border-top: 1px solid var(--line);
      color: var(--muted);
      font-size: 13px;
      text-align: center;
    }

    /* Back to top button */
    .back-to-top {
      position: fixed;
      bottom: 24px;
      right: 24px;
      width: 44px;
      height: 44px;
      background-color: var(--panel);
      border: 1px solid var(--line);
      border-radius: 50%;
      box-shadow: var(--shadow-md);
      display: flex;
      align-items: center;
      justify-content: center;
      cursor: pointer;
      opacity: 0;
      visibility: hidden;
      transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
      z-index: 1000;
    }
    
    .back-to-top.show {
      opacity: 1;
      visibility: visible;
    }
    
    .back-to-top:hover {
      transform: translateY(-4px);
      box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
      border-color: #cbd5e1;
      background-color: var(--soft);
    }
    
    .back-to-top svg {
      width: 20px;
      height: 20px;
      fill: none;
      stroke: var(--text);
      stroke-width: 2.5;
      stroke-linecap: round;
      stroke-linejoin: round;
    }

    @media (max-width: 1024px) {
      .grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
      .summary { grid-template-columns: 1fr; }
    }
    @media (max-width: 768px) {
      .grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
      .findings { grid-template-columns: 1fr; }
      .wrap { padding: 20px 16px 40px; }
      h1 { font-size: 22px; }
    }
    @media (max-width: 480px) {
      .grid { grid-template-columns: 1fr; }
      .toc { display: none; }
    }
  </style>
</head>
<body>
  <main class="wrap">
    <section class="hero">
      <h1>root 密码变更与入侵线索排查报告</h1>
      <div class="meta">
        <span class="meta-item">主机：${host_html}</span>
        <span class="meta-item">生成时间：${generated_at}</span>
        <span class="meta-item">系统：${system_html}</span>
      </div>
      <div class="notice">本报告包含系统日志、账号文件、SSH 配置和历史命令等敏感信息，仅用于本机取证排查，不要公开上传。</div>
      <div class="grid">
        <div class="card risk-${signal_class}">
          <div class="label">风险等级</div>
          <div class="value"><span class="badge ${signal_class}">${signal_level}</span></div>
        </div>
        <div class="card">
          <div class="label">置信度</div>
          <div class="value">${confidence_level}</div>
        </div>
        <div class="card">
          <div class="label">root 密码日期</div>
          <div class="value">${root_pwd_date}</div>
        </div>
        <div class="card">
          <div class="label">root 登录(来源数)</div>
          <div class="value">${root_login_events} / ${login_ip_count} IP</div>
        </div>
        <div class="card">
          <div class="label">凭据泄露线索</div>
          <div class="value">${secret_count}</div>
        </div>
      </div>
    </section>

    <nav class="toc" aria-label="报告目录">
      <a href="#overview">1. 总览</a>
      <a href="#metrics">2. 关键指标</a>
      <a href="#findings">3. 风险发现</a>
      <a href="#timeline">4. 关键时间线</a>
      <a href="#evidence">5. 证据详情</a>
    </nav>

    <h2 id="overview">1. 总览</h2>
    <div class="summary">
      <div class="section">
        <h3>研判摘要</h3>
        <p>${summary_text}</p>
        <p>${signal_text}</p>
        <p>置信度：${confidence_text}</p>
      </div>
      <div class="section">
        <h3>建议动作</h3>
        ${recommendation_html}
      </div>
    </div>

    <h2 id="metrics">2. 关键指标</h2>
    <div class="table-container">
      <table>
        <thead>
          <tr>
            <th>项目</th>
            <th>结果</th>
            <th>说明</th>
          </tr>
        </thead>
        <tbody>
          <tr>
            <td>/etc/shadow 修改时间</td>
            <td>${shadow_mtime}</td>
            <td>最近 7 天是否修改：${shadow_recent}</td>
          </tr>
          <tr>
            <td>/etc/passwd 修改时间</td>
            <td>${passwd_mtime}</td>
            <td>用于观察账号新增、UID 变化等痕迹。</td>
          </tr>
          <tr>
            <td>root 改密日志数</td>
            <td>${passwd_change_count}</td>
            <td>来自认证日志（${auth_label}）的 passwd/chauthtok 关键词。</td>
          </tr>
          <tr>
            <td>认证日志来源</td>
            <td>${auth_label}</td>
            <td>secure 为空时自动回退 journald；"无"则登录类检测会漏，须靠云审计。</td>
          </tr>
          <tr>
            <td>root 登录来源 IP</td>
            <td>${login_ip_count} 个</td>
            <td>共 ${root_login_events} 次成功登录。Top：${top_login_ips:-未提取}</td>
          </tr>
          <tr>
            <td>认证日志覆盖起点</td>
            <td>${secure_window}</td>
            <td>早于此时间的事件（含改密）不在日志中，需靠云审计补。</td>
          </tr>
          <tr>
            <td>ld.so.preload</td>
            <td>${preload_status}</td>
            <td>预加载劫持点，正常应为"无"；"存在"高度可疑。</td>
          </tr>
          <tr>
            <td>系统二进制篡改</td>
            <td>${rpm_tamper_count}</td>
            <td>rpm -Va 校验失败的二进制数（N/A=无 rpm）。</td>
          </tr>
          <tr>
            <td>隐藏进程</td>
            <td>${hidden_proc_count}</td>
            <td>/proc 有但 ps 看不到的 PID，rootkit 迹象。</td>
          </tr>
          <tr>
            <td>wtmp 完整性</td>
            <td>${wtmp_gap}</td>
            <td>"是"表示 wtmp 可能被清，改密-登录关联不可信。</td>
          </tr>
          <tr>
            <td>凭据泄露线索</td>
            <td>${secret_count} 处</td>
            <td>history/.netrc/进程命令行中疑似明文 key / 密码。</td>
          </tr>
          <tr>
            <td>可疑 SUID（rpm 校验）</td>
            <td>${suid_suspect_count} 个</td>
            <td>不属于任何包或校验失败的 SUID（不依赖可伪造的 mtime）。</td>
          </tr>
          <tr>
            <td>sudoers NOPASSWD</td>
            <td>${sudoers_nopasswd_count} 条</td>
            <td>免密提权规则，需逐条确认是否业务必需。</td>
          </tr>
          <tr>
            <td>auditd shadow 记录数</td>
            <td>${audit_shadow_count}</td>
            <td>auditd 状态：${audit_status}</td>
          </tr>
          <tr>
            <td>UID 0 账号数</td>
            <td>${uid0_count}</td>
            <td>${uid0_accounts:-未获取}</td>
          </tr>
          <tr>
            <td>空密码账号数</td>
            <td>${empty_pwd_count}</td>
            <td>来自 /etc/shadow。</td>
          </tr>
          <tr>
            <td>root authorized_keys</td>
            <td>${auth_keys_status}</td>
            <td>非空记录：${auth_keys_count}，修改时间：${auth_keys_mtime}</td>
          </tr>
          <tr>
            <td>SSH root 登录</td>
            <td>${permit_root}</td>
            <td>passwordauthentication：${password_auth}</td>
          </tr>
          <tr>
            <td>secure/messages 日志</td>
            <td>${secure_status} / ${messages_status}</td>
            <td>日志缺失会降低研判置信度。</td>
          </tr>
        </tbody>
      </table>
    </div>

    <h2 id="findings">3. 风险发现</h2>
    <div class="findings">
      ${finding_html}
    </div>

    <h2 id="timeline">4. 关键时间线</h2>
    <details class="evidence" open id="evidence-timeline">
      <summary>
        <div class="evidence-summary-left">
          <span class="evidence-title">登录、改密、sudo/su、日志服务相关事件</span>
          <span class="source">secure* + messages*</span>
        </div>
      </summary>
      <div class="code-wrapper">
        <div class="code-title">
          <span>自动截取最近 180 条关键事件</span>
          <button class="btn-copy" onclick="copyCode(this, 'pre-timeline')">复制日志</button>
        </div>
        <pre id="pre-timeline">${timeline_html}</pre>
      </div>
    </details>

    <h2 id="evidence">5. 证据详情</h2>
    <details class="evidence" open id="evidence-shadow-stat">
      <summary>
        <div class="evidence-summary-left">
          <span class="evidence-title">账号文件与 root 密码状态</span>
          <span class="source">stat / chage</span>
        </div>
      </summary>
      <div class="code-wrapper">
        <div class="code-title">
          <span>账号文件时间与 root 密码策略</span>
          <button class="btn-copy" onclick="copyCode(this, 'pre-shadow-stat')">复制日志</button>
        </div>
        <pre id="pre-shadow-stat">${shadow_stat_html}</pre>
      </div>
    </details>

    <details class="evidence" open id="evidence-passwd-change">
      <summary>
        <div class="evidence-summary-left">
          <span class="evidence-title">root 改密相关日志</span>
          <span class="source">/var/log/secure*</span>
        </div>
      </summary>
      <div class="code-wrapper">
        <div class="code-title">
          <span>passwd / chauthtok 关键词</span>
          <button class="btn-copy" onclick="copyCode(this, 'pre-passwd-change')">复制日志</button>
        </div>
        <pre id="pre-passwd-change">${passwd_change_html}</pre>
      </div>
    </details>

    <details class="evidence" id="evidence-root-login">
      <summary>
        <div class="evidence-summary-left">
          <span class="evidence-title">root 成功登录线索</span>
          <span class="source">/var/log/secure*</span>
        </div>
      </summary>
      <div class="code-wrapper">
        <div class="code-title">
          <span>Accepted password / publickey for root</span>
          <button class="btn-copy" onclick="copyCode(this, 'pre-root-login')">复制日志</button>
        </div>
        <pre id="pre-root-login">${root_login_html}</pre>
      </div>
    </details>

    <details class="evidence" open id="evidence-login-ip">
      <summary>
        <div class="evidence-summary-left">
          <span class="evidence-title">root 成功登录来源 IP 排名</span>
          <span class="source">secure* 解析</span>
        </div>
      </summary>
      <div class="code-wrapper">
        <div class="code-title">
          <span>按成功登录次数降序（核对每个 IP 是否可信）</span>
          <button class="btn-copy" onclick="copyCode(this, 'pre-login-ip')">复制数据</button>
        </div>
        <pre id="pre-login-ip">${login_ip_html}</pre>
      </div>
    </details>

    <details class="evidence" open id="evidence-secret">
      <summary>
        <div class="evidence-summary-left">
          <span class="evidence-title">明文密钥 / 口令泄露线索（已脱敏）</span>
          <span class="source">bash_history</span>
        </div>
      </summary>
      <div class="code-wrapper">
        <div class="code-title">
          <span>API key / sshpass 密码 / access key 等</span>
          <button class="btn-copy" onclick="copyCode(this, 'pre-secret')">复制</button>
        </div>
        <pre id="pre-secret">${secret_html}</pre>
      </div>
    </details>

    <details class="evidence" open id="evidence-authkeys">
      <summary>
        <div class="evidence-summary-left">
          <span class="evidence-title">authorized_keys 逐条归属</span>
          <span class="source">root + /home/*</span>
        </div>
      </summary>
      <div class="code-wrapper">
        <div class="code-title">
          <span>逐条核对 key 注释字段，警惕个人邮箱/外部命名</span>
          <button class="btn-copy" onclick="copyCode(this, 'pre-authkeys')">复制</button>
        </div>
        <pre id="pre-authkeys">${authkeys_html}</pre>
      </div>
    </details>

    <details class="evidence" id="evidence-failed-login">
      <summary>
        <div class="evidence-summary-left">
          <span class="evidence-title">认证失败与爆破线索</span>
          <span class="source">/var/log/secure*</span>
        </div>
      </summary>
      <div class="code-wrapper">
        <div class="code-title">
          <span>Failed password / Invalid user / authentication failure</span>
          <button class="btn-copy" onclick="copyCode(this, 'pre-failed-login')">复制日志</button>
        </div>
        <pre id="pre-failed-login">${failed_login_html}</pre>
      </div>
    </details>

    <details class="evidence" id="evidence-failed-ip">
      <summary>
        <div class="evidence-summary-left">
          <span class="evidence-title">失败登录来源 IP 排名</span>
          <span class="source">secure* 解析</span>
        </div>
      </summary>
      <div class="code-wrapper">
        <div class="code-title">
          <span>按出现次数降序排列</span>
          <button class="btn-copy" onclick="copyCode(this, 'pre-failed-ip')">复制数据</button>
        </div>
        <pre id="pre-failed-ip">${failed_ip_html}</pre>
      </div>
    </details>

    <details class="evidence" id="evidence-ssh">
      <summary>
        <div class="evidence-summary-left">
          <span class="evidence-title">SSH 配置与 root 密钥</span>
          <span class="source">sshd -T / authorized_keys</span>
        </div>
      </summary>
      <div class="code-wrapper">
        <div class="code-title">
          <span>root 登录策略与密钥内容</span>
          <button class="btn-copy" onclick="copyCode(this, 'pre-ssh')">复制配置</button>
        </div>
        <pre id="pre-ssh">${ssh_html}</pre>
      </div>
    </details>

    <details class="evidence" id="evidence-account">
      <summary>
        <div class="evidence-summary-left">
          <span class="evidence-title">异常账号线索</span>
          <span class="source">passwd / shadow</span>
        </div>
      </summary>
      <div class="code-wrapper">
        <div class="code-title">
          <span>UID 0、空密码、可能可登录账号</span>
          <button class="btn-copy" onclick="copyCode(this, 'pre-account')">复制账号</button>
        </div>
        <pre id="pre-account">${account_html}</pre>
      </div>
    </details>

    <details class="evidence" id="evidence-audit">
      <summary>
        <div class="evidence-summary-left">
          <span class="evidence-title">auditd 与 /etc/shadow 审计</span>
          <span class="source">auditd / ausearch</span>
        </div>
      </summary>
      <div class="code-wrapper">
        <div class="code-title">
          <span>shadow 文件审计事件</span>
          <button class="btn-copy" onclick="copyCode(this, 'pre-audit')">复制审计</button>
        </div>
        <pre id="pre-audit">${audit_html}</pre>
      </div>
    </details>

    <details class="evidence" id="evidence-ports">
      <summary>
        <div class="evidence-summary-left">
          <span class="evidence-title">监听端口与网络连接</span>
          <span class="source">ss -tunlp</span>
        </div>
      </summary>
      <div class="code-wrapper">
        <div class="code-title">
          <span>核对是否有非预期的监听端口或外联进程</span>
          <button class="btn-copy" onclick="copyCode(this, 'pre-ports')">复制</button>
        </div>
        <pre id="pre-ports">${ports_html}</pre>
      </div>
    </details>

    <details class="evidence" id="evidence-persistence">
      <summary>
        <div class="evidence-summary-left">
          <span class="evidence-title">持久化与提权后门线索</span>
          <span class="source">cron / systemd / SUID</span>
        </div>
      </summary>
      <div class="code-wrapper">
        <div class="code-title">
          <span>计划任务、近期 systemd 服务、近 15 天新增 SUID</span>
          <button class="btn-copy" onclick="copyCode(this, 'pre-persistence')">复制</button>
        </div>
        <pre id="pre-persistence">${persistence_html}</pre>
      </div>
    </details>

    <details class="evidence" open id="evidence-preload">
      <summary>
        <div class="evidence-summary-left">
          <span class="evidence-title">预加载劫持 / 免密提权后门</span>
          <span class="source">ld.so.preload / sudoers</span>
        </div>
      </summary>
      <div class="code-wrapper">
        <div class="code-title">
          <span>ld.so.preload 正常应为空；NOPASSWD 需逐条确认</span>
          <button class="btn-copy" onclick="copyCode(this, 'pre-preload')">复制</button>
        </div>
        <pre id="pre-preload">${preload_html}</pre>
      </div>
    </details>

    <details class="evidence" open id="evidence-rpm-tamper">
      <summary>
        <div class="evidence-summary-left">
          <span class="evidence-title">系统二进制完整性（rpm 校验）</span>
          <span class="source">rpm -Va</span>
        </div>
      </summary>
      <div class="code-wrapper">
        <div class="code-title">
          <span>仅列 /usr/bin、/usr/sbin、/lib 等被篡改的二进制（5=校验和变, M=权限变）</span>
          <button class="btn-copy" onclick="copyCode(this, 'pre-rpm-tamper')">复制</button>
        </div>
        <pre id="pre-rpm-tamper">${rpm_tamper_html}</pre>
      </div>
    </details>

    <details class="evidence" id="evidence-hidden-proc">
      <summary>
        <div class="evidence-summary-left">
          <span class="evidence-title">隐藏进程检查（ps vs /proc）</span>
          <span class="source">rootkit 迹象</span>
        </div>
      </summary>
      <div class="code-wrapper">
        <div class="code-title">
          <span>/proc 有但 ps 看不到的 PID，是内核级 rootkit 典型特征</span>
          <button class="btn-copy" onclick="copyCode(this, 'pre-hidden-proc')">复制</button>
        </div>
        <pre id="pre-hidden-proc">${hidden_proc_html}</pre>
      </div>
    </details>

    <details class="evidence" id="evidence-history">
      <summary>
        <div class="evidence-summary-left">
          <span class="evidence-title">Shell history 关键词</span>
          <span class="source">root + /home/*</span>
        </div>
      </summary>
      <div class="code-wrapper">
        <div class="code-title">
          <span>改密、shadow、下载、反弹 shell、清日志等关键词</span>
          <button class="btn-copy" onclick="copyCode(this, 'pre-history')">复制历史</button>
        </div>
        <pre id="pre-history">${history_html}</pre>
      </div>
    </details>

    <details class="evidence" id="evidence-tamper">
      <summary>
        <div class="evidence-summary-left">
          <span class="evidence-title">日志完整性与服务异常线索</span>
          <span class="source">secure/messages/wtmp/btmp</span>
        </div>
      </summary>
      <div class="code-wrapper">
        <div class="code-title">
          <span>日志文件状态与清理关键词</span>
          <button class="btn-copy" onclick="copyCode(this, 'pre-tamper')">复制日志</button>
        </div>
        <pre id="pre-tamper">${tamper_html}</pre>
      </div>
    </details>

    <details class="evidence" id="evidence-raw-list">
      <summary>
        <div class="evidence-summary-left">
          <span class="evidence-title">本次导出的文件清单</span>
          <span class="source">report directory</span>
        </div>
      </summary>
      <div class="code-wrapper">
        <div class="code-title">
          <span>完整证据请查看 report.txt 与 raw/</span>
          <button class="btn-copy" onclick="copyCode(this, 'pre-raw-list')">复制清单</button>
        </div>
        <pre id="pre-raw-list">${raw_list_html}</pre>
      </div>
    </details>

    <div class="footer">
      HTML 为重点线索摘要，完整原始输出仍以 report.txt 为准。最终结论需结合业务变更单、堡垒机记录、云厂商登录审计和人员操作记录复核。
    </div>
  </main>

  <!-- 回到顶部按钮 -->
  <button id="backToTop" class="back-to-top" onclick="scrollToTop()" title="回到顶部" aria-label="回到顶部">
    <svg viewBox="0 0 24 24">
      <polyline points="18 15 12 9 6 15"></polyline>
    </svg>
  </button>

  <script>
    function updateActiveToc() {
      var sections = ['overview', 'metrics', 'findings', 'timeline', 'evidence'];
      var activeSection = '';
      
      for (var i = 0; i < sections.length; i++) {
        var refElement = document.getElementById(sections[i]);
        if (!refElement) continue;
        
        var rect = refElement.getBoundingClientRect();
        // 当元素顶部距离视口上方小于 150px 时，我们认为已经滚过了它
        if (rect.top <= 150) {
          activeSection = sections[i];
        }
      }
      
      // 默认第一个
      if (!activeSection && sections.length > 0) {
        activeSection = sections[0];
      }
      
      if (activeSection) {
        document.querySelectorAll('.toc a').forEach(function(el) { el.classList.remove('active'); });
        var activeLink = document.querySelector('.toc a[href="#' + activeSection + '"]');
        if (activeLink) {
          activeLink.classList.add('active');
        }
      }
    }

    // 监听滚动事件，控制回到顶部按钮的显示与隐藏，以及导航锚点高亮
    window.addEventListener('scroll', function() {
      var btn = document.getElementById('backToTop');
      if (window.pageYOffset > 300) {
        btn.classList.add('show');
      } else {
        btn.classList.remove('show');
      }
      
      updateActiveToc();
    });

    window.addEventListener('DOMContentLoaded', function() {
      // 如果 URL 没有锚点哈希，则滚动到顶部，防止浏览器 Scroll Restoration 机制强制定位到上次浏览位置
      if (!window.location.hash) {
        window.scrollTo(0, 0);
      }
      updateActiveToc();
    });

    // 回到顶部函数
    function scrollToTop() {
      window.scrollTo({
        top: 0,
        behavior: 'smooth'
      });
    }

    // 复制到剪贴板功能
    function copyCode(btn, elementId) {
      var pre = document.getElementById(elementId);
      if (!pre) return;
      
      var textarea = document.createElement('textarea');
      textarea.value = pre.textContent || pre.innerText;
      document.body.appendChild(textarea);
      textarea.select();
      
      try {
        var successful = document.execCommand('copy');
        if (successful) {
          var originalText = btn.innerText;
          btn.innerText = '已复制!';
          btn.style.background = '#166534';
          setTimeout(function() {
            btn.innerText = originalText;
            btn.style.background = '';
          }, 2000);
        } else {
          alert('复制失败，请手动选择复制');
        }
      } catch (err) {
        alert('浏览器不支持复制，请手动选择复制');
      }
      
      document.body.removeChild(textarea);
    }
  </script>
</body>
</html>
EOF_HTML
}

echo "正在生成取证报告：$OUT_DIR"

# 确定认证日志来源（secure 为空时回退 journald），供 txt 采集与 HTML 判定统一使用
detect_auth_source
echo "认证日志来源: $(auth_source_label)"

cat > "$REPORT" <<EOF
root 密码变更与入侵线索排查报告

主机名: ${HOSTNAME_NOW}
时间: $(date '+%F %T %z')
系统: $(cat /etc/redhat-release 2>/dev/null || uname -a)

说明:
1. 本脚本只读，不修改系统。
2. 日志可能被攻击者删除或篡改，结果只能作为线索。
3. 重点关注 /var/log/secure*、/etc/shadow 修改时间、chage root、auditd 记录。
EOF

run_cmd "1. 系统基础信息" "
date
hostname
whoami
id
uptime
uname -a
cat /etc/redhat-release 2>/dev/null
"

run_cmd "2. root 用户信息" "
id root
grep '^root:' /etc/passwd
grep '^root:' /etc/shadow 2>/dev/null | awk -F: '{print \"root shadow 字段: 用户名=\"\$1\", 密码字段长度=\"length(\$2)\", 最后修改天数=\"\$3\", 最小天数=\"\$4\", 最大天数=\"\$5\", 警告天数=\"\$6\", 失效天数=\"\$7\", 过期天数=\"\$8}'
"

run_cmd "3. root 密码最后修改日期 chage" "
chage -l root 2>/dev/null
"

run_cmd "4. 换算 /etc/shadow 中 root 密码修改日期" "
days=\$(awk -F: '/^root:/{print \$3}' /etc/shadow 2>/dev/null)
echo \"shadow 第 3 字段天数: \$days\"
if [ -n \"\$days\" ]; then
    date -d \"1970-01-01 +\${days} days\" 2>/dev/null
fi
"

run_cmd "5. 关键账号文件时间" "
stat /etc/passwd /etc/shadow /etc/group /etc/gshadow 2>/dev/null
"

run_cmd "6. 认证日志中 root 改密记录（自动选 secure/journald）" "
read_auth | grep -aiE 'passwd.*root|password changed for root|password for root changed|chauthtok|pam_unix\\(passwd:chauthtok\\)'
"

run_cmd "7. 认证日志中所有 passwd/chpasswd/usermod/chage 相关记录" "
read_auth | grep -aiE 'passwd|chpasswd|usermod|chage|shadow|pam_unix\\(passwd'
"

run_cmd "8. root 登录成功记录" "
read_auth | grep -aiE 'Accepted (password|publickey|keyboard-interactive).* for root '
"

run_cmd "9. SSH 登录失败、爆破、异常用户" "
read_auth | grep -aiE 'Failed password|Invalid user|authentication failure|Did not receive identification|Connection closed by|refused connect|POSSIBLE BREAK-IN|reverse mapping checking getaddrinfo' | tail -300
"

run_cmd "10. su / sudo 提权记录" "
read_auth | grep -aiE 'sudo|su:|session opened|session closed|COMMAND=' | tail -500
"

run_cmd "11. last root 登录记录" "
last root 2>/dev/null
"

run_cmd "12. 最近登录记录 last" "
last 2>/dev/null | head -100
"

run_cmd "13. 失败登录 lastb" "
lastb 2>/dev/null | head -100
"

run_cmd "14. 当前登录用户" "
who
w
"

run_cmd "15. auditd 状态" "
systemctl status auditd --no-pager 2>/dev/null
ls -lh /var/log/audit/ 2>/dev/null
"

run_cmd "16. auditd 中 /etc/shadow 变更记录" "
ausearch -f /etc/shadow 2>/dev/null
"

run_cmd "17. auditd 中 passwd 命令执行记录" "
ausearch -x passwd 2>/dev/null
"

run_cmd "18. auditd 中 root 用户相关记录 uid=0" "
ausearch -ua 0 2>/dev/null | tail -500
"

run_cmd "19. root 历史命令中的改密/清日志/可疑命令" "
if [ -f /root/.bash_history ]; then
    grep -ainE 'passwd|chpasswd|usermod|chage|shadow|/etc/passwd|/etc/shadow|history|secure|audit|lastlog|wtmp|btmp|truncate|rm -f|cat /dev/null|echo.*>|curl|wget|nc|ncat|bash -i|chmod \\+x|authorized_keys|crontab' /root/.bash_history
else
    echo '/root/.bash_history 不存在'
fi
"

run_cmd "20. 所有普通用户历史命令中的可疑记录" "
for f in /home/*/.bash_history; do
    [ -f \"\$f\" ] || continue
    echo
    echo \"===== \$f =====\"
    grep -ainE 'passwd|chpasswd|usermod|chage|shadow|/etc/passwd|/etc/shadow|history|secure|audit|lastlog|wtmp|btmp|truncate|rm -f|cat /dev/null|echo.*>|curl|wget|nc|ncat|bash -i|chmod \\+x|authorized_keys|crontab' \"\$f\" 2>/dev/null
done
"

run_cmd "21. 检查日志文件时间和大小" "
ls -lah /var/log/secure* 2>/dev/null
ls -lah /var/log/messages* 2>/dev/null
ls -lah /var/log/audit/audit.log* 2>/dev/null
ls -lah /var/log/wtmp /var/log/btmp /var/log/lastlog 2>/dev/null
stat /var/log/secure /var/log/messages /var/log/wtmp /var/log/btmp /var/log/lastlog 2>/dev/null
"

run_cmd "22. 检查日志是否有明显清理痕迹" "
grep -aiE 'logrotate|rsyslog|auditd|journal|secure|messages|wtmp|btmp|lastlog|truncate|deleted|removed' /var/log/messages* 2>/dev/null | tail -300
"

run_cmd "23. 最近 15 天被修改的关键目录文件" "
find /etc /root /tmp /var/tmp /dev/shm -xdev -type f -mtime -15 -printf '%TY-%Tm-%Td %TH:%TM:%TS %u %g %m %p\n' 2>/dev/null | sort
"

run_cmd "24. SSH 配置与 root 登录设置" "
sshd -T 2>/dev/null | grep -Ei 'permitrootlogin|passwordauthentication|pubkeyauthentication|authorizedkeysfile|challengeresponseauthentication|usepam'
grep -nEi 'PermitRootLogin|PasswordAuthentication|PubkeyAuthentication|AuthorizedKeysFile|UsePAM' /etc/ssh/sshd_config 2>/dev/null
"

run_cmd "25. root SSH 公钥" "
ls -lah /root/.ssh 2>/dev/null
stat /root/.ssh /root/.ssh/authorized_keys 2>/dev/null
cat /root/.ssh/authorized_keys 2>/dev/null
"

run_cmd "26. 所有用户 authorized_keys" "
for f in /root/.ssh/authorized_keys /home/*/.ssh/authorized_keys; do
    [ -f \"\$f\" ] || continue
    echo
    echo \"===== \$f =====\"
    stat \"\$f\" 2>/dev/null
    cat \"\$f\" 2>/dev/null
done
"

run_cmd "27. 计划任务排查" "
echo '----- root crontab -----'
crontab -l 2>/dev/null

echo '----- /etc/crontab -----'
cat /etc/crontab 2>/dev/null

echo '----- /etc/cron.d -----'
ls -lah /etc/cron.d 2>/dev/null
for f in /etc/cron.d/*; do
    [ -f \"\$f\" ] && echo \"===== \$f =====\" && cat \"\$f\"
done

echo '----- cron dirs -----'
ls -lah /etc/cron.hourly /etc/cron.daily /etc/cron.weekly /etc/cron.monthly 2>/dev/null
"

run_cmd "28. 可疑自启动服务" "
systemctl list-unit-files --type=service 2>/dev/null | grep enabled
systemctl list-units --type=service --state=running 2>/dev/null
"

run_cmd "29. 最近修改的 systemd 服务文件" "
find /etc/systemd /usr/lib/systemd/system -type f -mtime -30 -printf '%TY-%Tm-%Td %TH:%TM:%TS %u %g %m %p\n' 2>/dev/null | sort
"

run_cmd "30. 网络连接和监听端口" "
ss -tunlp 2>/dev/null
"

run_cmd "31. 进程列表" "
ps auxww
"

run_cmd "32. 可疑 SUID 文件" "
find / -xdev -perm -4000 -type f -printf '%TY-%Tm-%Td %TH:%TM:%TS %u %g %m %p\n' 2>/dev/null | sort
"

run_cmd "33. 最近 15 天新增或修改的 SUID 文件" "
find / -xdev -perm -4000 -type f -mtime -15 -printf '%TY-%Tm-%Td %TH:%TM:%TS %u %g %m %p\n' 2>/dev/null | sort
"

run_cmd "34. 账户清单和 UID 0 用户" "
awk -F: '{print \$1,\$3,\$4,\$6,\$7}' /etc/passwd
echo
echo 'UID 为 0 的用户:'
awk -F: '(\$3==0){print}' /etc/passwd
"

run_cmd "35. 空密码或异常 shadow 账户" "
awk -F: '(\$2==\"\"){print \"空密码账户:\",\$1} (\$2!~/^!|^\\*|^\\$|^!!/){print \"可能可登录账户:\",\$1,\$2}' /etc/shadow 2>/dev/null
"

run_cmd "36. yum 安装历史" "
yum history 2>/dev/null | head -50
"

run_cmd "37. 最近安装或修改的 RPM 包" "
rpm -qa --last 2>/dev/null | head -100
"

run_cmd "38. 关键日志快速时间线（去噪）" "
read_auth | grep -aihE 'password changed for root|chauthtok|Accepted .* for root |Failed password|Invalid user|sudo:|su\\[|useradd|usermod|chage' | grep -avE 'logrotate|was HUPed|imjournal|Deactivated successfully' | tail -1000
"

run_cmd "39. 系统二进制完整性比对 rpm -Va（仅二进制路径）" "
if command -v rpm >/dev/null 2>&1; then
    rpm -Va 2>/dev/null | awk '\$1 ~ /5/ || \$1 ~ /M/ {print}' | grep -E '/(usr/)?(s?bin|lib(64)?|libexec)/'
else
    echo '系统无 rpm，跳过完整性比对'
fi
"

run_cmd "40. 预加载劫持点与免密提权 ld.so.preload / sudoers" "
echo '----- /etc/ld.so.preload (正常应为空) -----'
cat /etc/ld.so.preload 2>/dev/null
echo
echo '----- /etc/ld.so.conf.d -----'
ls -l /etc/ld.so.conf.d/ 2>/dev/null
echo
echo '----- sudoers NOPASSWD -----'
grep -rnE '^[^#]*NOPASSWD' /etc/sudoers /etc/sudoers.d/ 2>/dev/null
echo
echo '----- 各用户 crontab -----'
for cf in /var/spool/cron/* /var/spool/cron/crontabs/*; do [ -f \"\$cf\" ] && echo \"# \$cf\" && cat \"\$cf\"; done
"

run_cmd "41. 隐藏进程检查（ps 对比 /proc）" "
ps -eo pid= 2>/dev/null | tr -d ' ' | sort -u > '${OUT_DIR}/.psp'
for p in /proc/[0-9]*; do basename \"\$p\"; done | sort -u > '${OUT_DIR}/.prp'
echo '/proc 中存在但 ps 看不到的 PID（隐藏进程嫌疑，空=正常）：'
comm -13 '${OUT_DIR}/.psp' '${OUT_DIR}/.prp'
for pid in \$(comm -13 '${OUT_DIR}/.psp' '${OUT_DIR}/.prp'); do
    echo \"--- PID \$pid ---\"
    tr '\\0' ' ' < /proc/\$pid/cmdline 2>/dev/null; echo
    readlink /proc/\$pid/exe 2>/dev/null
done
rm -f '${OUT_DIR}/.psp' '${OUT_DIR}/.prp'
"

run_cmd "42. 凭据泄露扫描（已脱敏）history/.netrc/进程命令行" "
{
    grep -rahoE '(sk-[A-Za-z0-9]{20,}|sshpass -p [^ ]+|AKIA[0-9A-Z]{16}|password=[^ ]+|passwd=[^ ]+|token=[A-Za-z0-9]{16,})' /root/.bash_history /root/.mysql_history /root/.python_history /home/*/.bash_history /root/.netrc /home/*/.netrc 2>/dev/null
    for c in /proc/[0-9]*/cmdline; do tr '\\0' ' ' < \"\$c\" 2>/dev/null | grep -aoE '(sshpass -p [^ ]+|password=[^ ]+|sk-[A-Za-z0-9]{20,})'; done
} | sed -E 's/(sk-[A-Za-z0-9]{4})[A-Za-z0-9]+/\\1****【已脱敏】/g; s/(-p .)[^ ]+/\\1****【已脱敏】/g; s/(AKIA[0-9A-Z]{4})[0-9A-Z]+/\\1****【已脱敏】/g; s/(password=.)[^ ]+/\\1****【已脱敏】/g; s/(passwd=.)[^ ]+/\\1****【已脱敏】/g' | sort -u
"

cat >> "$REPORT" <<EOF

============================================================
39. 初步判断建议
============================================================

1. 如果看到类似：
   passwd: pam_unix(passwd:chauthtok): password changed for root

   说明 root 密码大概率通过 passwd/PAM 流程被修改。

2. 如果 /etc/shadow 的 Modify 时间变化了，但 /var/log/secure* 没有 passwd 改密记录：
   需要怀疑：
   - 攻击者直接编辑 /etc/shadow
   - 攻击者替换 /etc/shadow
   - /var/log/secure 被删除或清理
   - rsyslog/auditd 当时未运行
   - 日志轮转或日志缺失

3. 如果 auditd 有 /etc/shadow 记录：
   优先看 auditd，因为它可能包含执行进程、UID、时间等更强线索。

4. 对已经被攻陷的服务器：
   不建议只改 root 密码后继续使用。
   建议保全现场、导出日志、检查后门、重装系统、恢复业务数据、重新下发密钥。
EOF

echo "正在复制关键原始日志..."

copy_if_exists "/var/log/secure*" "$RAW_DIR"
copy_if_exists "/var/log/messages*" "$RAW_DIR"
copy_if_exists "/var/log/audit/audit.log*" "$RAW_DIR"
copy_if_exists "/var/log/wtmp" "$RAW_DIR"
copy_if_exists "/var/log/btmp" "$RAW_DIR"
copy_if_exists "/var/log/lastlog" "$RAW_DIR"

# 无 secure 文件时，把 journald 认证记录落盘，保证原始证据不丢
if [ "$AUTH_SOURCE" = "journald" ]; then
    read_auth > "$RAW_DIR/journald_auth.log" 2>/dev/null
fi

cp -a /etc/passwd "$RAW_DIR/passwd" 2>/dev/null
cp -a /etc/shadow "$RAW_DIR/shadow" 2>/dev/null
cp -a /etc/group "$RAW_DIR/group" 2>/dev/null
cp -a /etc/gshadow "$RAW_DIR/gshadow" 2>/dev/null
cp -a /etc/ssh/sshd_config "$RAW_DIR/sshd_config" 2>/dev/null

# bash_history 落盘前脱敏，避免打包带走明文 key/密码（chmod 600 限制读取）
SECRET_MASK='/mysql|mariadb|mysqldump/ s/(-p)[^ ]+/\1****【已脱敏】/g; s/(sk-[A-Za-z0-9]{4})[A-Za-z0-9]+/\1****【已脱敏】/g; s/(sshpass -p .)[^ ]+/\1****【已脱敏】/g; s/(AKIA[0-9A-Z]{4})[0-9A-Z]+/\1****【已脱敏】/g; s/(password=.)[^ ]+/\1****【已脱敏】/g; s/(passwd=.)[^ ]+/\1****【已脱敏】/g; s/(token=.)[^ ]+/\1****【已脱敏】/g'
if [ -f /root/.bash_history ]; then
    sed -E "$SECRET_MASK" /root/.bash_history > "$RAW_DIR/root_bash_history" 2>/dev/null
fi
chmod -R go-rwx "$RAW_DIR" 2>/dev/null

generate_html_report
tar -czf "${OUT_DIR}.tar.gz" "$OUT_DIR" 2>/dev/null

echo
echo "排查完成。"
echo "报告目录: $OUT_DIR"
echo "主报告: $REPORT"
echo "压缩包: ${OUT_DIR}.tar.gz"
echo
echo "建议优先查看："
echo "1. $REPORT"
echo "2. $HTML_REPORT"
echo "2. 搜索关键词：password changed for root"
echo "3. 搜索关键词：chauthtok"
echo "4. 搜索关键词：Accepted.*root"
echo "5. 搜索关键词：/etc/shadow"
