shell脚本监控ssl证书到期时间

一、需求

说明:

  (1)读取域名列表文件。

  (2)获取域名到期时间,进行告警后邮件提醒。

#!/bin/bash
## 第1步  配置文件
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
PURPLE='\033[0;35m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
# 告警阈值(天)
WARNING_DAYS=20
CRITICAL_DAYS=10
ALERT_DAYS=10
# 超时设置(秒)
CONNECT_TIMEOUT=5
# 邮件配置
EMAIL_ENABLED=false
EMAIL_TO="xxxxxxx@qq.com"     # 目标邮箱地址
EMAIL_COMMAND="mailx"             # 邮件发送命令
EMAIL_SUBJECT_PREFIX="SSL证书检查报告" # 邮件主题前缀
# 全局变量
ALERT_DOMAINS=()  # 存储需要告警的域名
ALL_CERTIFICATES=()  # 存储所有证书检查结果
DOMAIN_LIST_FILE="domains.txt"

# 日志函数
log() {
    local level=$1
    local message=$2
    local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
    echo -e "[$timestamp] [$level] $message"
}
## 第4步 
# 显示使用帮助
show_usage() {
    echo -e "${GREEN}SSL证书到期监控脚本${NC}"
    echo "使用方法: $0 [选项]"
    echo ""
    echo "选项:"
    echo "  -f, --file <文件>    从文件读取域名列表 (默认: $DOMAIN_LIST_FILE)"
    echo "  -d, --detailed       显示详细证书信息"
    echo "  -q, --quick          快速检查模式(默认)"
    echo "  -t, --timeout <秒>   设置连接超时时间(默认: ${CONNECT_TIMEOUT}秒)"
    echo "  -m, --mail           启用邮件发送功能"
    echo "  -h, --help           显示此帮助信息"
    echo ""
    echo "示例:"
    echo "  $0 -f domains.txt        # 从文件检查"
    echo "  $0 -f domains.txt -d     # 详细模式检查"
    echo "  $0 -f domains.txt -m     # 检查并发送邮件"
    echo ""
    echo "域名列表文件格式:"
    echo "  # 注释行"
    echo "  example.com"
    echo "  google.com"
    echo "  mysite.com:8443"
}
## 第9步 
# 带超时的SSL连接检查
ssl_connect_with_timeout() {
    local domain=$1
    local port=$2
    
    # 使用timeout命令设置超时
    if command -v timeout >/dev/null 2>&1; then
        # 如果有timeout命令
        timeout $CONNECT_TIMEOUT bash -c "
            echo | openssl s_client -connect \"$domain:$port\" -servername \"$domain\" 2>/dev/null
        " 2>/dev/null
    else
        # 如果没有timeout命令,使用其他方法实现超时
        local pid
        # local result
        
        # 在后台执行openssl命令
        (echo | openssl s_client -connect "$domain:$port" -servername "$domain" 2>/dev/null) &
        pid=$!
        
        # 等待进程结束,最多等待CONNECT_TIMEOUT秒
        local count=0
        while [ $count -lt $CONNECT_TIMEOUT ]; do
            if ! kill -0 $pid 2>/dev/null; then
                # 进程已经结束
                break
            fi
            sleep 1
            count=$((count + 1))
        done
        
        # 如果进程还在运行,杀死它
        if kill -0 $pid 2>/dev/null; then
            kill $pid 2>/dev/null
            wait $pid 2>/dev/null
            return 124  # 超时返回码
        else
            # 获取命令执行结果
            wait $pid
            return $?
        fi
    fi
}

## 第8步 
# 获取SSL证书剩余天数
get_ssl_days_remaining() {
    local domain=$1
    local port=${2:-443}
    
    # 获取证书到期时间(带超时)
    local not_after
    not_after=$(ssl_connect_with_timeout "$domain" "$port" | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
    
    local ssl_result=$?
    
    if [ $ssl_result -eq 124 ]; then
        echo "TIMEOUT"
        return 1
    elif [ $ssl_result -ne 0 ] || [ -z "$not_after" ]; then
        echo "ERROR"
        return 1
    fi
    
    # 转换为时间戳
    local expiry_timestamp
    expiry_timestamp=$(date -d "$not_after" +%s 2>/dev/null)
    if [ $? -ne 0 ]; then
        echo "ERROR"
        return 1
    fi
    
    local current_timestamp=$(date +%s)
    local days_remaining=$(( (expiry_timestamp - current_timestamp) / 86400 ))
    
    echo "$days_remaining"
}

## 第7步 
# 快速检查模式(只检查剩余天数)
quick_check_domain() {
    local domain=$1
    local port=${2:-443}
    local days_remaining=$(get_ssl_days_remaining "$domain" "$port")
    
    if [ "$days_remaining" == "TIMEOUT" ]; then
        echo -e "${RED}⏰ $domain: 连接超时 (${CONNECT_TIMEOUT}秒)${NC}"
        ALERT_DOMAINS+=("${RED}⏰ $domain: 连接超时${NC}")
        ALL_CERTIFICATES+=("${RED}⏰ $domain: 连接超时${NC}")
    elif [ "$days_remaining" == "ERROR" ]; then
        echo -e "${RED}❌ $domain: 检查失败${NC}"
        ALERT_DOMAINS+=("${RED}❌ $domain: 检查失败${NC}")
        ALL_CERTIFICATES+=("${RED}❌ $domain: 检查失败${NC}")
    elif [ "$days_remaining" -lt 0 ]; then
        echo -e "${RED}🔴 $domain: 已过期 $(( -days_remaining )) 天!${NC}"
        ALERT_DOMAINS+=("${RED}🔴 $domain: 已过期 $(( -days_remaining )) 天${NC}")
        ALL_CERTIFICATES+=("${RED}🔴 $domain: 已过期 $(( -days_remaining )) 天${NC}")
    elif [ "$days_remaining" -le "$ALERT_DAYS" ]; then
        echo -e "${RED}🔴 $domain: 剩余 $days_remaining 天 (需处理)${NC}"
        ALERT_DOMAINS+=("${RED}🔴 $domain: 剩余 $days_remaining 天${NC}")
        ALL_CERTIFICATES+=("${RED}🔴 $domain: 剩余 $days_remaining 天${NC}")
    elif [ "$days_remaining" -le "$WARNING_DAYS" ]; then
        echo -e "${YELLOW}🟡 $domain: 剩余 $days_remaining 天${NC}"
        ALL_CERTIFICATES+=("${YELLOW}🟡 $domain: 剩余 $days_remaining 天${NC}")
    else
        echo -e "${GREEN}✅ $domain: 剩余 $days_remaining 天${NC}"
        ALL_CERTIFICATES+=("${GREEN}✅ $domain: 剩余 $days_remaining 天${NC}")
    fi
}

## 第6步 
# 从文件读取域名列表
read_domains_from_file() {
    local file_path=$1
    local domains=()
    
    if [ ! -f "$file_path" ]; then
        echo -e "${RED}错误: 域名列表文件 $file_path 不存在${NC}" >&2
        return 1
    fi
    
    while IFS= read -r line || [ -n "$line" ]; do
        # 跳过空行和注释行
        line=$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
        [[ -z "$line" || "$line" =~ ^[[:space:]]*# ]] && continue
        
        domains+=("$line")
    done < "$file_path"
    
    printf '%s\n' "${domains[@]}"
}

## 第5步 
# 批量检查域名
check_multiple_domains() {
    local mode=$1
    local file_path=$2
    local domains=()
    
    # 从文件读取域名
    if [ -n "$file_path" ] && [ -f "$file_path" ]; then
        mapfile -t domains < <(read_domains_from_file "$file_path")
        if [ $? -ne 0 ]; then
            echo -e "${RED}错误: 无法读取域名列表文件${NC}"
            return 1
        fi
    else
        echo -e "${RED}错误: 域名列表文件不存在${NC}"
        return 1
    fi
    
    echo -e "${BLUE}开始检查 ${#domains[@]} 个域名的SSL证书...${NC}"
    echo "告警阈值: ${ALERT_DAYS}天"
    echo "超时设置: ${CONNECT_TIMEOUT}秒"
    echo "模式: $mode"
    echo "=========================================="
    
    for domain_entry in "${domains[@]}"; do
        # 支持 domain:port 格式
        if [[ "$domain_entry" == *":"* ]]; then
            domain="${domain_entry%:*}"
            port="${domain_entry#*:}"
        else
            domain="$domain_entry"
            port="443"
        fi
        
        # 清理域名
        domain=$(echo "$domain" | sed -e 's|^https://||' -e 's|^http://||' -e 's|/.*$||')
        
        case "$mode" in
            "detailed")
                get_ssl_cert_info "$domain" "$port" "$days_remaining"
                ;;
            "quick"|*)
                quick_check_domain "$domain" "$port"
                ;;
        esac
    done
}

## 第10步 
# 显示告警汇总
show_alerts() {
    if [ ${#ALERT_DOMAINS[@]} -eq 0 ]; then
        echo -e "\n${GREEN}🎉 所有证书状态正常,无需处理!${NC}"
        return
    fi
    
    echo -e "\n${RED}🚨 证书告警汇总 (剩余 ≤ ${ALERT_DAYS} 天/连接问题):${NC}"
    echo "=========================================="
    
    local has_critical_alerts=0
    
    for alert in "${ALERT_DOMAINS[@]}"; do
        if [[ "$alert" == *"剩余 $ALERT_DAYS"* ]] || 
           [[ "$alert" == *"已过期"* ]] || 
           [[ "$alert" == *"检查失败"* ]] || 
           [[ "$alert" == *"连接超时"* ]]; then
            echo -e "$alert"
            has_critical_alerts=1
        fi
    done
    
    if [ $has_critical_alerts -eq 0 ]; then
        echo -e "${GREEN}暂无紧急告警${NC}"
    fi
    
    echo "=========================================="
}

## 第11步 
# 检查是否有需要告警的证书
has_alert_certificates() {
    for alert in "${ALERT_DOMAINS[@]}"; do
        return 0
    done
    return 1  # 没有需要告警的证书
}

## 第13步
# 生成邮件内容
generate_email_content() {
    local email_content=""
    email_content+="SSL证书检查报告\n检查时间: $(date)\n\n"

    # 显示所有证书状态
    if [ ${#ALL_CERTIFICATES[@]} -gt 0 ]; then
        email_content+="📋 所有证书检查结果:\n"
        email_content+="==========================================\n"
        for cert in "${ALL_CERTIFICATES[@]}"; do
            email_content+="$cert\n"
        done
        email_content+="==========================================\n\n"
    fi

    # 显示告警证书
    email_content+="🚨 证书告警汇总 (剩余 ≤ ${ALERT_DAYS} 天/连接问题)"    

    if [ ${#ALERT_DOMAINS[@]} -eq 0 ]; then
        email_content+="\n${GREEN}🎉 所有证书状态正常,无需处理!${NC}"
    elif [ ${#ALERT_DOMAINS[@]} -gt 0 ]; then
        email_content+="\n==========================================\n"
        
        local has_critical_alerts=0
        
        for alert in "${ALERT_DOMAINS[@]}"; do

            email_content+="$alert\n" 
            has_critical_alerts=1

        done
    fi  
    
    echo -e "$email_content"
}


## 第12步
# 发送邮件
send_email() {
    local subject="${EMAIL_SUBJECT_PREFIX} - $(date '+%Y-%m-%d %H:%M')"
    local content
    content=$(generate_email_content)
    
    #echo "$content"

    echo "--------------------------------------------------1111---------------------------------------------------- "
    echo "$content"
    echo "--------------------------------------------------1111---------------------------------------------------- "
    # 检查是否启用邮件发送
    if [ "$EMAIL_ENABLED" != "true" ]; then
        echo -e "${YELLOW}邮件发送未启用${NC}"
        return 0
    fi
    
    # 检查邮件命令是否存在
    if ! command -v "$EMAIL_COMMAND" >/dev/null 2>&1; then
        echo -e "${RED}错误: 邮件命令 '$EMAIL_COMMAND' 不存在${NC}"
        return 1
    fi
    
    # 发送邮件
    echo -e "$content" | sed 's/\x1b\[[0-9;]*m//g' | $EMAIL_COMMAND -s "$subject" "$EMAIL_TO"
    local mail_result=$?
    
    if [ $mail_result -eq 0 ]; then
        echo -e "${GREEN}邮件发送成功${NC}"
        return 0
    else
        echo -e "${RED}邮件发送失败 (返回码: $mail_result)${NC}"
        return 1
    fi
}

## 第3步 
# 主函数
main() {
    local mode="quick"
    local file_path=""
    
    # 处理命令行参数
    while [[ $# -gt 0 ]]; do
        case $1 in
            -f|--file)
                if [ -n "$2" ]; then
                    file_path="$2"
                    shift 2
                else
                    echo -e "${RED}错误: --file 需要参数${NC}"
                    show_usage
                    exit 1
                fi
                ;;
            -q|--quick)
                mode="quick"
                shift
                ;;
            -t|--timeout)
                if [ -n "$2" ] && [[ "$2" =~ ^[0-9]+$ ]]; then
                    CONNECT_TIMEOUT="$2"
                    shift 2
                else
                    echo -e "${RED}错误: --timeout 需要数字参数${NC}"
                    show_usage
                    exit 1
                fi
                ;;
            -m|--mail)
                EMAIL_ENABLED=true
                shift
                ;;
            -h|--help)
                show_usage
                exit 0
                ;;
            -*)
                echo -e "${RED}错误: 未知选项 $1${NC}"
                show_usage
                exit 1
                ;;
            *)
                echo -e "${RED}错误: 不支持的参数 $1${NC}"
                show_usage
                exit 1
                ;;
        esac
    done
    
    # 如果没有指定文件,使用默认文件
    if [ -z "$file_path" ]; then
        if [ -f "$DOMAIN_LIST_FILE" ]; then
            file_path="$DOMAIN_LIST_FILE"
        else
            echo -e "${RED}错误: 未指定域名文件且默认文件 $DOMAIN_LIST_FILE 不存在${NC}"
            show_usage
            exit 1
        fi
    fi

    
    # 执行检查
    check_multiple_domains "$mode" "$file_path"

    # 显示告警汇总
    show_alerts
    
    # 发送邮件(如果启用且有需要告警的证书)
    if [ "$EMAIL_ENABLED" == "true" ]; then
        if has_alert_certificates; then
            echo -e "\n${BLUE}检测到需要告警的证书,正在发送邮件...${NC}"
            send_email
        else
            echo -e "\n${GREEN}无需要告警的证书,无需发送邮件${NC}"
        fi
    fi

}

## 第2步 
# 执行主函数
main "$@"

然后执行命令

./ssl_alert.sh -f domains.txt -m

在crontab中添加定时任务,进行定期执行

posted @ 2025-10-10 16:22  大司徒  阅读(13)  评论(0)    收藏  举报