提交 76496f7c authored 作者: 陈泽健's avatar 陈泽健

feat(security): 添加安全合规检测和服务器探测模块

- 实现弱密码检测功能,包括MySQL/Redis/EMQX/Linux弱密码检查
- 实现安全基线扫描功能,包括SUID文件/定时任务/SSH暴力破解/异常端口检测
- 添加服务器探测功能,包括连接验证、平台类型识别和系统探测
- 集成依赖检查和交互式服务器选择功能
- 提供完整的安全检测报告输出机制
上级 f8ef4a5d
# ==============================================================================
# SecurityCheck.psm1
# ------------------------------------------------------------------------------
# 安全合规检测模块
#
# .SYNOPSIS
# 提供弱密码检测和安全基线扫描功能
#
# .DESCRIPTION
# 本模块包含:
# - Test-WeakPassword: 弱密码检测(MySQL/Redis/EMQX/Linux)
# - Test-SecurityBaseline: 安全基线扫描(SUID/Crontab/SSH暴力破解/异常端口)
#
# .NOTES
# 版本:1.0.0
# 创建日期:2026-06-06
# 关联PRD:服务自检检测优化_需求文档 PRD 2.1
#
# ==============================================================================
# 导入公共模块
$ModuleDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$CommonModulePath = Join-Path $ModuleDir "Common.psm1"
if (Test-Path $CommonModulePath) {
Import-Module $CommonModulePath -Force -Global -ErrorAction SilentlyContinue
}
# ==============================================================================
# 弱密码字典
# ==============================================================================
$Global:WeakPasswordDict = @(
"", "123456", "password", "root", "admin", "redis", "mysql", "test", "guest"
)
# ==============================================================================
# SUID 白名单
# ==============================================================================
$Global:SUIDWhitelist = @(
"/usr/bin/sudo", "/usr/bin/passwd", "/usr/bin/su", "/usr/bin/ping",
"/usr/bin/mount", "/usr/bin/umount", "/usr/bin/newgrp", "/usr/bin/chsh",
"/usr/bin/chfn", "/usr/bin/gpasswd", "/usr/lib/openssh/ssh-keysign"
)
# ==============================================================================
# 端口白名单
# ==============================================================================
$Global:NewPlatformPortWhitelist = @(22, 80, 443, 1883, 3306, 6379, 8083, 8084, 8888, 18083)
$Global:OldPlatformPortWhitelist = @(22, 80, 443, 1883, 3306, 6379, 8081, 8082, 8443, 9001)
# ==============================================================================
# 弱密码检测函数
# ==============================================================================
function Test-WeakPassword {
<#
.SYNOPSIS
检测各类中间件和系统的弱密码问题
.DESCRIPTION
检测MySQL Root访问范围、MySQL空密码用户、Redis无密码/弱密码、
EMQX默认密码、Linux空密码账户。
.PARAMETER Server
服务器连接信息哈希表
.PARAMETER PlatformType
平台类型 ("new" 或 "old")
.OUTPUTS
System.Collections.Hashtable[]
返回包含检测结果哈希表的数组
#>
param(
[Parameter(Mandatory=$true)]
[hashtable]$Server,
[Parameter(Mandatory=$true)]
[ValidateSet('new','old')]
[string]$PlatformType
)
Write-Log -Level "INFO" -Message "========== 安全合规检测:弱密码检测 =========="
$results = @()
# --- 1. MySQL Root 访问范围检测 ---
$mysqlResult = Test-MySQLWeakPassword -Server $Server
if ($mysqlResult) { $results += $mysqlResult }
# --- 2. Redis 无密码/弱密码检测 ---
$redisResult = Test-RedisWeakPassword -Server $Server
if ($redisResult) { $results += $redisResult }
# --- 3. EMQX 默认密码检测 ---
$emqxResult = Test-EMQXWeakPassword -Server $Server
if ($emqxResult) { $results += $emqxResult }
# --- 4. Linux 空密码账户检测 ---
$linuxResult = Test-LinuxWeakPassword -Server $Server
if ($linuxResult) { $results += $linuxResult }
return $results
}
# ==============================================================================
# MySQL 弱密码检测(Root访问范围 + 空密码用户)
# ==============================================================================
function Test-MySQLWeakPassword {
param(
[Parameter(Mandatory=$true)]
[hashtable]$Server
)
$containerName = $MiddlewareConfig.MySQL.ContainerName
$mysqlPassword = $MiddlewareConfig.MySQL.Password
# 检查MySQL容器是否存在
$checkCmd = "docker ps --format '{{.Names}}' | grep -E '${containerName}|mysql' | head -n 1"
$checkResult = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $checkCmd
if ($checkResult.ExitCode -ne 0 -or -not $checkResult.Output) {
Write-Log -Level "INFO" -Message "[安全] 未检测到MySQL容器,跳过MySQL弱密码检测"
return $null
}
$actualContainer = (@($checkResult.Output)[0].ToString().Trim() -replace "`r","")
Write-Log -Level "INFO" -Message "[安全] MySQL弱密码检测,容器: $actualContainer"
$results = @()
# --- MySQL Root 访问范围 ---
try {
$rootHostCmd = "docker exec $actualContainer mysql -uroot -p`"$mysqlPassword`" -N -e `"SELECT host FROM mysql.user WHERE user='root'`" 2>/dev/null"
$rootHostResult = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $rootHostCmd
if ($rootHostResult.ExitCode -eq 0 -and $rootHostResult.Output) {
$hosts = $rootHostResult.Output -split "`n" | Where-Object { $_ -match '\S' } | ForEach-Object { $_.Trim() -replace "`r","" }
$hasWildcard = $hosts | Where-Object { $_ -eq '%' }
if ($hasWildcard) {
Write-Log -Level "WARN" -Message "[安全] MySQL Root 允许任意主机访问(root@%)"
$results += @{
Check = "MySQL Root 访问范围"
Status = "危险"
Details = "root@% 允许任意主机访问 | 当前host值: $($hosts -join ', ')"
Value = "$($hosts -join ', ')"
Success = $false
}
} else {
Write-Log -Level "SUCCESS" -Message "[安全] MySQL Root 仅允许本地访问"
$results += @{
Check = "MySQL Root 访问范围"
Status = "正常"
Details = "root 仅允许 $($hosts -join ', ') 访问"
Value = "$($hosts -join ', ')"
Success = $true
}
}
} else {
$results += @{
Check = "MySQL Root 访问范围"
Status = "跳过"
Details = "无法查询(可能密码错误或权限不足)"
Value = ""
Success = $false
}
}
} catch {
Write-Log -Level "WARN" -Message "[安全] MySQL Root 访问范围检测异常: $($_.Exception.Message)"
}
# --- MySQL 空密码用户 ---
try {
$emptyPwdCmd = "docker exec $actualContainer mysql -uroot -p`"$mysqlPassword`" -N -e `"SELECT user,host FROM mysql.user WHERE authentication_string='' OR plugin='mysql_native_password' AND authentication_string='*'`" 2>/dev/null"
$emptyPwdResult = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $emptyPwdCmd
if ($emptyPwdResult.ExitCode -eq 0 -and $emptyPwdResult.Output) {
$emptyUsers = $emptyPwdResult.Output -split "`n" | Where-Object { $_ -match '\S' }
if ($emptyUsers.Count -gt 0) {
Write-Log -Level "WARN" -Message "[安全] MySQL 存在空密码用户: $($emptyUsers -join ', ')"
$results += @{
Check = "MySQL 空密码用户"
Status = "危险"
Details = "发现 $($emptyUsers.Count) 个空密码用户: $($emptyUsers -join ' | ')"
Value = "$($emptyUsers.Count)"
Success = $false
}
} else {
$results += @{
Check = "MySQL 空密码用户"
Status = "正常"
Details = "无空密码用户"
Value = "0"
Success = $true
}
}
} else {
$results += @{
Check = "MySQL 空密码用户"
Status = "正常"
Details = "无空密码用户(或查询返回空)"
Value = "0"
Success = $true
}
}
} catch {
Write-Log -Level "WARN" -Message "[安全] MySQL 空密码用户检测异常: $($_.Exception.Message)"
}
return $results
}
# ==============================================================================
# Redis 弱密码检测
# ==============================================================================
function Test-RedisWeakPassword {
param(
[Parameter(Mandatory=$true)]
[hashtable]$Server
)
$containerName = $MiddlewareConfig.Redis.ContainerName
# 检查Redis容器是否存在
$checkCmd = "docker ps --format '{{.Names}}' | grep -E '${containerName}|redis' | head -n 1"
$checkResult = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $checkCmd
if ($checkResult.ExitCode -ne 0 -or -not $checkResult.Output) {
Write-Log -Level "INFO" -Message "[安全] 未检测到Redis容器,跳过Redis弱密码检测"
return $null
}
$actualContainer = (@($checkResult.Output)[0].ToString().Trim() -replace "`r","")
Write-Log -Level "INFO" -Message "[安全] Redis弱密码检测,容器: $actualContainer"
# 读取 redis.conf 中的 requirepass 配置
$configCmd = "docker exec $actualContainer sh -c `"grep -E '^requirepass' /etc/redis/redis.conf 2>/dev/null || grep -E '^requirepass' /usr/local/etc/redis/redis.conf 2>/dev/null || echo 'NO_CONFIG'`""
$configResult = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $configCmd
$result = $null
if ($configResult.ExitCode -eq 0 -and $configResult.Output) {
$configOutput = (@($configResult.Output)[0].ToString().Trim() -replace "`r","")
if ($configOutput -eq "NO_CONFIG" -or $configOutput -notmatch 'requirepass') {
# 未设置密码
Write-Log -Level "WARN" -Message "[安全] Redis 未设置密码(requirepass 为空)"
$result = @{
Check = "Redis 密码检测"
Status = "危险"
Details = "requirepass 未设置,Redis 无密码保护"
Value = "无密码"
Success = $false
}
} else {
# 提取密码值
$password = ""
if ($configOutput -match 'requirepass\s+(\S+)') {
$password = $matches[1]
}
# 检查是否为弱密码
$isWeak = $false
foreach ($weakPwd in $Global:WeakPasswordDict) {
if ($password -eq $weakPwd) {
$isWeak = $true
break
}
}
if ($isWeak) {
Write-Log -Level "WARN" -Message "[安全] Redis 使用弱密码"
$result = @{
Check = "Redis 密码检测"
Status = "危险"
Details = "Redis 密码为弱密码(在弱密码字典中)"
Value = "弱密码"
Success = $false
}
} else {
Write-Log -Level "SUCCESS" -Message "[安全] Redis 密码强度正常"
$result = @{
Check = "Redis 密码检测"
Status = "正常"
Details = "requirepass 已设置且非弱密码"
Value = "已设置"
Success = $true
}
}
}
} else {
$result = @{
Check = "Redis 密码检测"
Status = "跳过"
Details = "无法读取Redis配置文件"
Value = ""
Success = $false
}
}
return $result
}
# ==============================================================================
# EMQX 弱密码检测
# ==============================================================================
function Test-EMQXWeakPassword {
param(
[Parameter(Mandatory=$true)]
[hashtable]$Server
)
$containerName = $MiddlewareConfig.MQTT.ContainerName
$dashboardPort = $MiddlewareConfig.MQTT.DashboardPort
# 检查EMQX容器是否存在
$checkCmd = "docker ps --format '{{.Names}}' | grep -E '${containerName}|emqx' | head -n 1"
$checkResult = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $checkCmd
if ($checkResult.ExitCode -ne 0 -or -not $checkResult.Output) {
Write-Log -Level "INFO" -Message "[安全] 未检测到EMQX容器,跳过EMQX弱密码检测"
return $null
}
$actualContainer = (@($checkResult.Output)[0].ToString().Trim() -replace "`r","")
Write-Log -Level "INFO" -Message "[安全] EMQX弱密码检测,容器: $actualContainer"
# 检测EMQX Dashboard默认密码(通过API认证测试)
# EMQX 5.x 默认用户名: admin 密码: public
$authCmd = "docker exec $actualContainer curl -s -o /dev/null -w '%{http_code}' -u admin:public http://localhost:$dashboardPort/api/v5/status 2>/dev/null || echo 'FAIL'"
$authResult = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $authCmd
$result = $null
if ($authResult.ExitCode -eq 0 -and $authResult.Output) {
$httpCode = (@($authResult.Output)[0].ToString().Trim() -replace "`r","")
if ($httpCode -match '200' -or $httpCode -match '204') {
Write-Log -Level "WARN" -Message "[安全] EMQX 使用默认密码(admin/public)"
$result = @{
Check = "EMQX 默认密码检测"
Status = "危险"
Details = "EMQX Dashboard 使用默认密码 admin/public"
Value = "默认密码"
Success = $false
}
} else {
Write-Log -Level "SUCCESS" -Message "[安全] EMQX 未使用默认密码"
$result = @{
Check = "EMQX 默认密码检测"
Status = "正常"
Details = "EMQX Dashboard 未使用默认密码(认证返回 $httpCode)"
Value = "非默认"
Success = $true
}
}
} else {
$result = @{
Check = "EMQX 默认密码检测"
Status = "跳过"
Details = "无法访问EMQX Dashboard API"
Value = ""
Success = $false
}
}
return $result
}
# ==============================================================================
# Linux 空密码账户检测
# ==============================================================================
function Test-LinuxWeakPassword {
param(
[Parameter(Mandatory=$true)]
[hashtable]$Server
)
Write-Log -Level "INFO" -Message "[安全] 检测Linux空密码账户..."
# 解析 /etc/shadow 检查空密码账户
# 格式: username:$6$hash:... (第2个字段为密码哈希)
# 空密码: 字段为空或为 "!" 或 "*" 表示锁定,:: 表示空密码
$shadowCmd = "awk -F: 'length(`$2)==0 {print `$1}' /etc/shadow 2>/dev/null || echo 'SHADOW_FAIL'"
$shadowResult = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $shadowCmd
$result = $null
if ($shadowResult.ExitCode -eq 0 -and $shadowResult.Output) {
$output = $shadowResult.Output -join ""
$emptyUsers = $output -split "`n" | Where-Object { $_ -match '\S' -and $_ -ne 'SHADOW_FAIL' }
if ($emptyUsers.Count -gt 0) {
Write-Log -Level "WARN" -Message "[安全] 发现 $($emptyUsers.Count) 个空密码账户: $($emptyUsers -join ', ')"
$result = @{
Check = "Linux 空密码账户"
Status = "危险"
Details = "发现 $($emptyUsers.Count) 个空密码账户: $($emptyUsers -join ', ')"
Value = "$($emptyUsers.Count)"
Success = $false
}
} else {
Write-Log -Level "SUCCESS" -Message "[安全] 无Linux空密码账户"
$result = @{
Check = "Linux 空密码账户"
Status = "正常"
Details = "无空密码账户"
Value = "0"
Success = $true
}
}
} else {
$result = @{
Check = "Linux 空密码账户"
Status = "跳过"
Details = "无法读取 /etc/shadow"
Value = ""
Success = $false
}
}
return $result
}
# ==============================================================================
# 安全基线扫描函数
# ==============================================================================
function Test-SecurityBaseline {
<#
.SYNOPSIS
执行安全基线扫描
.DESCRIPTION
检测可疑SUID文件、异常crontab、SSH暴力破解、异常端口。
.PARAMETER Server
服务器连接信息哈希表
.PARAMETER PlatformType
平台类型 ("new" 或 "old")
.OUTPUTS
System.Collections.Hashtable[]
返回包含检测结果哈希表的数组
#>
param(
[Parameter(Mandatory=$true)]
[hashtable]$Server,
[Parameter(Mandatory=$true)]
[ValidateSet('new','old')]
[string]$PlatformType
)
Write-Log -Level "INFO" -Message "========== 安全合规检测:安全基线扫描 =========="
$results = @()
# --- 1. 可疑 SUID 文件检测 ---
$suidResult = Test-SUIDFiles -Server $Server
if ($suidResult) { $results += $suidResult }
# --- 2. 异常 crontab 检测 ---
$cronResult = Test-Crontab -Server $Server
if ($cronResult) { $results += $cronResult }
# --- 3. SSH 暴力破解检测 ---
$sshResult = Test-SSHBruteforce -Server $Server
if ($sshResult) { $results += $sshResult }
# --- 4. 异常端口检测 ---
$portResult = Test-OpenPorts -Server $Server -PlatformType $PlatformType
if ($portResult) { $results += $portResult }
return $results
}
# ==============================================================================
# SUID 文件检测
# ==============================================================================
function Test-SUIDFiles {
param(
[Parameter(Mandatory=$true)]
[hashtable]$Server
)
Write-Log -Level "INFO" -Message "[安全] 检测可疑SUID文件..."
# 排除 Docker overlay/containerd 文件系统,避免容器层 SUID 误报
$suidCmd = "find / -perm -4000 -type f -not -path '/data/dockers/*' -not -path '/var/lib/docker/*' -not -path '/proc/*' -not -path '/sys/*' 2>/dev/null | sort"
$suidResult = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $suidCmd
$result = $null
if ($suidResult.ExitCode -eq 0 -and $suidResult.Output) {
$suidFiles = $suidResult.Output -split "`n" | Where-Object { $_ -match '\S' } | ForEach-Object { $_.Trim() -replace "`r","" }
# 与白名单对比,找出可疑文件
$suspiciousFiles = @()
foreach ($file in $suidFiles) {
$isWhitelisted = $false
foreach ($wl in $Global:SUIDWhitelist) {
if ($file -eq $wl) {
$isWhitelisted = $true
break
}
}
if (-not $isWhitelisted) {
$suspiciousFiles += $file
}
}
if ($suspiciousFiles.Count -gt 0) {
Write-Log -Level "WARN" -Message "[安全] 发现 $($suspiciousFiles.Count) 个白名单外SUID文件"
$result = @{
Check = "可疑 SUID 文件"
Status = "警告"
Details = "发现 $($suspiciousFiles.Count) 个白名单外SUID文件: $($suspiciousFiles -join ' | ')"
Value = "$($suspiciousFiles.Count)"
Success = $false
}
} else {
Write-Log -Level "SUCCESS" -Message "[安全] 所有SUID文件均在白名单中(共 $($suidFiles.Count) 个)"
$result = @{
Check = "可疑 SUID 文件"
Status = "正常"
Details = "所有 $($suidFiles.Count) 个SUID文件均在白名单中"
Value = "$($suidFiles.Count)"
Success = $true
}
}
} else {
$result = @{
Check = "可疑 SUID 文件"
Status = "跳过"
Details = "无法执行SUID文件扫描"
Value = ""
Success = $false
}
}
return $result
}
# ==============================================================================
# 异常 crontab 检测
# ==============================================================================
function Test-Crontab {
param(
[Parameter(Mandatory=$true)]
[hashtable]$Server
)
Write-Log -Level "INFO" -Message "[安全] 检测异常crontab..."
# 收集所有用户的 crontab 和系统级定时任务
$cronCmd = @"
echo "=== 用户crontab ==="
for user in `$(cut -d: -f1 /etc/passwd 2>/dev/null); do
crontab_content="`$(crontab -u "`$user" -l 2>/dev/null | grep -vE '^\s*#|^\s*$' || true)"
if [ -n "`$crontab_content" ]; then
echo "[用户:`$user]"
echo "`$crontab_content"
fi
done
echo "=== 系统crontab ==="
cat /etc/crontab 2>/dev/null | grep -vE '^\s*#|^\s*$' || true
echo "=== cron.d ==="
for f in /etc/cron.d/*; do
[ -f "`$f" ] && echo "[文件:`$f]" && grep -vE '^\s*#|^\s*$' "`$f" 2>/dev/null
done
"@
$cronResult = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $cronCmd
$result = $null
if ($cronResult.ExitCode -eq 0 -and $cronResult.Output) {
$cronOutput = $cronResult.Output -join "`n"
$cronEntries = $cronOutput -split "`n" | Where-Object { $_ -match '\S' }
if ($cronEntries.Count -gt 0) {
# 已知的合法定时任务关键词
$knownPatterns = @(
"health_check", "check_server", "backup", "logrotate", "certbot",
"updatedb", "man-db", "mlocate", "apticron", "cron-apt",
"sysstat", "sa1", "sa2", "popularity-contest", "ntp",
"docker", "container", "repair", "issue_handler",
"clean", "tmpwatch", "tmpreaper"
)
$suspiciousEntries = @()
foreach ($entry in $cronEntries) {
$isKnown = $false
foreach ($pattern in $knownPatterns) {
if ($entry -match $pattern) {
$isKnown = $true
break
}
}
# 跳过标题行和空行
if ($entry -match '=====' -or $entry -match '^\[用户:' -or $entry -match '^\[文件:') {
$isKnown = $true
}
if (-not $isKnown -and $entry -match '/') {
$suspiciousEntries += $entry
}
}
if ($suspiciousEntries.Count -gt 0) {
Write-Log -Level "WARN" -Message "[安全] 发现 $($suspiciousEntries.Count) 条可疑定时任务"
$result = @{
Check = "异常 crontab"
Status = "警告"
Details = "发现 $($suspiciousEntries.Count) 条可疑定时任务(需人工确认)"
Value = "$($suspiciousEntries.Count)"
Success = $false
}
} else {
Write-Log -Level "SUCCESS" -Message "[安全] 所有定时任务均为已知任务(共 $($cronEntries.Count) 条)"
$result = @{
Check = "异常 crontab"
Status = "正常"
Details = "所有 $($cronEntries.Count) 条定时任务均为已知任务"
Value = "$($cronEntries.Count)"
Success = $true
}
}
} else {
$result = @{
Check = "异常 crontab"
Status = "正常"
Details = "无定时任务"
Value = "0"
Success = $true
}
}
} else {
$result = @{
Check = "异常 crontab"
Status = "跳过"
Details = "无法读取crontab信息"
Value = ""
Success = $false
}
}
return $result
}
# ==============================================================================
# SSH 暴力破解检测
# ==============================================================================
function Test-SSHBruteforce {
param(
[Parameter(Mandatory=$true)]
[hashtable]$Server
)
Write-Log -Level "INFO" -Message "[安全] 检测SSH暴力破解..."
# 分析 /var/log/secure(CentOS/RHEL)或 /var/log/auth.log(Ubuntu/Debian)
$sshCmd = @'
logfile=""
if [ -f /var/log/secure ]; then
logfile="/var/log/secure"
elif [ -f /var/log/auth.log ]; then
logfile="/var/log/auth.log"
fi
if [ -n "$logfile" ]; then
grep -i "failed\|failure\|invalid" "$logfile" 2>/dev/null | grep -oE 'from [0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | awk '{print $2}' | sort | uniq -c | sort -rn | head -n 10
else
echo "NO_LOG"
fi
'@
$sshResult = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $sshCmd
$result = $null
if ($sshResult.ExitCode -eq 0 -and $sshResult.Output) {
$output = $sshResult.Output -join "`n"
if ($output -match 'NO_LOG') {
$result = @{
Check = "SSH 暴力破解"
Status = "跳过"
Details = "未找到SSH日志文件(/var/log/secure 或 /var/log/auth.log)"
Value = ""
Success = $false
}
} else {
$lines = $output -split "`n" | Where-Object { $_ -match '^\s*\d+' }
$bruteforceIPs = @()
foreach ($line in $lines) {
if ($line -match '^\s*(\d+)\s+(\S+)') {
$count = [int]$matches[1]
$ip = $matches[2]
if ($count -ge 50) {
$bruteforceIPs += "$ip ($count 次)"
}
}
}
if ($bruteforceIPs.Count -gt 0) {
Write-Log -Level "WARN" -Message "[安全] 检测到SSH暴力破解: $($bruteforceIPs -join ', ')"
$result = @{
Check = "SSH 暴力破解"
Status = "危险"
Details = "发现 $($bruteforceIPs.Count) 个IP失败登录 >= 50次: $($bruteforceIPs -join ' | ')"
Value = "$($bruteforceIPs.Count)"
Success = $false
}
} else {
$totalFailed = 0
foreach ($line in $lines) {
if ($line -match '^\s*(\d+)') {
$totalFailed += [int]$matches[1]
}
}
Write-Log -Level "SUCCESS" -Message "[安全] 未检测到SSH暴力破解(总失败登录: $totalFailed)"
$result = @{
Check = "SSH 暴力破解"
Status = "正常"
Details = "未检测到暴力破解(总失败登录: $totalFailed,无IP超过50次阈值)"
Value = "$totalFailed"
Success = $true
}
}
}
} else {
$result = @{
Check = "SSH 暴力破解"
Status = "跳过"
Details = "无法分析SSH日志"
Value = ""
Success = $false
}
}
return $result
}
# ==============================================================================
# 异常端口检测
# ==============================================================================
function Test-OpenPorts {
param(
[Parameter(Mandatory=$true)]
[hashtable]$Server,
[Parameter(Mandatory=$true)]
[ValidateSet('new','old')]
[string]$PlatformType
)
Write-Log -Level "INFO" -Message "[安全] 检测异常端口..."
$portWhitelist = if ($PlatformType -eq "new") { $Global:NewPlatformPortWhitelist } else { $Global:OldPlatformPortWhitelist }
$portCmd = "netstat -tlnp 2>/dev/null | grep LISTEN | awk '{print `$4}' | grep -oE '[0-9]+$' | sort -u"
$portResult = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $portCmd
$result = $null
if ($portResult.ExitCode -eq 0 -and $portResult.Output) {
$listeningPorts = $portResult.Output -split "`n" | Where-Object { $_ -match '^\d+$' } | ForEach-Object { [int]($_.Trim()) }
$unexpectedPorts = @()
foreach ($port in $listeningPorts) {
$isWhitelisted = $false
foreach ($wlPort in $portWhitelist) {
if ($port -eq $wlPort) {
$isWhitelisted = $true
break
}
}
if (-not $isWhitelisted) {
$unexpectedPorts += $port
}
}
if ($unexpectedPorts.Count -gt 0) {
Write-Log -Level "WARN" -Message "[安全] 发现 $($unexpectedPorts.Count) 个非预期监听端口: $($unexpectedPorts -join ', ')"
$result = @{
Check = "异常端口"
Status = "警告"
Details = "发现 $($unexpectedPorts.Count) 个非预期监听端口: $($unexpectedPorts -join ', ') | 白名单: $($portWhitelist -join ',')"
Value = "$($unexpectedPorts -join ',')"
Success = $false
}
} else {
Write-Log -Level "SUCCESS" -Message "[安全] 所有监听端口均在白名单中(共 $($listeningPorts.Count) 个)"
$result = @{
Check = "异常端口"
Status = "正常"
Details = "所有 $($listeningPorts.Count) 个监听端口均在白名单中"
Value = "$($listeningPorts.Count)"
Success = $true
}
}
} else {
$result = @{
Check = "异常端口"
Status = "跳过"
Details = "无法获取监听端口列表"
Value = ""
Success = $false
}
}
return $result
}
# ==============================================================================
# 安全检测总入口(弱密码 + 基线扫描)
# ==============================================================================
function Test-SecurityCheck {
<#
.SYNOPSIS
安全合规检测总入口
.DESCRIPTION
执行弱密码检测和安全基线扫描,返回所有检测结果的汇总。
.PARAMETER Server
服务器连接信息哈希表
.PARAMETER PlatformType
平台类型 ("new" 或 "old")
.OUTPUTS
System.Collections.Hashtable[]
返回包含所有安全检测结果哈希表的数组
#>
param(
[Parameter(Mandatory=$true)]
[hashtable]$Server,
[Parameter(Mandatory=$true)]
[ValidateSet('new','old')]
[string]$PlatformType
)
Write-Log -Level "INFO" -Message "========== 开始安全合规检测 =========="
$allResults = @()
# 弱密码检测
$weakPwdResults = Test-WeakPassword -Server $Server -PlatformType $PlatformType
if ($weakPwdResults) { $allResults += $weakPwdResults }
# 安全基线扫描
$baselineResults = Test-SecurityBaseline -Server $Server -PlatformType $PlatformType
if ($baselineResults) { $allResults += $baselineResults }
Write-Log -Level "INFO" -Message "========== 安全合规检测完成(共 $($allResults.Count) 项) =========="
return $allResults
}
# ==============================================================================
# 导出模块函数
# ==============================================================================
Export-ModuleMember -Function @(
'Test-SecurityCheck',
'Test-WeakPassword',
'Test-SecurityBaseline'
)
# ==============================================================================
# ServerProfile.psm1
# ------------------------------------------------------------------------------
# 服务器探测模块
#
# .SYNOPSIS
# 提供服务器选择、连接验证、平台/系统探测功能
#
# .DESCRIPTION
# 本模块包含服务器交互相关的函数:
# - Test-Dependencies: 检测 plink/pscp/sshpass 可用性
# - Select-Server: 交互式服务器选择
# - Test-SSHConnection: SSH 连接测试
# - Get-PlatformType: 检测新/旧平台类型
# - Get-SystemType: 检测容器类型
# - Get-UjavaSystemVariant: 检测 ujava 变体
#
# .NOTES
# 版本:1.0.0
# 创建日期:2026-06-06
# 从 check_server_health.ps1 主脚本拆分而来
#
# ==============================================================================
# 导入公共模块
$ModuleDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$CommonModulePath = Join-Path $ModuleDir "Common.psm1"
if (Test-Path $CommonModulePath) {
Import-Module $CommonModulePath -Force -Global -ErrorAction SilentlyContinue
}
# ==============================================================================
# 检查依赖
# ==============================================================================
function Test-Dependencies {
<#
.SYNOPSIS
检查系统依赖工具
.DESCRIPTION
检测 plink.exe、pscp.exe、sshpass 等工具是否可用,设置全局变量。
优先使用脚本同目录下的 plink.exe,其次检查远程容器更新目录和系统 PATH。
.OUTPUTS
System.Boolean
依赖检查通过返回 true,失败返回 false
#>
Write-Log -Level "INFO" -Message "检查系统依赖..."
$osVersion = [System.Environment]::OSVersion.Version
Write-Log -Level "INFO" -Message " Windows 版本: $($osVersion.Major).$($osVersion.Minor).$($osVersion.Build)"
$hasPasswordTool = $false
# 1. 优先检查脚本同目录下的 plink.exe
$localPlinkPath = Join-Path $global:SCRIPT_DIR "plink.exe"
if (Test-Path $localPlinkPath) {
$global:PLINK_PATH = $localPlinkPath
Write-Log -Level "INFO" -Message " plink 已找到 (本地): $localPlinkPath"
$hasPasswordTool = $true
$global:PreferredSSHTool = "plink"
}
else {
# 2. 检查远程容器更新目录下的 plink.exe
$remoteUpdatePlinkPath = Join-Path (Split-Path $global:SCRIPT_DIR -Parent) "远程容器更新\plink.exe"
if (Test-Path $remoteUpdatePlinkPath) {
$global:PLINK_PATH = $remoteUpdatePlinkPath
Write-Log -Level "INFO" -Message " plink 已找到 (远程容器更新目录): $remoteUpdatePlinkPath"
$hasPasswordTool = $true
$global:PreferredSSHTool = "plink"
}
else {
# 3. 检查系统 PATH 中的 plink
try {
$systemPlink = Get-Command plink -ErrorAction Stop
$global:PLINK_PATH = $systemPlink.Source
Write-Log -Level "INFO" -Message " plink 已找到 (系统): $($global:PLINK_PATH)"
$hasPasswordTool = $true
$global:PreferredSSHTool = "plink"
}
catch {
Write-Log -Level "WARN" -Message " plink.exe 未找到"
}
}
}
# 如果没有 plink,检查 sshpass
if (-not $hasPasswordTool) {
try {
$sshpassPath = Get-Command sshpass -ErrorAction Stop
Write-Log -Level "INFO" -Message " sshpass 已找到: $($sshpassPath.Source)"
$hasPasswordTool = $true
$global:PreferredSSHTool = "sshpass"
}
catch {
Write-Log -Level "WARN" -Message " sshpass 未找到"
}
}
if (-not $hasPasswordTool) {
Write-Host ""
Write-Log -Level "ERROR" -Message " 未检测到密码认证工具"
Write-Host ""
Write-Log -Level "ERROR" -Message " 请按以下方式解决:"
Write-Host ""
Write-Log -Level "ERROR" -Message " 方式1 (推荐,离线可用):"
Write-Log -Level "ERROR" -Message " 将 plink.exe 放在脚本同目录下"
Write-Log -Level "ERROR" -Message " 当前脚本目录: $global:SCRIPT_DIR"
Write-Host ""
Write-Log -Level "ERROR" -Message " 下载地址:"
Write-Log -Level "ERROR" -Message " plink.exe: https://the.earth.li/~sgtatham/putty/latest/w64/plink.exe"
Write-Host ""
return $false
}
# 检查 pscp(用于文件传输/日志导出)
if ($global:PreferredSSHTool -eq "plink") {
# 1. 优先检查脚本同目录下的 pscp.exe
$localPscpPath = Join-Path $global:SCRIPT_DIR "pscp.exe"
if (Test-Path $localPscpPath) {
$global:PSCP_PATH = $localPscpPath
Write-Log -Level "INFO" -Message " pscp 已找到 (本地): $localPscpPath"
}
else {
# 2. 检查远程容器更新目录下的 pscp.exe
$remoteUpdatePscpPath = Join-Path (Split-Path $global:SCRIPT_DIR -Parent) "远程容器更新\pscp.exe"
if (Test-Path $remoteUpdatePscpPath) {
$global:PSCP_PATH = $remoteUpdatePscpPath
Write-Log -Level "INFO" -Message " pscp 已找到 (远程容器更新目录): $remoteUpdatePscpPath"
}
else {
# 3. 检查系统 PATH 中的 pscp
try {
$systemPscp = Get-Command pscp -ErrorAction Stop
$global:PSCP_PATH = $systemPscp.Source
Write-Log -Level "INFO" -Message " pscp 已找到 (系统): $($global:PSCP_PATH)"
}
catch {
Write-Log -Level "WARN" -Message " pscp.exe 未找到,日志导出功能将不可用"
}
}
}
}
Write-Log -Level "INFO" -Message "系统依赖检查通过 (使用 $global:PreferredSSHTool 进行密码认证)"
return $true
}
# ==============================================================================
# 选择服务器
# ==============================================================================
function Select-Server {
<#
.SYNOPSIS
交互式选择目标服务器
.DESCRIPTION
显示预设服务器列表供用户选择,也支持手动输入服务器信息。
密码输入使用 SecureString 方式隐藏显示。
.OUTPUTS
System.Collections.Hashtable
服务器信息哈希表 (IP, User, Pass, Port, Desc),取消返回 null
#>
Write-Log -Level "INFO" -Message "可选择的目标服务器:"
Write-Host ""
foreach ($key in ($ServerList.Keys | Sort-Object)) {
$server = $ServerList[$key]
Write-Host " [$key] $($server.Desc) ($($server.IP) $($server.User))"
}
Write-Host " [0] 手动输入服务器信息"
Write-Host ""
$serverKey = Read-Host "请输入服务器编号"
if ($serverKey -eq "0") {
Write-Log -Level "INFO" -Message "进入手动输入模式"
Write-Host ""
$remoteHost = Read-Host "请输入目标服务器 IP 地址"
if ([string]::IsNullOrEmpty($remoteHost)) {
Write-Log -Level "ERROR" -Message "服务器 IP 地址不能为空"
return $null
}
$sshPortInput = Read-Host "请输入 SSH 端口号 [默认 22]"
if ([string]::IsNullOrEmpty($sshPortInput)) {
$sshPort = 22
} else {
$sshPort = [int]$sshPortInput
}
$remoteUserInput = Read-Host "请输入登录用户名 [默认 root]"
if ([string]::IsNullOrEmpty($remoteUserInput)) {
$remoteUser = "root"
} else {
$remoteUser = $remoteUserInput
}
$remotePassSecure = Read-Host "请输入登录密码" -AsSecureString
$BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($remotePassSecure)
$remotePass = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR)
[System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($BSTR)
if ([string]::IsNullOrEmpty($remotePass)) {
Write-Log -Level "ERROR" -Message "登录密码不能为空"
return $null
}
Write-Log -Level "INFO" -Message "已配置目标服务器: ${remoteUser}@${remoteHost}:${sshPort}"
return @{
IP = $remoteHost
User = $remoteUser
Pass = $remotePass
Port = $sshPort
Desc = "手动输入服务器"
}
}
elseif ($ServerList.ContainsKey($serverKey)) {
$server = $ServerList[$serverKey].Clone()
# 如果预设列表中没有指定端口,使用默认22
if (-not $server.ContainsKey('Port') -or $server.Port -eq 0) {
$server.Port = 22
}
Write-Log -Level "INFO" -Message "已选择 $($server.Desc) ($($server.IP)):$($server.Port)"
return $server
}
else {
Write-Log -Level "ERROR" -Message "编号 $serverKey 不存在,请重新运行脚本"
return $null
}
}
# ==============================================================================
# 测试 SSH 连接
# ==============================================================================
function Test-SSHConnection {
<#
.SYNOPSIS
测试 SSH 连接是否正常
.PARAMETER Server
服务器连接信息哈希表
.OUTPUTS
System.Boolean
#>
param(
[hashtable]$Server
)
Write-Log -Level "INFO" -Message "测试 SSH 连接..."
$result = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command "echo CONNECTION_OK"
if (($result.ExitCode -ne 0) -or ($result.Output -notmatch "CONNECTION_OK")) {
Write-Log -Level "ERROR" -Message "SSH 连接失败!"
Write-Log -Level "ERROR" -Message "输出信息: $($result.Output)"
Write-Log -Level "ERROR" -Message "请检查: 1) IP地址是否正确 2) 端口是否正确 3) 密码是否正确 4) 网络是否可达"
return $false
}
Write-Log -Level "SUCCESS" -Message "SSH 连接测试通过"
return $true
}
# ==============================================================================
# 检测平台类型
# ==============================================================================
function Get-PlatformType {
<#
.SYNOPSIS
检测远程服务器的平台类型
.DESCRIPTION
通过检查 /data/services 目录是否存在来判定新统一平台或传统平台。
.PARAMETER Server
服务器连接信息哈希表
.OUTPUTS
System.String
"new" 表示新统一平台,"old" 表示传统平台
#>
param(
[hashtable]$Server
)
Write-Log -Level "INFO" -Message "自动检测目标服务器平台类型..."
$result = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command "[ -d /data/services ] && echo 'NEW_PLATFORM' || echo 'OLD_PLATFORM'"
$outputLines = $result.Output -split "`n" | Where-Object { $_ -match '\S' }
$platformCheck = ($outputLines | Select-Object -Last 1).Trim()
if ($platformCheck -eq "NEW_PLATFORM") {
Write-Log -Level "SUCCESS" -Message "检测到 /data/services 目录存在,识别为【新统一平台】"
return "new"
}
else {
Write-Log -Level "SUCCESS" -Message "未检测到 /data/services 目录,识别为【传统平台】"
return "old"
}
}
# ==============================================================================
# 检测系统类型(容器类型)
# ==============================================================================
function Get-SystemType {
<#
.SYNOPSIS
检测远程服务器上的容器类型
.DESCRIPTION
通过 docker ps 命令检测运行中的容器,分类为 ujava、upython、upython_voice。
.PARAMETER Server
服务器连接信息哈希表
.OUTPUTS
System.Collections.Hashtable
包含 HasUjava, HasUpython, HasUpythonVoice, Containers 等信息
#>
param(
[hashtable]$Server
)
Write-Log -Level "INFO" -Message "自动检测系统类型(容器)..."
# 检测运行中的容器
$result = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command "docker ps --format '{{.Names}}' 2>/dev/null || echo 'DOCKER_ERROR'"
if ($result.Output -match "DOCKER_ERROR" -or $result.ExitCode -ne 0) {
Write-Log -Level "WARN" -Message "Docker 命令执行失败,可能未安装 Docker"
return @{
HasUjava = $false
HasUpython = $false
HasUpythonVoice = $false
Containers = @()
}
}
$containers = $result.Output -split "`n" | Where-Object { $_ -match '\S' } | ForEach-Object { $_.Trim() }
$systemInfo = @{
HasUjava = $false
HasUpython = $false
HasUpythonVoice = $false
Containers = $containers
UjavaContainer = $null
UpythonContainer = $null
UpythonVoiceContainer = $null
}
foreach ($container in $containers) {
if ($container -match '^ujava\d*$') {
$systemInfo.HasUjava = $true
$systemInfo.UjavaContainer = $container
Write-Log -Level "INFO" -Message " 检测到 ujava 容器: $container"
}
elseif ($container -match '^upython\d*$' -and $container -notmatch 'voice') {
$systemInfo.HasUpython = $true
$systemInfo.UpythonContainer = $container
Write-Log -Level "INFO" -Message " 检测到 upython 容器: $container -> 运维集控系统"
}
elseif ($container -match '^upython_voice\d*$') {
$systemInfo.HasUpythonVoice = $true
$systemInfo.UpythonVoiceContainer = $container
Write-Log -Level "INFO" -Message " 检测到 upython_voice 容器: $container -> 转录系统"
}
}
if (-not $systemInfo.HasUjava -and -not $systemInfo.HasUpython -and -not $systemInfo.HasUpythonVoice) {
Write-Log -Level "WARN" -Message " 未检测到任何已知容器类型"
}
return $systemInfo
}
# ==============================================================================
# ujava 系统细分检测
# ==============================================================================
function Get-UjavaSystemVariant {
<#
.SYNOPSIS
检测 ujava 系统细分类型
.DESCRIPTION
通过检查 /var/www/java/unifiedPlatform 目录是否存在,判定 ujava 系统是
统一平台系统还是会议预定系统。仅对传统平台目录体系有意义。
.PARAMETER Server
服务器连接信息哈希表
.OUTPUTS
System.String
"unified" 或 "meeting"
#>
param(
[Parameter(Mandatory=$true)] [hashtable]$Server
)
$cmd = "[ -d /var/www/java/unifiedPlatform ] && echo 'UNIFIED' || echo 'MEETING'"
$res = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $cmd
$last = (($res.Output -split "`n") | Where-Object { $_ -match '\S' } | Select-Object -Last 1).Trim()
if ($last -eq "UNIFIED") { return "unified" }
return "meeting"
}
# ==============================================================================
# 导出模块函数
# ==============================================================================
Export-ModuleMember -Function @(
'Test-Dependencies',
'Select-Server',
'Test-SSHConnection',
'Get-PlatformType',
'Get-SystemType',
'Get-UjavaSystemVariant'
)
# ==============================================================================
# ShellAdapter.psm1
# ------------------------------------------------------------------------------
# Shell 脚本模式适配器模块
#
# .SYNOPSIS
# 提供 Shell 脚本模式的检测功能,作为 PowerShell 模式的替代方案
#
# .DESCRIPTION
# 本模块包含 Shell 脚本模式的所有检测函数和基础设施:
# - Invoke-RemoteShellCheck: 远程执行 Shell 脚本并获取输出
# - ConvertFrom-ShellJson: 解析 Shell 脚本 JSON 输出
# - Convert-ResourceCheckToStandard: 资源检测结果格式转换
# - Parse-ResourceCheckText: 解析 resource_check.sh 文本输出
# - Test-ServerResources-Shell: Shell 模式资源检测
# - Test-MQTTConnection-Shell: Shell 模式中间件检测
# - Test-DNSResolution-Shell: Shell 模式 DNS 检测
# - Test-NTPService-Shell: Shell 模式 NTP 检测
# - Test-Firewall-Shell: Shell 模式防火墙检测
# - Test-ConfigIPs-Shell: Shell 模式配置 IP 检测
# - Test-UjavaServices-Shell: Shell 模式服务检测
#
# .NOTES
# 版本:1.0.0
# 创建日期:2026-06-06
# 从 check_server_health.ps1 主脚本拆分而来
# 注意:Upload-ShellScript 已移至 Common.psm1
#
# ==============================================================================
# 导入公共模块
$ModuleDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$CommonModulePath = Join-Path $ModuleDir "Common.psm1"
if (Test-Path $CommonModulePath) {
Import-Module $CommonModulePath -Force -Global -ErrorAction SilentlyContinue
}
# 导入数据备份模块(需要 Download-RemoteFile)
$DataBackupModulePath = Join-Path $ModuleDir "DataBackup.psm1"
if (Test-Path $DataBackupModulePath) {
Import-Module $DataBackupModulePath -Force -Global -ErrorAction SilentlyContinue
}
# ==============================================================================
# 执行远程Shell脚本
# ==============================================================================
function Invoke-RemoteShellCheck {
<#
.SYNOPSIS
远程执行 Shell 脚本并获取输出
.DESCRIPTION
上传 Shell 脚本到远程服务器,执行并获取输出。
优先使用 base64 编码传输结果,失败时回退到原始输出解析。
.PARAMETER Server
服务器连接信息哈希表
.PARAMETER ScriptName
要执行的脚本文件名
.PARAMETER Arguments
脚本参数
.PARAMETER RemotePath
远程临时目录路径
.OUTPUTS
System.String
脚本输出内容,失败返回 null
#>
param(
[Parameter(Mandatory=$true)] [hashtable]$Server,
[Parameter(Mandatory=$true)] [string]$ScriptName,
[Parameter(Mandatory=$false)] [string]$Arguments = "",
[Parameter(Mandatory=$false)] [string]$RemotePath = "/tmp/health_check"
)
# 上传脚本
$uploadSuccess = Upload-ShellScript -Server $Server -ScriptName $ScriptName -RemotePath $RemotePath
if (-not $uploadSuccess) {
Write-Log -Level "ERROR" -Message "[SHELL] 脚本上传失败: $ScriptName"
return $null
}
# 执行脚本并将输出保存到临时文件
$outputFile = "/tmp/health_check_output.txt"
# 先检查远程是否有base64命令
$checkCmd = "which base64 || echo 'not_found'"
$checkResult = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $checkCmd
if ($checkResult.Output -match "base64") {
# 使用base64编码传输
$cmd = "cd $RemotePath && chmod +x common.sh $ScriptName && ./$ScriptName $Arguments > $outputFile 2>&1 && base64 $outputFile > ${outputFile}.b64 && rm -f $outputFile"
Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $cmd | Out-Null
# 下载base64文件
$localTempDir = Join-Path $env:TEMP "health_check"
if (-not (Test-Path $localTempDir)) {
New-Item -ItemType Directory -Path $localTempDir -Force | Out-Null
}
$localBase64File = Join-Path $localTempDir "output.b64"
if ($global:PSCP_PATH -and (Test-Path $global:PSCP_PATH)) {
$pscpArgs = @(
"-P", $Server.Port,
"-l", $Server.User,
"-pw", $Server.Pass,
"-batch",
"$($Server.User)@$($Server.IP):${outputFile}.b64",
$localBase64File
)
& $global:PSCP_PATH @pscpArgs 2>&1 | Out-Null
if ((Test-Path $localBase64File) -and ((Get-Item $localBase64File).Length -gt 100)) {
try {
$base64Content = Get-Content $localBase64File -Raw
# 移除换行符
$base64Content = $base64Content -replace "`r", "" -replace "`n", ""
$decodedContent = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($base64Content))
Remove-Item $localBase64File -Force -ErrorAction SilentlyContinue
Remove-Item $localTempDir -Force -ErrorAction SilentlyContinue
Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command "rm -f ${outputFile}.b64; rm -rf $RemotePath" | Out-Null
return $decodedContent
} catch {
Write-Log -Level "ERROR" -Message "[SHELL] Base64解码失败: $($_.Exception.Message)"
}
}
}
}
# 清理远程临时文件
Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command "rm -f $outputFile ${outputFile}.b64; rm -rf $RemotePath" | Out-Null
# 如果base64方法失败,使用原始方法(添加过滤)
Write-Log -Level "WARN" -Message "[SHELL] Base64方法失败,使用原始方法: $ScriptName"
$cmd = "cd $RemotePath && chmod +x common.sh $ScriptName && ./$ScriptName $Arguments 2>&1"
$result = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $cmd
if ($result -and $result.Output) {
if ($result.Output -is [array]) {
$output = $result.Output -join ""
} else {
$output = $result.Output.ToString()
}
# 移除plink输出中的多余内容
$lines = $output -split "`n"
$jsonStarted = $false
$jsonLines = @()
$braceCount = 0
foreach ($line in $lines) {
$trimmed = $line.Trim()
if (-not $jsonStarted -and $trimmed.StartsWith("{")) {
$jsonStarted = $true
}
if ($jsonStarted) {
$jsonLines += $line
$braceCount += ($line.ToCharArray() | Where-Object { $_ -eq "{" } | Measure-Object).Count
$braceCount -= ($line.ToCharArray() | Where-Object { $_ -eq "}" } | Measure-Object).Count
if ($braceCount -le 0 -and $trimmed.EndsWith("}")) {
break
}
}
}
if ($jsonLines.Count -gt 0) {
return $jsonLines -join "`n"
}
}
return $null
}
# ==============================================================================
# 解析Shell脚本JSON结果
# ==============================================================================
function ConvertFrom-ShellJson {
<#
.SYNOPSIS
解析 Shell 脚本的 JSON 输出
.DESCRIPTION
过滤掉 plink 输出中的非 JSON 行,提取并解析 JSON 内容。
.PARAMETER JsonString
Shell 脚本输出的原始字符串
.OUTPUTS
解析后的 JSON 对象,失败返回 null
#>
param(
[Parameter(Mandatory=$false)] [string]$JsonString = ""
)
# 检查输入是否为空
if ([string]::IsNullOrEmpty($JsonString)) {
Write-Log -Level "ERROR" -Message "[SHELL] Shell脚本未返回任何输出"
return $null
}
try {
# 过滤掉非JSON行(plink输出、错误信息等)
# JSON通常以 { 开头,所以只保留从 { 开始的内容
$lines = $JsonString -split "`n"
$jsonLines = @()
$inJson = $false
$braceCount = 0
foreach ($line in $lines) {
$trimmed = $line.Trim()
# 跳过空行
if ([string]::IsNullOrEmpty($trimmed)) { continue }
# 跳过常见命令输出和错误信息(包括中文输出)
if ($trimmed -match "^(chmod|mkdir|rm|cd|\$|pscp:|plink:|Fatal|Network|Connection|已用|总用量|Mem:|Swap:|Cpu)") { continue }
# 找到JSON开始位置
if (-not $inJson -and $trimmed.StartsWith("{")) {
$inJson = $true
}
if ($inJson) {
$jsonLines += $line
$braceCount += ($line.ToCharArray() | Where-Object { $_ -eq "{" } | Measure-Object).Count
$braceCount -= ($line.ToCharArray() | Where-Object { $_ -eq "}" } | Measure-Object).Count
# 当大括号平衡时,结束解析
if ($braceCount -le 0 -and $trimmed.EndsWith("}")) {
break
}
}
}
if ($jsonLines.Count -eq 0) {
Write-Log -Level "ERROR" -Message "[SHELL] 未找到JSON输出"
return $null
}
$cleanJson = $jsonLines -join "`n"
return $cleanJson | ConvertFrom-Json
}
catch {
Write-Log -Level "ERROR" -Message "[SHELL] JSON解析失败: $($_.Exception.Message)"
Write-Log -Level "INFO" -Message "[SHELL] 原始输出前200字符: $($JsonString.Substring(0, [Math]::Min(200, $JsonString.Length)))"
return $null
}
}
# ==============================================================================
# 资源检测(Shell模式)- 使用文件下载方式避免输出截断
# ==============================================================================
function Test-ServerResources-Shell {
<#
.SYNOPSIS
Shell 模式的服务器资源检测
.DESCRIPTION
上传 resource_check.sh 到远程服务器执行,下载输出文件并解析结果。
上传失败时回退到 PowerShell 模式。
.PARAMETER Server
服务器连接信息哈希表
.OUTPUTS
System.Collections.Hashtable
资源检测结果哈希表
#>
param(
[Parameter(Mandatory=$true)] [hashtable]$Server
)
Write-Host ""
Write-Log -Level "INFO" -Message "========== 开始资源检测 (Shell模式, 文件方式) =========="
# 上传脚本到远程服务器
$remotePath = "/tmp/health_check_resources"
$uploadSuccess = Upload-ShellScript -Server $Server -ScriptName "resource_check.sh" -RemotePath $remotePath
if (-not $uploadSuccess) {
Write-Log -Level "ERROR" -Message "[资源] 脚本上传失败,回退到PowerShell模式"
return Test-ServerResources -Server $Server
}
# 定义远程输出文件路径
$remoteOutputFile = "/tmp/resource_check_output.txt"
$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
$localOutputFile = Join-Path $env:TEMP "resource_check_${timestamp}.txt"
try {
# 执行远程脚本,将输出保存到文件
$cmd = "cd $remotePath && chmod +x common.sh resource_check.sh && ./resource_check.sh --format text --check all > $remoteOutputFile 2>&1"
Write-Log -Level "INFO" -Message "[资源] 执行远程检测命令..."
$execResult = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $cmd
# 下载输出文件
Write-Log -Level "INFO" -Message "[资源] 下载检测结果文件..."
$downloadResult = Download-RemoteFile -Server $Server -RemotePath $remoteOutputFile -LocalPath $localOutputFile -TimeoutSeconds 60
if ($downloadResult.Success -and (Test-Path $localOutputFile)) {
Write-Log -Level "SUCCESS" -Message "[资源] 检测结果下载成功,开始解析..."
# 读取文件内容
$content = Get-Content $localOutputFile -Raw -Encoding UTF8
# 解析文本内容并转换为标准格式
$parsedResults = Parse-ResourceCheckText -Content $content
$results = Convert-ResourceCheckToStandard -ParsedResults $parsedResults
# 清理临时文件
Remove-Item $localOutputFile -Force -ErrorAction SilentlyContinue
Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command "rm -f $remoteOutputFile; rm -rf $remotePath" | Out-Null
Write-Log -Level "INFO" -Message "========== 结束资源检测 (Shell模式) =========="
return $results
}
else {
Write-Log -Level "ERROR" -Message "[资源] 检测结果下载失败"
return $null
}
}
catch {
Write-Log -Level "ERROR" -Message "[资源] 执行过程中发生异常: $($_.Exception.Message)"
return $null
}
}
# ==============================================================================
# 将解析结果转换为标准hashtable格式
# ==============================================================================
function Convert-ResourceCheckToStandard {
<#
.SYNOPSIS
将资源检测解析结果转换为标准哈希表格式
.DESCRIPTION
将 Parse-ResourceCheckText 的输出转换为统一的哈希表结构,
包含 OS、Architecture、CPU、Memory、Disk 等键。
.PARAMETER ParsedResults
Parse-ResourceCheckText 返回的 PSCustomObject 数组
.OUTPUTS
System.Collections.Hashtable
#>
param(
[Parameter(Mandatory=$true)] [array]$ParsedResults
)
$results = @{
OS = $null
Architecture = $null
CPU = $null
Memory = $null
Disk = @()
Firewall = $null
}
$cpuUsage = 0
$cpuCores = 0
$cpuLoad = ""
$memTotal = 0
$memUsed = 0
$memPercent = 0
$memTotalMB = 0
$memUsedMB = 0
# 系统信息变量
$osName = ""
$osVersion = ""
$systemArch = ""
$kernelVersion = ""
foreach ($item in $ParsedResults) {
switch ($item.Category) {
"系统信息" {
switch ($item.Item) {
"操作系统" { $osName = $item.Value }
"系统版本" { $osVersion = $item.Value }
"系统架构" { $systemArch = $item.Value }
"内核版本" { $kernelVersion = $item.Value }
}
}
"CPU" {
switch ($item.Item) {
"核心数" { $cpuCores = [int]$item.Value }
"使用率" {
if ($item.Value -match '([\d.]+)%') {
$cpuUsage = [double]$matches[1]
}
}
"负载平均" { $cpuLoad = $item.Value }
}
}
"内存" {
if ($item.Item -eq "使用率" -and $item.Value -match '([\d.]+)%') {
$memPercent = [double]$matches[1]
}
# 尝试获取总计和已用
if ($item.Item -eq "总计" -and $item.Value -match '([\d.]+)GB') {
$memTotal = [double]$matches[1]
}
if ($item.Item -eq "已用" -and $item.Value -match '([\d.]+)GB') {
$memUsed = [double]$matches[1]
}
# 如果是MB单位
if ($item.Item -eq "总计" -and $item.Value -match '([\d.]+)MB') {
$memTotalMB = [double]$matches[1]
}
if ($item.Item -eq "已用" -and $item.Value -match '([\d.]+)MB') {
$memUsedMB = [double]$matches[1]
}
}
"磁盘" {
# 解析磁盘信息:例如 "10G/50G (20%)" 或 "5G/20G (25%)"
if ($item.Value -match '([\d.]+[A-Z]+)/([\d.]+[A-Z])\s+\((\d+)%\)') {
$used = $matches[1]
$size = $matches[2]
$percent = [int]$matches[3]
$status = $item.Status
$results.Disk += @{
Device = "unknown"
Size = $size
Used = $used
Percent = $percent
MountPoint = $item.Item
Status = $status
}
}
}
}
}
# 构建CPU结果
if ($cpuCores -gt 0 -or $cpuUsage -gt 0) {
$cpuStatus = if ($cpuUsage -lt 70) { "正常" } elseif ($cpuUsage -lt 90) { "警告" } else { "严重" }
$results.CPU = @{
Usage = $cpuUsage
Cores = $cpuCores
Status = $cpuStatus
Success = ($cpuUsage -lt 90)
}
}
# 构建内存结果
if ($memTotal -gt 0 -or $memTotalMB -gt 0 -or $memPercent -gt 0) {
# 如果有GB单位数据,使用它;否则从MB转换
if ($memTotal -eq 0 -and $memTotalMB -gt 0) {
$memTotal = [math]::Round($memTotalMB / 1024, 2)
if ($memUsedMB -eq 0 -and $memPercent -gt 0) {
$memUsed = [math]::Round($memTotal * $memPercent / 100, 2)
} else {
$memUsed = [math]::Round($memUsedMB / 1024, 2)
}
}
# 如果只有百分比,估算(假设16GB总内存)
if ($memTotal -eq 0 -and $memPercent -gt 0) {
$memTotal = 16
$memUsed = [math]::Round($memTotal * $memPercent / 100, 2)
}
# 如果有总计和已用但没有百分比,计算百分比
if ($memPercent -eq 0 -and $memTotal -gt 0 -and $memUsed -gt 0) {
$memPercent = [math]::Round(($memUsed / $memTotal) * 100, 1)
}
$memStatus = if ($memPercent -lt 70) { "正常" } elseif ($memPercent -lt 90) { "警告" } else { "严重" }
$results.Memory = @{
Total = $memTotal
Used = $memUsed
Percent = $memPercent
Status = $memStatus
Success = ($memPercent -lt 90)
}
}
# 添加OS和架构信息
$osInfo = if ($osName -and $osVersion) { "$osName $osVersion" } elseif ($osName) { $osName } else { "Linux" }
$results.OS = @{
Info = $osInfo
Status = "正常"
Success = $true
}
$results.Architecture = @{
Arch = if ($systemArch) { $systemArch } else { "x86_64" }
Kernel = if ($kernelVersion) { $kernelVersion } else { "unknown" }
Status = "正常"
Success = $true
}
return $results
}
# ==============================================================================
# 解析资源检测文本输出
# ==============================================================================
function Parse-ResourceCheckText {
<#
.SYNOPSIS
解析 resource_check.sh 的文本输出
.DESCRIPTION
将 resource_check.sh 的文本输出解析为结构化的 PSCustomObject 数组。
支持系统信息、CPU、内存、磁盘、网络等检测项。
.PARAMETER Content
resource_check.sh 输出的文本内容
.OUTPUTS
PSCustomObject 数组
#>
param(
[Parameter(Mandatory=$true)] [string]$Content
)
$results = @()
$lines = $Content -split "`n"
$currentSection = $null
$memTotal = 0
$memUsed = 0
$memPercent = 0
foreach ($line in $lines) {
$trimmed = $line.Trim()
# 跳过空行和分隔线
if ([string]::IsNullOrEmpty($trimmed) -or $trimmed -match '^=+$|^---+$') {
continue
}
# 系统信息
if ($trimmed -match '^操作系统:\s*(.+)$') {
$osName = $matches[1].Trim()
$results += [PSCustomObject]@{
Category = "系统信息"
Item = "操作系统"
Value = $osName
Status = "正常"
}
}
elseif ($trimmed -match '^系统版本:\s*(.+)$') {
$osVersion = $matches[1].Trim()
$results += [PSCustomObject]@{
Category = "系统信息"
Item = "系统版本"
Value = $osVersion
Status = "正常"
}
}
elseif ($trimmed -match '^系统架构:\s*(.+)$') {
$arch = $matches[1].Trim()
$results += [PSCustomObject]@{
Category = "系统信息"
Item = "系统架构"
Value = $arch
Status = "正常"
}
}
elseif ($trimmed -match '^内核版本:\s*(.+)$') {
$kernel = $matches[1].Trim()
$results += [PSCustomObject]@{
Category = "系统信息"
Item = "内核版本"
Value = $kernel
Status = "正常"
}
}
# CPU信息
elseif ($trimmed -match '^核心数:\s*(\d+)') {
$cores = $matches[1]
$results += [PSCustomObject]@{
Category = "CPU"
Item = "核心数"
Value = $cores
Status = "正常"
}
}
elseif ($trimmed -match '^使用率:\s*([\d.]+)%') {
$usage = [double]$matches[1]
$status = if ($usage -lt 80) { "正常" } elseif ($usage -lt 90) { "警告" } else { "严重" }
$results += [PSCustomObject]@{
Category = "CPU"
Item = "使用率"
Value = "$usage%"
Status = $status
}
}
elseif ($trimmed -match '^负载平均:\s*(.+)') {
$load = $matches[1].Trim()
$results += [PSCustomObject]@{
Category = "CPU"
Item = "负载平均"
Value = $load
Status = "正常"
}
}
# 内存信息
elseif ($trimmed -match '^--- 内存 ---') {
# 进入内存区域
$currentSection = "内存"
}
elseif ($trimmed -match '^总计:\s*([\d.]+)(GB|MB)') {
$memTotalValue = [double]$matches[1]
$memTotalUnit = $matches[2]
# 转换为GB存储
if ($memTotalUnit -eq "MB") {
$memTotal = $memTotalValue / 1024
} else {
$memTotal = $memTotalValue
}
}
elseif ($trimmed -match '^已用:\s*([\d.]+)(GB|MB)') {
$memUsedValue = [double]$matches[1]
$memUsedUnit = $matches[2]
# 转换为GB存储
if ($memUsedUnit -eq "MB") {
$memUsed = $memUsedValue / 1024
} else {
$memUsed = $memUsedValue
}
}
elseif ($trimmed -match '^使用率:\s*([\d.]+)%') {
$memPercent = [double]$matches[1]
$status = if ($memPercent -lt 70) { "正常" } elseif ($memPercent -lt 90) { "警告" } else { "严重" }
$results += [PSCustomObject]@{
Category = "内存"
Item = "使用率"
Value = "$memPercent%"
Status = $status
}
}
# 磁盘信息
elseif ($trimmed -match '^--- 磁盘 ---') {
# 进入磁盘区域
$currentSection = "磁盘"
}
elseif ($trimmed -match '^(/\S+|/[a-zA-Z]\S*)\s+([\d.]+[A-Z])\s+([\d.]+[A-Z])\s+\[([\d.]+)%\]\s+(\S+)') {
$mountpoint = $matches[1]
$size = $matches[2]
$used = $matches[3]
$percent = [int]$matches[4]
$status = $matches[5]
$results += [PSCustomObject]@{
Category = "磁盘"
Item = $mountpoint
Value = "$used/$size ($percent%)"
Status = $status
}
}
# 网络信息
elseif ($trimmed -match '^--- 网络 ---') {
# 进入网络区域
$currentSection = "网络"
}
elseif ($trimmed -match '^TCP连接数:\s*(\d+)') {
$tcpCount = $matches[1]
$results += [PSCustomObject]@{
Category = "网络"
Item = "TCP连接数"
Value = $tcpCount
Status = "正常"
}
}
elseif ($trimmed -match '^TIME_WAIT数:\s*(\d+)') {
$timeWait = $matches[1]
$results += [PSCustomObject]@{
Category = "网络"
Item = "TIME_WAIT数"
Value = $timeWait
Status = if ([int]$timeWait -gt 1000) { "警告" } else { "正常" }
}
}
}
# 添加内存总计和已用信息(如果有的话)
if ($memTotal -gt 0) {
$results += [PSCustomObject]@{
Category = "内存"
Item = "总计"
Value = "$memTotal"+"GB"
Status = "正常"
}
}
if ($memUsed -gt 0) {
$results += [PSCustomObject]@{
Category = "内存"
Item = "已用"
Value = "$memUsed"+"GB"
Status = "正常"
}
}
return $results
}
# ==============================================================================
# 中间件检测(Shell模式)
# ==============================================================================
function Test-MQTTConnection-Shell {
<#
.SYNOPSIS
Shell 模式的中间件检测
.DESCRIPTION
通过 middleware_check.sh 脚本检测 Redis、MySQL、EMQX、FastDFS 中间件状态。
.PARAMETER Server
服务器连接信息哈希表
.PARAMETER EmqxLogPath
EMQX 日志路径(可选)
.OUTPUTS
PSCustomObject 数组
#>
param(
[Parameter(Mandatory=$true)] [hashtable]$Server,
[Parameter(Mandatory=$false)] [string]$EmqxLogPath = ""
)
Write-Host ""
Write-Log -Level "INFO" -Message "========== 开始中间件检测 (Shell模式) =========="
$arguments = "--format json --check all"
$result = Invoke-RemoteShellCheck -Server $Server -ScriptName "middleware_check.sh" -Arguments $arguments
$data = ConvertFrom-ShellJson -JsonString $result
if (-not $data) {
Write-Log -Level "ERROR" -Message "[中间件] Shell脚本执行失败"
return @()
}
$results = @()
# Redis检测结果(包含详细信息)
if ($data.redis) {
$redisStatus = $data.redis.status
$icon = if ($redisStatus -eq "running") { "[运行]" } else { "[停止]" }
$redisSuccess = ($redisStatus -eq "running")
# 构建详细信息字符串
$redisDetails = ""
if ($redisSuccess) {
$version = if ($data.redis.version) { $data.redis.version } else { "unknown" }
$memory = if ($data.redis.memory) { $data.redis.memory } else { "N/A" }
$clients = if ($data.redis.clients) { $data.redis.clients } else { "0" }
$redisDetails = "Redis服务完全正常(连接+读写+删除+信息采集) | 容器: $($data.redis.container) | 端口: $($data.redis.port) | 版本: $version | 内存: $memory | 连接数: $clients"
Write-Log -Level "INFO" -Message " $icon Redis ($($data.redis.container):$($data.redis.port)): $redisStatus | 版本: $version | 内存: $memory | 连接: $clients"
} else {
$redisDetails = if ($redisStatus -eq "stopped") { "容器未运行" } elseif ($redisStatus -eq "not_found") { "容器不存在" } else { "连接失败" }
Write-Log -Level "INFO" -Message " $icon Redis ($($data.redis.container):$($data.redis.port)): $redisStatus"
}
$results += [PSCustomObject]@{
Check = "Redis连接检测"
Status = if ($redisSuccess) { "正常" } else { "异常" }
Details = $redisDetails
Success = $redisSuccess
}
}
# MySQL检测结果(包含详细信息)
if ($data.mysql) {
$mysqlStatus = $data.mysql.status
$icon = if ($mysqlStatus -eq "running") { "[运行]" } else { "[停止]" }
$mysqlSuccess = ($mysqlStatus -eq "running")
# 构建详细信息字符串
$mysqlDetails = ""
if ($mysqlSuccess) {
$version = if ($data.mysql.version) { $data.mysql.version } else { "unknown" }
$mysqlDetails = "MySQL连接正常 | 容器: $($data.mysql.container) | 端口: $($data.mysql.port) | 版本: $version"
Write-Log -Level "INFO" -Message " $icon MySQL ($($data.mysql.container):$($data.mysql.port)): $mysqlStatus | 版本: $version"
} else {
$mysqlDetails = if ($mysqlStatus -eq "stopped") { "容器未运行" } elseif ($mysqlStatus -eq "not_found") { "容器不存在" } else { "连接失败" }
Write-Log -Level "INFO" -Message " $icon MySQL ($($data.mysql.container):$($data.mysql.port)): $mysqlStatus"
}
$results += [PSCustomObject]@{
Check = "MySQL连接检测"
Status = if ($mysqlSuccess) { "正常" } else { "异常" }
Details = $mysqlDetails
Success = $mysqlSuccess
}
}
# EMQX检测结果(包含详细信息)
if ($data.emqx) {
$emqxStatus = $data.emqx.status
$icon = if ($emqxStatus -eq "running") { "[运行]" } else { "[停止]" }
$emqxSuccess = ($emqxStatus -eq "running")
# 构建详细信息字符串
$emqxDetails = ""
if ($emqxSuccess) {
$dashboardPort = if ($data.emqx.dashboard_port) { $data.emqx.dashboard_port } else { "N/A" }
$emqxDetails = "MQTT服务连接正常 | 容器: $($data.emqx.container) | 端口: $($data.emqx.port) | Dashboard: $dashboardPort"
Write-Log -Level "INFO" -Message " $icon EMQX ($($data.emqx.container):$($data.emqx.port)): $emqxStatus | Dashboard: $dashboardPort"
} else {
$emqxDetails = if ($emqxStatus -eq "stopped") { "容器未运行" } elseif ($emqxStatus -eq "not_found") { "容器不存在" } else { "连接失败" }
Write-Log -Level "INFO" -Message " $icon EMQX ($($data.emqx.container):$($data.emqx.port)): $emqxStatus"
}
$results += [PSCustomObject]@{
Check = "MQTT/EMQX连接检测"
Status = if ($emqxSuccess) { "正常" } else { "异常" }
Details = $emqxDetails
Success = $emqxSuccess
}
}
# FastDFS检测结果(包含详细信息)
if ($data.fastdfs) {
$fastdfsStatus = $data.fastdfs.status
$icon = if ($fastdfsStatus -eq "running") { "[运行]" } else { "[停止]" }
$fastdfsSuccess = ($fastdfsStatus -eq "running")
# 构建详细信息字符串
$fastdfsDetails = ""
if ($fastdfsSuccess) {
$network = if ($data.fastdfs.network) { $data.fastdfs.network } else { "unknown" }
$fastdfsDetails = "FastDFS存储服务正常 | 容器: $($data.fastdfs.container) | 网络模式: $network"
Write-Log -Level "INFO" -Message " $icon FastDFS ($($data.fastdfs.container)): $fastdfsStatus | 网络: $network"
} else {
$fastdfsDetails = if ($fastdfsStatus -eq "stopped") { "服务未运行" } elseif ($fastdfsStatus -eq "not_found") { "容器不存在" } else { "检测失败" }
Write-Log -Level "INFO" -Message " $icon FastDFS ($($data.fastdfs.container)): $fastdfsStatus"
}
$results += [PSCustomObject]@{
Check = "FastDFS存储检测"
Status = if ($fastdfsSuccess) { "正常" } else { "异常" }
Details = $fastdfsDetails
Success = $fastdfsSuccess
}
}
Write-Log -Level "INFO" -Message "========== 结束中间件检测 (Shell模式) =========="
return $results
}
# ==============================================================================
# DNS检测(Shell模式)
# ==============================================================================
function Test-DNSResolution-Shell {
<#
.SYNOPSIS
Shell 模式的 DNS 解析检测
.PARAMETER Server
服务器连接信息哈希表
.OUTPUTS
PSCustomObject 数组
#>
param(
[Parameter(Mandatory=$true)] [hashtable]$Server
)
Write-Host ""
Write-Log -Level "INFO" -Message "========== 开始DNS检测 (Shell模式) =========="
$result = Invoke-RemoteShellCheck -Server $Server -ScriptName "dns_check.sh" -Arguments "--format json"
$data = ConvertFrom-ShellJson -JsonString $result
if (-not $data) {
Write-Log -Level "ERROR" -Message "[DNS] Shell脚本执行失败"
return @()
}
$results = @()
if ($data.results) {
foreach ($domain in $data.results) {
$icon = if ($domain.status -eq "success") { "[OK]" } else { "[FAIL]" }
Write-Log -Level "INFO" -Message " $icon $($domain.domain) -> $($domain.ip)"
$results += [PSCustomObject]@{
Check = $domain.domain
Status = if ($domain.status -eq "success") { "正常" } else { "异常" }
Details = if ($domain.ip) { $domain.ip } else { "解析失败" }
Success = ($domain.status -eq "success")
}
}
}
# 添加DNS服务器信息到结果中
$dnsServer = if ($data.dns_server) { $data.dns_server } else { "unknown" }
$results += [PSCustomObject]@{
Check = "DNS配置"
Status = "正常"
Details = "DNS服务器: $dnsServer"
Success = $true
Type = "DNSConfig"
}
Write-Log -Level "INFO" -Message "DNS服务器: $dnsServer"
Write-Log -Level "INFO" -Message "========== 结束DNS检测 (Shell模式) =========="
return $results
}
# ==============================================================================
# NTP检测(Shell模式)
# ==============================================================================
function Test-NTPService-Shell {
<#
.SYNOPSIS
Shell 模式的 NTP 服务检测
.DESCRIPTION
直接通过 SSH 命令检测 NTP 服务状态和时间同步情况。
.PARAMETER Server
服务器连接信息哈希表
.OUTPUTS
PSCustomObject 数组
#>
param(
[Parameter(Mandatory=$true)] [hashtable]$Server
)
Write-Host ""
Write-Log -Level "INFO" -Message "========== 开始NTP检测 (Shell模式) =========="
# 直接执行SSH命令获取NTP信息,避免Shell脚本输出截断问题
$cmd = "export LANG=C && timedatectl status 2>/dev/null | grep -E 'System clock synchronized|NTP service'"
$result = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $cmd
if (-not $result -or -not $result.Output) {
Write-Log -Level "ERROR" -Message "[NTP] 无法获取NTP状态"
return $null
}
# 解析输出
$output = if ($result.Output -is [array]) { $result.Output -join "`n" } else { $result.Output.ToString() }
$ntpService = "unknown"
$serviceStatus = "stopped"
$timeSync = "unknown"
# 检查NTP服务状态
if ($output -match "NTP service:\s+active") {
$ntpService = "chronyd"
$serviceStatus = "running"
} elseif ($output -match "NTP service:\s+inactive") {
$serviceStatus = "stopped"
}
# 检查时间同步状态
if ($output -match "System clock synchronized:\s+yes") {
$timeSync = "synchronized"
} elseif ($output -match "System clock synchronized:\s+no") {
$timeSync = "not_synchronized"
}
# 获取NTP服务器(限制数量以避免输出过长)
# 使用单引号避免PowerShell变量展开
$cmd2 = @'
export LANG=C && cat /etc/chrony.conf 2>/dev/null | grep '^server ' | awk '{print $2}' | head -1
'@
$result2 = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $cmd2
$ntpServers = if ($result2.Output) { $result2.Output -join "" } else { "unknown" }
$icon = if ($serviceStatus -eq "running") { "[运行]" } else { "[停止]" }
Write-Log -Level "INFO" -Message " $icon NTP服务: $ntpService ($serviceStatus)"
if ($timeSync -eq "synchronized") {
Write-Log -Level "SUCCESS" -Message " [OK] 时间同步: 已同步"
} else {
Write-Log -Level "WARN" -Message " [WARN] 时间同步: 未同步"
}
Write-Log -Level "INFO" -Message " NTP服务器: $ntpServers"
$results = @()
$results += [PSCustomObject]@{
Service = "NTP"
Daemon = $ntpService
Status = $serviceStatus
TimeSync = $timeSync
}
Write-Log -Level "INFO" -Message "========== 结束NTP检测 (Shell模式) =========="
return $results
}
# ==============================================================================
# 防火墙检测(Shell模式)
# ==============================================================================
function Test-Firewall-Shell {
<#
.SYNOPSIS
Shell 模式的防火墙检测
.PARAMETER Server
服务器连接信息哈希表
.OUTPUTS
System.Collections.Hashtable
包含 Status, Description, OpenPorts 的结果哈希表
#>
param(
[Parameter(Mandatory=$true)] [hashtable]$Server
)
Write-Host ""
Write-Log -Level "INFO" -Message "========== 开始防火墙检测 (Shell模式) =========="
# 检测防火墙状态
$cmd = "export LANG=C && systemctl is-active firewalld 2>/dev/null || echo 'inactive'"
$result = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $cmd
$firewallStatus = "unknown"
$firewallDescription = "未知"
if ($result.Output) {
$output = if ($result.Output -is [array]) { $result.Output -join "" } else { $result.Output.ToString() }
$output = $output.Trim()
if ($output -eq "active") {
$firewallStatus = "active"
$firewallDescription = "已启用 (firewalld)"
Write-Log -Level "INFO" -Message " 防火墙状态: $firewallDescription"
} elseif ($output -eq "inactive") {
$firewallStatus = "inactive"
$firewallDescription = "未启用"
Write-Log -Level "WARN" -Message " 防火墙状态: $firewallDescription"
}
}
# 获取开放的端口和服务
$openPorts = ""
if ($firewallStatus -eq "active") {
$cmd2 = "export LANG=C && firewall-cmd --list-ports 2>/dev/null && firewall-cmd --list-services 2>/dev/null"
$result2 = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $cmd2
if ($result2.Output) {
$portsOutput = if ($result2.Output -is [array]) { $result2.Output -join " " } else { $result2.Output.ToString() }
$openPorts = $portsOutput -replace "`n", " " -replace "\s+", " "
Write-Log -Level "INFO" -Message " 开放端口/服务: $openPorts"
}
}
$results = @{
Status = $firewallStatus
Description = $firewallDescription
OpenPorts = $openPorts
}
Write-Log -Level "INFO" -Message "========== 结束防火墙检测 (Shell模式) =========="
return $results
}
# ==============================================================================
# 配置IP检测(Shell模式)
# ==============================================================================
function Test-ConfigIPs-Shell {
<#
.SYNOPSIS
Shell 模式的配置文件 IP 检测
.DESCRIPTION
检查远程服务器配置文件中的硬编码 IP 地址。
.PARAMETER Server
服务器连接信息哈希表
.PARAMETER PlatformType
平台类型 ("new" 或 "old")
.OUTPUTS
PSCustomObject 数组
#>
param(
[Parameter(Mandatory=$true)] [hashtable]$Server,
[Parameter(Mandatory=$false)] [string]$PlatformType
)
Write-Host ""
Write-Log -Level "INFO" -Message "========== 开始配置IP检测 (Shell模式) =========="
$results = @()
# 根据平台类型确定配置文件路径
$configFiles = @()
if ($PlatformType -eq "new") {
$configFiles = @(
"/data/middleware/nginx/config/*.conf",
"/data/middleware/emqx/config/*.conf",
"/data/middleware/mysql/conf/my.cnf"
)
} else {
$configFiles = @(
"/var/www/java/nginx-conf.d/*.conf",
"/var/www/emqx/config/*.conf"
)
}
# 检查每个配置文件
$ipPattern = '\b([0-9]{1,3}\.){3}[0-9]\b'
foreach ($configPattern in $configFiles) {
# 先展开通配符
$cmd = "export LANG=C && ls $configPattern 2>/dev/null"
$result = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $cmd
if (-not $result.Output) { continue }
$files = $result.Output -split "`n" | Where-Object { $_ -ne "" }
foreach ($file in $files) {
# 检查文件中的IP
$cmd2 = "export LANG=C && grep -oE '$ipPattern' '$file' 2>/dev/null | sort -u | head -5"
$result2 = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $cmd2
if ($result2.Output) {
$ipsOutput = if ($result2.Output -is [array]) { $result2.Output -join "" } else { $result2.Output.ToString() }
$ips = @($ipsOutput -split "`n" | Where-Object { $_ -match $ipPattern })
if ($ips.Count -gt 0) {
Write-Log -Level "INFO" -Message " [包含IP] $file"
foreach ($ip in $ips) {
$results += [PSCustomObject]@{
File = $file
IP = $ip.Trim()
}
}
}
}
}
}
Write-Log -Level "INFO" -Message "汇总: 检测 $($results.Count) 个IP地址"
Write-Log -Level "INFO" -Message "========== 结束配置IP检测 (Shell模式) =========="
return $results
}
# ==============================================================================
# Java服务检测(Shell模式)
# ==============================================================================
function Test-UjavaServices-Shell {
<#
.SYNOPSIS
Shell 模式的 Java 服务检测
.PARAMETER Server
服务器连接信息哈希表
.PARAMETER ContainerName
容器名称
.PARAMETER PlatformType
平台类型,默认 "new"
.OUTPUTS
PSCustomObject 数组
#>
param(
[Parameter(Mandatory=$true)] [hashtable]$Server,
[Parameter(Mandatory=$false)] [string]$ContainerName,
[Parameter(Mandatory=$false)] [string]$PlatformType = "new"
)
Write-Host ""
if ($ContainerName) {
Write-Log -Level "INFO" -Message "========== 检测 ujava 服务 (容器: $ContainerName) (Shell模式) =========="
}
else {
Write-Log -Level "INFO" -Message "========== 检测 ujava 服务 (宿主机) (Shell模式) =========="
}
$arguments = "--container $ContainerName --platform $PlatformType --format json"
$result = Invoke-RemoteShellCheck -Server $Server -ScriptName "java_check.sh" -Arguments $arguments
$data = ConvertFrom-ShellJson -JsonString $result
if (-not $data) {
Write-Log -Level "ERROR" -Message "[UJAVA] Shell脚本执行失败"
return @()
}
$results = @()
if ($data.services) {
foreach ($service in $data.services) {
$statusIcon = if ($service.status -eq "运行中") { "[OK]" } else { "[FAIL]" }
Write-Log -Level "INFO" -Message " $statusIcon $($service.name) ($($service.jar)) [$($service.location)]: $($service.status)"
$results += [PSCustomObject]@{
Service = $service.name
Pattern = $service.jar
Status = $service.status
Running = ($service.status -eq "运行中")
}
}
}
Write-Log -Level "INFO" -Message "========== 结束检测 (Shell模式) =========="
return $results
}
# ==============================================================================
# 导出模块函数
# ==============================================================================
Export-ModuleMember -Function @(
'Invoke-RemoteShellCheck',
'ConvertFrom-ShellJson',
'Test-ServerResources-Shell',
'Convert-ResourceCheckToStandard',
'Parse-ResourceCheckText',
'Test-MQTTConnection-Shell',
'Test-DNSResolution-Shell',
'Test-NTPService-Shell',
'Test-Firewall-Shell',
'Test-ConfigIPs-Shell',
'Test-UjavaServices-Shell'
)
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论