今天分享一款Linux运维实用脚本——核心文件修改告警脚本。做运维的都清楚,Linux系统核心配置文件(如/etc/passwd、/etc/shadow)、业务关键文件,一旦误改或恶意篡改,极易导致服务异常、系统瘫痪甚至数据泄露,而我们无法24小时手动监控,因此一款自动化告警工具十分必要。​

这也是我开发该脚本的初衷:用简洁低成本的方式,实现核心文件修改的实时监控告警,解放双手、降低安全风险。​

一、监控背景:为什么需要这款脚本?​

 

核心文件被修改主要分两类场景,均有不小风险:一是运维误操作(如vim编辑失误、cp命令覆盖),二是服务器入侵后的恶意篡改(如篡改密码文件、植入恶意代码)。​

传统手动检查、定期对比MD5的方式,效率低、无实时性,无法及时发现风险,这款轻量化脚本正是为解决该痛点而生。​

二、解决方案:核心文件修改告警脚本​

 

脚本核心思路:定期计算核心文件MD5值,与历史MD5值对比,若有变化(文件被修改),立即向指定联系人发送告警。​

脚本无额外依赖(用Linux自带命令),可自定义监控文件、频率,支持邮件等告警方式,部署简单,适配所有主流Linux系统。

1. 脚本核心逻辑

全程自动化,无需人工干预,核心分4步:

(1)初始化配置:定义监控文件列表、监控频率、告警接收人等;

(2)首次校验:计算监控文件MD5值,保存为历史对比基准;

(3)定期对比:按设定频率重新计算MD5,与历史值比对;

(4)触发告警:MD5值变化即判定文件被修改,发送告警(含文件路径、修改时间、服务器IP),并更新历史MD5避免重复告警。

2. 脚本优势

核心3个优势,适配日常运维:

(1)轻量化无依赖,部署即用;(2)高度可自定义,适配不同需求;(3)实时性强、误报率低,无遗漏风险。

3. 部署与使用(3步上手)

(1)上传脚本至服务器指定目录;(2)编辑脚本,修改监控、告警相关配置;(3)通过crontab设置自动运行,实现24小时监控。

脚本代码

#!/bin/bash
# sysguard - 关键文件监控(智能防风暴版)
# 作者:锦轩
# 日期:2025-11-20
# 特点:核心文件监控 + 智能防风暴 + 完整性检查

set -euo pipefail

# ==============================================================================
## ===== 【请在此处修改钉钉机器人的token配置】=====
# ==============================================================================
DINGTALK_WEBHOOK="https://oapi.dingtalk.com/robot/send?access_token=xxxxxxxxxx"

# 核心监控文件(分级监控)
declare -a CRITICAL_FILES=(
    "/etc/passwd"
    "/etc/shadow"
    "/etc/group"
    "/etc/gshadow"
    "/etc/sudoers"
    "/etc/ssh/sshd_config"
)

declare -a IMPORTANT_FILES=(
    "/etc/hosts.allow"
    "/etc/hosts.deny"
    "/etc/hosts"
    "/etc/systemd/system/*.service"
    "/etc/rc.local"
    "/etc/crontab"
    "/etc/cron.d/*"
    "/var/spool/cron/*"
    "/root/.ssh/authorized_keys"
)

# 防风暴配置
ALERT_COOLDOWN=300  # 相同文件5分钟内不重复告警
MAX_ALERTS_PER_HOUR=20  # 每小时最大告警数
# ==============================================================================

log() { echo "[$(date '+%F %T %Z')] [INFO] $*" >&2; }
err() { echo "[$(date '+%F %T %Z')] [ERROR] $*" >&2; exit 1; }

# ========== 清理旧版本 ==========
cleanup_old() {
    log "🔍 检测并清理旧版本..."
    
    # 停止服务
    if systemctl is-active --quiet sysguard 2>/dev/null; then
        log "  停止旧服务"
        sudo systemctl stop sysguard || true
    fi
    
    # 禁用服务
    if systemctl is-enabled --quiet sysguard 2>/dev/null; then
        sudo systemctl disable sysguard || true
    fi
    
    # 删除服务文件
    sudo rm -f /etc/systemd/system/sysguard.service
    
    # 删除脚本
    sudo rm -f /usr/local/bin/sysguard.sh
    
    # 删除数据
    sudo rm -rf /var/lib/sysguard/
    sudo rm -f /var/log/sysguard.log
    
    # 重载 systemd
    sudo systemctl daemon-reload 2>/dev/null || true
    
    log "✅ 旧版本已清理"
}

# ========== 安装依赖 ==========
install_deps() {
    log "📦 安装依赖..."
    if command -v apt-get >/dev/null; then
        export DEBIAN_FRONTEND=noninteractive
        sudo apt-get update -qq
        sudo apt-get install -y -qq inotify-tools jq curl
    elif command -v dnf >/dev/null; then
        sudo dnf install -y inotify-tools jq curl
    elif command -v yum >/dev/null; then
        sudo yum install -y inotify-tools jq curl
    else
        err "不支持的系统"
    fi
    log "✅ 依赖就绪"
}

# ========== 生成监控脚本 ==========
generate_monitor_script() {
    log "📝 生成监控脚本..."
    
    # 创建临时脚本文件
    cat > /tmp/sysguard.sh << 'TEMPLATE_EOF'
#!/bin/bash
set -eo pipefail

# 配置
DINGTALK_WEBHOOK="DINGTALK_WEBHOOK_PLACEHOLDER"
LOG_FILE='/var/log/sysguard.log'
STATE_FILE='/var/lib/sysguard/state'
ALERT_COOLDOWN=ALERT_COOLDOWN_PLACEHOLDER
MAX_ALERTS_PER_HOUR=MAX_ALERTS_PER_HOUR_PLACEHOLDER

# 文件分类
CRITICAL_FILES=(
CRITICAL_FILES_PLACEHOLDER
)

IMPORTANT_FILES=(
IMPORTANT_FILES_PLACEHOLDER
)

# 合并所有监控文件
MONITORED_FILES=("${CRITICAL_FILES[@]}" "${IMPORTANT_FILES[@]}")

# 防风暴变量
ALERT_COUNT=0
ALERT_RESET_TIME=$(date +%s)
LAST_ALERTS=()

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

# 检查是否在冷却期内
in_cooldown() {
    local file="$1"
    local current_time=$(date +%s)
    
    # 安全地检查数组
    if [[ ${#LAST_ALERTS[@]} -eq 0 ]]; then
        return 1
    fi
    
    for alert in "${LAST_ALERTS[@]}"; do
        IFS='|' read -r alert_file alert_time <<< "$alert"
        if [[ "$alert_file" == "$file" ]]; then
            if (( current_time - alert_time < ALERT_COOLDOWN )); then
                return 0
            fi
        fi
    done
    return 1
}

# 更新告警记录
update_alert_record() {
    local file="$1"
    local current_time=$(date +%s)
    
    # 清理过期记录
    local new_records=()
    if [[ ${#LAST_ALERTS[@]} -gt 0 ]]; then
        for alert in "${LAST_ALERTS[@]}"; do
            IFS='|' read -r alert_file alert_time <<< "$alert"
            if (( current_time - alert_time < ALERT_COOLDOWN )); then
                new_records+=("$alert")
            fi
        done
    fi
    
    # 添加新记录
    new_records+=("$file|$current_time")
    LAST_ALERTS=("${new_records[@]}")
    
    # 更新告警计数
    ALERT_COUNT=$((ALERT_COUNT + 1))
    local current_hour=$(date +%H)
    if [[ "$current_hour" != "$ALERT_HOUR" ]]; then
        ALERT_COUNT=1
        ALERT_HOUR="$current_hour"
    elif [[ $ALERT_COUNT -ge $MAX_ALERTS_PER_HOUR ]]; then
        log "⚠️ 达到每小时告警上限,暂停告警"
        return 1
    fi
    return 0
}

# 文件完整性检查
check_integrity() {
    local file="$1"
    local baseline_file="/var/lib/sysguard/baseline/$(basename "$file").baseline"
    
    if [[ -f "$baseline_file" ]]; then
        local current_sha=$(sha256sum "$file" 2>/dev/null | awk '{print $1}')
        local baseline_sha=$(awk '{print $1}' "$baseline_file" 2>/dev/null)
        
        if [[ "$current_sha" != "$baseline_sha" ]]; then
            log "🚨 文件完整性破坏: $file (SHA256变更)"
            return 1
        fi
    fi
    return 0
}

# 初始化基线
init_baseline() {
    mkdir -p /var/lib/sysguard/baseline
    for file in "${MONITORED_FILES[@]}"; do
        if [[ -f "$file" ]]; then
            sha256sum "$file" > "/var/lib/sysguard/baseline/$(basename "$file").baseline" 2>/dev/null || true
        fi
    done
    log "初始化文件完整性基线"
}

send_alert() {
    local file="$1" event="$2" sha256="$3"
    
    # 防风暴检查
    if in_cooldown "$file"; then
        log "⏳ 跳过冷却期内文件: $file"
        return 0
    fi
    
    if ! update_alert_record "$file"; then
        return 0
    fi
    
    local hn=$(hostname -s 2>/dev/null || echo "unknown")
    local ip=$(ip -4 route get 8.8.8.8 2>/dev/null | awk 'NR==1 {print $7; exit}' 2>/dev/null || echo "127.0.0.1")
    local ts=$(date '+%Y-%m-%d %H:%M:%S %Z')

    # 确定告警级别
    local severity="⚠️"
    local title="文件变更告警"
    for critical_file in "${CRITICAL_FILES[@]}"; do
        if [[ "$file" == "$critical_file" ]]; then
            severity="🚨"
            title="[CRITICAL] 核心文件变更"
            break
        fi
    done

    # 使用 jq 构建 JSON
    local payload
    payload=$(jq -nc \
        --arg hn "$hn" \
        --arg ip "$ip" \
        --arg ts "$ts" \
        --arg file "$file" \
        --arg event "$event" \
        --arg sha256 "$sha256" \
        --arg title "$title" \
        --arg severity "$severity" \
        '{
            "msgtype": "markdown",
            "markdown": {
                "title": $title,
                "text": (
                    "### " + $severity + " 系统关键文件变更告警\n" +
                    "- **主机**: `" + $hn + "` (" + $ip + ")\n" +
                    "- **时间**: `" + $ts + "`\n" +
                    "- **文件**: `" + $file + "`\n" +
                    "- **事件**: `" + $event + "`\n" +
                    "- **SHA256**: `" + $sha256 + "`\n" +
                    "> 🛡️ 请立即核查是否为未授权变更!\n\n" +
                    "@所有人"
                )
            },
            "at": {
                "isAtAll": true
            }
        }')

    local i=0
    while (( i < 3 )); do
        response=$(curl -sS --max-time 10 \
            -H "Content-Type: application/json" \
            -d "$payload" \
            "$DINGTALK_WEBHOOK" 2>&1) || response="curl_failed"

        if [[ $response == *"\"errcode\":0"* ]]; then
            log "✅ 钉钉告警成功: $file"
            return 0
        fi
        sleep $((++i))
    done
    log "❌ 告警失败: $file"
    return 1
}

main() {
    mkdir -p /var/lib/sysguard /var/log
    touch "$STATE_FILE" "$LOG_FILE"
    chmod 600 "$STATE_FILE" "$LOG_FILE"
    
    # 初始化变量
    ALERT_HOUR=$(date +%H)
    ALERT_COUNT=0
    LAST_ALERTS=()
    
    # 初始化基线
    init_baseline
    
    log "sysguard 启动,监控 ${#MONITORED_FILES[@]} 个文件"
    log "核心文件: ${#CRITICAL_FILES[@]} 个,重要文件: ${#IMPORTANT_FILES[@]} 个"

    # 构建实际存在的文件列表
    local valid_files=()
    for f in "${MONITORED_FILES[@]}"; do
        if [[ -e "$f" ]]; then
            valid_files+=("$f")
        elif [[ "$f" == *"*"* ]]; then
            # 处理通配符
            for expanded_file in $f; do
                if [[ -e "$expanded_file" ]]; then
                    valid_files+=("$expanded_file")
                fi
            done
        fi
    done

    log "实际监控文件: ${#valid_files[@]} 个"

    inotifywait -m -q --format '%e %w%f' \
        -e modify,attrib,move_self,moved_to,create,delete \
        "${valid_files[@]}" 2>/dev/null | while read -r ev f; do
        
        # 防误报:跳过临时文件和锁文件
        if [[ "$f" =~ \.(swp|swx|tmp)$ ]] || [[ "$f" =~ \.(lock|LOCK)$ ]]; then
            continue
        fi
        
        now=$(date +%s)
        last=$(stat -c %Y "$STATE_FILE" 2>/dev/null || echo 0)
        ((now - last < 3)) && continue
        touch "$STATE_FILE"

        # 完整性检查
        if ! check_integrity "$f"; then
            sha=$(sha256sum "$f" 2>/dev/null | awk '{print $1}' || echo "N/A")
            log "🚨 变更: $f | $ev | $sha"
            send_alert "$f" "$ev" "$sha" &
        else
            log "📝 正常变更: $f | $ev"
        fi
    done
}

main
TEMPLATE_EOF

    # 安全地替换占位符
    log "  配置监控脚本参数..."
    
    # 替换简单参数
    sed -i "s|DINGTALK_WEBHOOK_PLACEHOLDER|$DINGTALK_WEBHOOK|g" /tmp/sysguard.sh
    sed -i "s|ALERT_COOLDOWN_PLACEHOLDER|$ALERT_COOLDOWN|g" /tmp/sysguard.sh
    sed -i "s|MAX_ALERTS_PER_HOUR_PLACEHOLDER|$MAX_ALERTS_PER_HOUR|g" /tmp/sysguard.sh
    
    # 替换文件列表 - 使用安全的临时文件方法
    local critical_temp=$(mktemp)
    local important_temp=$(mktemp)
    
    for f in "${CRITICAL_FILES[@]}"; do
        echo "    \"$f\"" >> "$critical_temp"
    done
    
    for f in "${IMPORTANT_FILES[@]}"; do
        echo "    \"$f\"" >> "$important_temp"
    done
    
    # 使用sed从文件读取替换内容
    sed -i "/CRITICAL_FILES_PLACEHOLDER/{
        r $critical_temp
        d
    }" /tmp/sysguard.sh
    
    sed -i "/IMPORTANT_FILES_PLACEHOLDER/{
        r $important_temp
        d
    }" /tmp/sysguard.sh
    
    # 清理临时文件
    rm -f "$critical_temp" "$important_temp"
    
    # 安装脚本
    sudo install -m 700 -o root -g root /tmp/sysguard.sh /usr/local/bin/sysguard.sh
    rm -f /tmp/sysguard.sh
    log "✅ 监控脚本部署完成"
}

# ========== 生成服务 ==========
generate_service() {
    log "🔧 生成系统服务..."
    cat > /tmp/sysguard.service << 'EOF'
[Unit]
Description=System File Monitor
After=network.target

[Service]
Type=simple
User=root
ExecStart=/usr/local/bin/sysguard.sh
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target
EOF

    sudo install -m 644 /tmp/sysguard.service /etc/systemd/system/sysguard.service
    rm -f /tmp/sysguard.service
    sudo systemctl daemon-reload
    sudo systemctl enable --now sysguard
    log "✅ 服务已启动"
}

# ========== 主流程 ==========
main() {
    log "🚀 开始部署 sysguard(智能防风暴版)..."
    cleanup_old
    install_deps
    generate_monitor_script
    generate_service

    cat << EOF

✅ 部署完成!

📊 监控配置:
- 核心文件: ${#CRITICAL_FILES[@]} 个(实时告警)
- 重要文件: ${#IMPORTANT_FILES[@]} 个(智能告警)
- 防风暴机制: ${ALERT_COOLDOWN}秒冷却期,每小时最多${MAX_ALERTS_PER_HOUR}条告警

🔧 下一步:
1. 【关键】锁定脚本防篡改:
   sudo chattr +i /usr/local/bin/sysguard.sh

2. 测试告警(仅测试环境!):
   echo "# \$(date) test" | sudo tee -a /etc/hosts.allow

3. 查看状态:
   sudo systemctl status sysguard

4. 实时日志:
   journalctl -u sysguard -f

🛡️ 已启用文件完整性检查和智能防风暴机制!
EOF
}

# 运行主函数
main

 

 

文章作者: 锦轩
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 锦轩个人小站
操作系统 linux
喜欢就支持一下吧
打赏
微信 微信
支付宝 支付宝