#!/usr/bin/env bash
set -uo pipefail

VERSION="2.3"
REPORT_DIR="/root/image_security_report_$(date +%Y%m%d_%H%M%S)"
SUMMARY_FILE="$REPORT_DIR/summary.txt"
DETAIL_FILE="$REPORT_DIR/detail.log"
CLAM_REPORT="$REPORT_DIR/clamav.log"
RKH_REPORT="$REPORT_DIR/rkhunter.log"
SUSPICIOUS_FILE="$REPORT_DIR/suspicious_items.log"

mkdir -p "$REPORT_DIR"
: > "$DETAIL_FILE"
: > "$SUSPICIOUS_FILE"

PASS_COUNT=0
FAIL_COUNT=0
ERROR_COUNT=0

log() {
    echo "[$(date '+%F %T')] $*" | tee -a "$DETAIL_FILE"
}

add_pass() {
    PASS_COUNT=$((PASS_COUNT + 1))
    echo "[PASS] $*" | tee -a "$DETAIL_FILE"
}

add_fail() {
    FAIL_COUNT=$((FAIL_COUNT + 1))
    echo "[FAIL] $*" | tee -a "$DETAIL_FILE"
}

add_error() {
    ERROR_COUNT=$((ERROR_COUNT + 1))
    echo "[ERROR] $*" | tee -a "$DETAIL_FILE"
}

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

detect_os() {
    if [ -r /etc/os-release ]; then
        . /etc/os-release
        OS_ID="${ID:-unknown}"
        OS_LIKE="${ID_LIKE:-}"
    else
        OS_ID="unknown"
        OS_LIKE=""
    fi
}

install_tools() {
    log "开始安装检测工具"
    detect_os

    if command_exists apt-get; then
        export DEBIAN_FRONTEND=noninteractive
        apt-get update >>"$DETAIL_FILE" 2>&1 || return 1
        apt-get install -y clamav clamav-freshclam rkhunter procps iproute2 findutils file cron >>"$DETAIL_FILE" 2>&1 || return 1
    elif command_exists dnf; then
        dnf install -y epel-release >>"$DETAIL_FILE" 2>&1 || true
        dnf install -y clamav clamav-update rkhunter procps-ng iproute findutils file cronie >>"$DETAIL_FILE" 2>&1 || return 1
    elif command_exists yum; then
        yum install -y epel-release >>"$DETAIL_FILE" 2>&1 || true
        yum install -y clamav clamav-update rkhunter procps-ng iproute findutils file cronie >>"$DETAIL_FILE" 2>&1 || return 1
    else
        return 1
    fi
}

update_signatures() {
    log "更新 ClamAV 病毒库"
    if command_exists freshclam; then
        freshclam >>"$DETAIL_FILE" 2>&1
        rc=$?
        if [ "$rc" -eq 0 ]; then
            add_pass "ClamAV 病毒库更新成功"
        else
            add_error "ClamAV 病毒库更新失败或未完全更新"
        fi
    else
        add_error "未找到 freshclam"
    fi

    log "更新 RKHunter 数据文件"
    if command_exists rkhunter; then
        rkhunter --update >>"$DETAIL_FILE" 2>&1
        rc=$?
        if [ "$rc" -eq 0 ] || [ "$rc" -eq 2 ]; then
            add_pass "RKHunter 数据更新完成"
        else
            add_error "RKHunter 数据更新异常"
        fi
    else
        add_error "未找到 rkhunter"
    fi
}

run_clamav() {
    log "执行 ClamAV 全盘扫描"
    if ! command_exists clamscan; then
        add_error "ClamAV 未安装，无法执行病毒扫描"
        return
    fi

    clamscan -r -i \
        --exclude-dir='^/proc' \
        --exclude-dir='^/sys' \
        --exclude-dir='^/dev' \
        --exclude-dir='^/run' \
        --exclude-dir='^/mnt' \
        --exclude-dir='^/media' \
        --exclude-dir="$REPORT_DIR" \
        / >"$CLAM_REPORT" 2>&1
    rc=$?

    infected=$(awk -F: '/Infected files:/ {gsub(/ /,"",$2); print $2}' "$CLAM_REPORT" | tail -1)
    errors=$(awk -F: '/Total errors:/ {gsub(/ /,"",$2); print $2}' "$CLAM_REPORT" | tail -1)
    infected=${infected:-0}
    errors=${errors:-0}

    if [ "$rc" -eq 1 ] || [ "$infected" -gt 0 ] 2>/dev/null; then
        add_fail "ClamAV 发现感染文件：$infected 个"
    elif [ "$rc" -eq 2 ] || [ "$errors" -gt 0 ] 2>/dev/null; then
        add_error "ClamAV 扫描出现错误：$errors 个，请查看 $CLAM_REPORT"
    elif [ "$rc" -eq 0 ]; then
        add_pass "ClamAV 未发现感染文件"
    else
        add_error "ClamAV 扫描结果无法判断，退出码：$rc"
    fi
}

run_rkhunter() {
    log "执行 RKHunter 检测"
    if ! command_exists rkhunter; then
        add_error "RKHunter 未安装，无法执行 Rootkit 检测"
        return
    fi

    rkhunter --check --skip-keypress --report-warnings-only >"$RKH_REPORT" 2>&1
    rc=$?
    warnings=$(grep -Eic '(^|[[:space:]])Warning:|warning found|warnings found' "$RKH_REPORT" 2>/dev/null || true)

    if [ "$warnings" -gt 0 ]; then
        add_fail "RKHunter 发现 $warnings 条异常警告，请查看报告"
    elif [ "$rc" -eq 0 ] || [ "$rc" -eq 1 ]; then
        add_pass "RKHunter 未发现明确 Rootkit 告警"
    else
        add_error "RKHunter 返回异常退出码：$rc，请查看报告"
    fi
}

check_persistence() {
    log "检查持久化后门"

    local found=0
    local out="$REPORT_DIR/persistence.log"
    : > "$out"

    if [ -s /etc/ld.so.preload ]; then
        echo "发现非空文件：/etc/ld.so.preload" >>"$out"
        cat /etc/ld.so.preload >>"$out"
        found=1
    fi

    grep -RsnEI --binary-files=without-match \
        '(curl|wget).*(\||;).*(sh|bash)|/dev/tcp/|bash[[:space:]]+-i|nc[[:space:]].*-e|base64[[:space:]]+-d|/tmp/|/var/tmp/|/dev/shm/' \
        /etc/rc.local /etc/profile /etc/profile.d /root/.bashrc /etc/bashrc /etc/cron.d /etc/crontab \
        2>/dev/null >>"$out" || true

    [ -s "$out" ] && found=1

    if [ "$found" -eq 1 ]; then
        add_fail "发现可疑持久化配置，请查看 $out"
    else
        add_pass "未发现明显持久化后门配置"
    fi
}

check_accounts_ssh() {
    log "检查 SSH 公钥和异常账号"

    local out="$REPORT_DIR/accounts_ssh.log"
    : > "$out"

    awk -F: '$3==0 {print "UID=0账号: "$1}' /etc/passwd >>"$out"
    uid0_count=$(awk -F: '$3==0 {c++} END {print c+0}' /etc/passwd)

    awk -F: '($2=="" || $2=="!") {print "空密码或未设置密码字段: "$1}' /etc/shadow 2>/dev/null >>"$out" || true

    find /root /home -xdev -type f -path '*/.ssh/authorized_keys' -print -exec sed 's/^/  /' {} \; \
        >>"$out" 2>/dev/null || true

    grep -E '^[[:space:]]*(PermitRootLogin|PasswordAuthentication|PermitEmptyPasswords|AuthorizedKeysCommand)' \
        /etc/ssh/sshd_config 2>/dev/null >>"$out" || true

    if [ "$uid0_count" -gt 1 ]; then
        add_fail "发现多个 UID=0 账号，请查看 $out"
    elif grep -Eq '^PermitEmptyPasswords[[:space:]]+yes' /etc/ssh/sshd_config 2>/dev/null; then
        add_fail "SSH 配置允许空密码登录"
    elif find /root /home -xdev -type f -path '*/.ssh/authorized_keys' -size +0c 2>/dev/null | grep -q .; then
        add_fail "镜像中存在 authorized_keys，请查看 $out"
    else
        add_pass "未发现明显异常账号或 SSH 后门"
    fi
}

check_tmp_files() {
    log "检查临时目录可疑文件"

    local out="$REPORT_DIR/tmp_suspicious.log"
    : > "$out"

    for d in /tmp /var/tmp /dev/shm; do
        [ -d "$d" ] || continue
        find "$d" -xdev -type f \( -perm /111 -o -name '.*' \) -printf '%m %u %g %TY-%Tm-%Td %TH:%TM %p\n' \
            >>"$out" 2>/dev/null || true
    done

    if [ -s "$out" ]; then
        add_fail "临时目录发现可执行文件或隐藏文件，请查看 $out"
    else
        add_pass "临时目录未发现明显可疑文件"
    fi
}

check_suid_sgid() {
    log "检查 SUID/SGID 文件"

    local out="$REPORT_DIR/suid_sgid.log"
    : > "$out"

    find / -xdev -type f \( -perm -4000 -o -perm -2000 \) \
        -printf '%m %u %g %p\n' 2>/dev/null | sort >"$out"

    suspicious=$(awk '$4 ~ /^\/(tmp|var\/tmp|dev\/shm|opt|usr\/local)\// {print}' "$out")
    if [ -n "$suspicious" ]; then
        printf '%s\n' "$suspicious" >>"$SUSPICIOUS_FILE"
        add_fail "在非常规目录发现 SUID/SGID 文件，请查看 $out"
    else
        add_pass "未在非常规目录发现 SUID/SGID 文件"
    fi
}

check_systemd() {
    log "检查 systemd 异常服务"

    local out="$REPORT_DIR/systemd_services.log"
    : > "$out"

    grep -RsnEI --include='*.service' \
        'Exec(Start|StartPre|StartPost)=.*(/tmp/|/var/tmp/|/dev/shm/|curl|wget|/dev/tcp/|bash[[:space:]]+-i|nc[[:space:]].*-e|base64)' \
        /etc/systemd/system /usr/lib/systemd/system /lib/systemd/system \
        2>/dev/null >>"$out" || true

    find /etc/systemd/system -type f -name '*.service' -print >>"$out" 2>/dev/null || true

    if grep -Eq 'Exec(Start|StartPre|StartPost)=' "$out"; then
        add_fail "发现可疑 systemd 执行项，请查看 $out"
    else
        add_pass "未发现明显可疑 systemd 执行项"
    fi
}

check_cron() {
    log "检查 Cron 异常任务"

    local out="$REPORT_DIR/cron.log"
    : > "$out"

    grep -RsnEI --binary-files=without-match \
        '(curl|wget).*(\||;).*(sh|bash)|/dev/tcp/|bash[[:space:]]+-i|nc[[:space:]].*-e|base64[[:space:]]+-d|/tmp/|/var/tmp/|/dev/shm/' \
        /etc/crontab /etc/cron.d /etc/cron.hourly /etc/cron.daily /etc/cron.weekly /etc/cron.monthly \
        /var/spool/cron /var/spool/cron/crontabs 2>/dev/null >>"$out" || true

    if [ -s "$out" ]; then
        add_fail "发现可疑 Cron 任务，请查看 $out"
    else
        add_pass "未发现明显可疑 Cron 任务"
    fi
}

check_process_ports() {
    log "检查监听端口和可疑进程"

    local out="$REPORT_DIR/process_ports.log"
    : > "$out"

    echo "===== 监听端口 =====" >>"$out"
    ss -lntup 2>/dev/null >>"$out" || true

    echo >>"$out"
    echo "===== 已删除但仍运行的文件 =====" >>"$out"
    if command_exists lsof; then
        lsof +L1 2>/dev/null >>"$out" || true
    else
        find /proc/[0-9]*/exe -lname '* (deleted)' -printf '%p -> %l\n' 2>/dev/null >>"$out" || true
    fi

    echo >>"$out"
    echo "===== 从临时目录运行的进程 =====" >>"$out"
    for exe in /proc/[0-9]*/exe; do
        target=$(readlink "$exe" 2>/dev/null || true)
        case "$target" in
            /tmp/*|/var/tmp/*|/dev/shm/*)
                echo "$exe -> $target" >>"$out"
                ;;
        esac
    done

    if grep -Eq ' \(deleted\)$| -> /(tmp|var/tmp|dev/shm)/' "$out"; then
        add_fail "发现已删除仍运行或从临时目录启动的进程，请查看 $out"
    else
        add_pass "未发现明显可疑运行进程；监听端口已记录"
    fi
}

write_summary() {
    local result exit_code

    if [ "$ERROR_COUNT" -gt 0 ]; then
        result="ERROR"
        exit_code=3
    elif [ "$FAIL_COUNT" -gt 0 ]; then
        result="FAIL"
        exit_code=2
    else
        result="PASS"
        exit_code=0
    fi

    {
        echo "========================================"
        echo "Linux 新镜像安全检测报告 v$VERSION"
        echo "========================================"
        echo "检测时间：$(date '+%F %T')"
        echo "主机名称：$(hostname)"
        echo "系统版本：$(. /etc/os-release 2>/dev/null; echo "${PRETTY_NAME:-unknown}")"
        echo
        echo "最终结果：$result"
        echo "PASS：$PASS_COUNT"
        echo "FAIL：$FAIL_COUNT"
        echo "ERROR：$ERROR_COUNT"
        echo
        echo "结果说明："
        echo "PASS             未发现明确安全风险"
        echo "FAIL             发现感染文件或明确高风险配置"
        echo "ERROR            检测工具安装失败或检测未完整执行"
        echo
        echo "详细报告目录：$REPORT_DIR"
        echo "========================================"
    } | tee "$SUMMARY_FILE"

    exit "$exit_code"
}

main() {
    START_TIME=$(date +%s)

    log "Linux 新镜像安全检测 v$VERSION 开始"

    if ! install_tools; then
        add_error "检测工具安装失败"
        write_summary
    fi

    update_signatures
    run_clamav
    run_rkhunter
    check_persistence
    check_accounts_ssh
    check_tmp_files
    check_suid_sgid
    check_systemd
    check_cron
    check_process_ports

    END_TIME=$(date +%s)
    ELAPSED=$((END_TIME - START_TIME))
    log "检测总耗时：$((ELAPSED / 60)) 分 $((ELAPSED % 60)) 秒"

    write_summary
}

main "$@"
