本工具通过 PowerShell 和 CMD 命令采集 Windows 系统的 27 类关键信息,自动生成 HTML 和 Word 双格式巡检报告,参考 MySQL 运维报告的样式,突出风险等级与整改建议。
功能特性
- ✅ 27 项全面检查:涵盖系统信息、硬件资源、网络配置、安全审计、用户权限、进程服务、启动项、计划任务、已安装软件、事件日志等。
- ✅ 双格式报告 :同时输出
.html和.doc文件,内容一致,后者可直接用 Microsoft Word 打开。 - ✅ 风险可视化:自动评估综合风险等级(高/中/低),计算安全评分(满分100分)。
- ✅ 智能事件解读:内置常见 Windows 事件知识库,对日志进行精准说明。
- ✅ 零依赖:仅需 Python 3.6+,无需额外安装第三方库。
使用方法
1. 准备工作
- 操作系统:Windows 7 / Windows Server 2008 R2 及以上(建议 Windows 10/11、Windows Server 2016+)
- Python 环境:Python 3.6 或更高版本(下载 Python)
- 权限:建议以管理员身份运行,否则部分信息(如防火墙状态、部分注册表项)可能无法采集。
2. 运行脚本
cmd
# 下载脚本后,在命令提示符(管理员)中执行
python windows_inspection.py
3. 获取报告
脚本会在当前目录生成两个文件:
System_Inspection_Report_YYYYMMDD_HHMMSS.html-- 网页版报告System_Inspection_Report_YYYYMMDD_HHMMSS.doc-- Word 兼容版报告(直接双击打开)
报告内容概览
| 章节 | 主要内容 |
|---|---|
| 主机基本信息 | 计算机名、操作系统、BIOS、许可证状态、运行时间、安全启动等 |
| 硬件资源状态 | CPU/内存/磁盘/GPU 详细信息、使用率、物理磁盘健康度 |
| 网络配置与连接 | 网卡状态、IP 地址、监听端口、共享文件夹、连接统计 |
| 安全配置审计 | 防火墙、远程桌面、BitLocker、密码策略、系统更新、Defender 状态 |
| 用户与权限 | 本地用户账户、管理员组成员列表 |
| 进程与服务分析 | 进程总数、运行中服务数、内存占用 Top 10 进程 |
| 启动项与计划任务 | 注册表启动项、非微软计划任务(含命令) |
| 已安装软件 | 软件名称、版本、发布者、安装日期(最多显示 25 个) |
| 事件日志分析 | 系统与应用的错误事件聚合、严重程度判定、知识库解读 |
| 风险评估与建议 | 发现的问题清单、综合风险等级、安全评分、具体修复建议 |
输出示例
-
报告封面
-
安全评分与风险等级
- 绿色(≥80分):状态良好
- 黄色(60~79分):存在中低风险,需关注
- 红色(<60分):高风险,建议立即处理
-
表格样式
所有数据均以表格形式呈现,关键指标附带进度条(如磁盘使用率)。
注意事项
- 执行策略 :若遇到 PowerShell 执行策略限制,脚本已自动绕过(
-ExecutionPolicy Bypass),但仍建议以管理员身份运行。 - 采集耗时:首次运行或系统软件较多时,脚本可能需要 30~60 秒完成采集,请耐心等待。
- 事件日志限制:仅采集最近 200 条错误级别日志(System 和 Application),若历史错误过多,报告会按事件 ID 聚合展示。
- Word 报告兼容性 :
.doc文件实为 HTML 格式,Word 打开时可能会提示"文件格式不匹配",选择"是"即可正常显示。 - 语言环境:脚本自动适配中文/英文系统输出,部分命令输出可能为英文,但报告内已做中文化映射。
- 网络依赖:不依赖互联网,所有数据均从本机采集。
自定义修改
- 添加新检查项 :在
collect_all()函数中按现有风格添加 PowerShell 或 cmd 命令。 - 调整风险判定规则 :修改
issues列表中的条件或suggestions列表中的建议。 - 修改事件知识库 :编辑
EVENT_KB字典,键为事件ID|提供程序名称,值为(严重程度, 说明)。 - 变更报告样式 :直接修改
generate_html()函数中的 CSS 样式块。
常见问题
Q:提示"'powershell' 不是内部或外部命令"
A:系统环境变量 PATH 中缺少 PowerShell 路径,请检查 Windows 安装是否完整。
Q:部分信息显示"N/A"或空白
A:可能是权限不足或系统组件缺失,建议以管理员身份重新运行。
Q:生成的 Word 报告打开排版错乱
A:请使用 Microsoft Word 2010 以上版本打开,或直接用浏览器打开 HTML 文件。
Q:能否在 Linux 上运行?
A:不可以,本脚本依赖 Windows 特有的 WMI 和 PowerShell 命令。
许可说明
本脚本仅供内部运维使用,可自由修改和分发。如用于生产环境,建议先在测试机验证。
版本 :1.0
更新日期 :2026-05-19
适用平台:Windows
脚本如下:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Windows 系统一键巡检脚本 - 支持 HTML + Word 双报告 (27项)
"""
import os
import platform
import socket
import datetime
import subprocess
import html as html_mod
from typing import Dict, Any, List, Tuple
def run_command(cmd: str, timeout: int = 30) -> Dict[str, Any]:
try:
result = subprocess.run(cmd, capture_output=True, shell=True, timeout=timeout)
stdout, stderr = '', ''
for enc in ('utf-8', 'gbk', 'latin-1'):
try:
stdout = result.stdout.decode(enc)
break
except:
continue
for enc in ('utf-8', 'gbk', 'latin-1'):
try:
stderr = result.stderr.decode(enc)
break
except:
continue
return {'success': result.returncode == 0, 'stdout': stdout.replace('\x00', ''),
'stderr': stderr.replace('\x00', '')}
except Exception as e:
return {'success': False, 'stdout': '', 'stderr': str(e)}
def ps(cmd: str, timeout: int = 30) -> str:
import tempfile
tmp = os.path.join(tempfile.gettempdir(), '_insp.ps1')
with open(tmp, 'w', encoding='utf-8-sig') as f:
f.write(f'[Console]::OutputEncoding = [System.Text.Encoding]::UTF8\n{cmd}')
r = run_command(f'powershell -NoProfile -ExecutionPolicy Bypass -File "{tmp}"', timeout)
try:
os.remove(tmp)
except:
pass
return r['stdout'].strip() if r['success'] else r['stderr'].strip()
def esc(t): return html_mod.escape(str(t))
def status_badge(text, color):
colors = {'green': '#34a853', 'yellow': '#f9ab00', 'red': '#ea4335', 'blue': '#1a73e8', 'gray': '#9aa0a6'}
c = colors.get(color, colors['gray'])
return f'<span style="background:{c};color:#fff;padding:2px 10px;border-radius:4px;font-size:12px;font-weight:600">{esc(text)}</span>'
def pct_bar(pct, width=100):
color = '#34a853' if pct < 70 else '#f9ab00' if pct < 90 else '#ea4335'
return f'<div style="background:#e0e0e0;border-radius:4px;height:18px;width:{width}px;display:inline-block;vertical-align:middle"><div style="background:{color};height:100%;width:{pct}%;border-radius:4px"></div></div> <b>{pct}%</b>'
# ==================== 数据采集(精简) ====================
def collect_all() -> Dict[str, Any]:
d = {}
# 1. 系统基本信息
d['hostname'] = socket.gethostname()
d['os'] = f"Windows {platform.release()} (v{platform.version()})"
d['arch'] = f"{platform.architecture()[0]} {platform.machine()}"
d['user'] = f"{os.environ.get('USERDOMAIN', '')}\\{os.environ.get('USERNAME', '')}"
d['cpus'] = os.environ.get('NUMBER_OF_PROCESSORS', 'N/A')
# 2. 运行时间
d['boot_time'] = ps("(Get-CimInstance Win32_OperatingSystem).LastBootUpTime.ToString('yyyy-MM-dd HH:mm:ss')")
try:
boot = datetime.datetime.strptime(d['boot_time'].strip(), '%Y-%m-%d %H:%M:%S')
delta = datetime.datetime.now() - boot
d['uptime'] = f"{delta.days}天 {int(delta.total_seconds() % 86400 // 3600)}时 {int(delta.total_seconds() % 3600 // 60)}分"
except:
d['uptime'] = 'N/A'
# 3. 许可证
lic = ps('cscript //Nologo "$env:SystemRoot\\System32\\slmgr.vbs" /dli 2>&1')
d['license_status'] = '已授权' if '已授权' in lic or 'Licensed' in lic else '未授权/未知'
d['license_type'] = 'KMS' if 'KMS' in lic else 'MAK' if 'MAK' in lic else 'Retail' if 'RETAIL' in lic else '未知'
# 提取过期时间
for line in lic.split('\n'):
if '分钟' in line and '过期' in line:
d['license_expire'] = line.split(':')[-1].strip() if ':' in line else line.strip()
break
else:
d['license_expire'] = 'N/A'
# 4. BIOS
d['bios'] = ps("$b=Get-CimInstance Win32_BIOS; Write-Output \"$($b.Manufacturer) | $($b.SMBIOSBIOSVersion) | $($b.ReleaseDate.ToString('yyyy-MM-dd'))\"")
d['motherboard'] = ps("$c=Get-CimInstance Win32_ComputerSystem; Write-Output \"$($c.Manufacturer) $($c.Model)\"")
d['secure_boot'] = ps("try{if(Confirm-SecureBootUEFI){'已启用'}else{'已禁用'}}catch{'不支持/Legacy BIOS'}")
# 5. CPU
cpu = ps("$c=Get-CimInstance Win32_Processor; Write-Output \"$($c.Name)|$($c.NumberOfCores)|$($c.NumberOfLogicalProcessors)|$($c.MaxClockSpeed)|$($c.LoadPercentage)\"")
parts = cpu.split('|') if '|' in cpu else ['N/A'] * 5
d['cpu_name'] = parts[0].strip() if len(parts) > 0 else 'N/A'
d['cpu_cores'] = parts[1].strip() if len(parts) > 1 else 'N/A'
d['cpu_threads'] = parts[2].strip() if len(parts) > 2 else 'N/A'
d['cpu_freq'] = parts[3].strip() if len(parts) > 3 else 'N/A'
d['cpu_load'] = parts[4].strip() if len(parts) > 4 else 'N/A'
# 6. 内存
mem = ps('$o=Get-CimInstance Win32_OperatingSystem;$t=[math]::Round($o.TotalVisibleMemorySize/1MB,2);$f=[math]::Round($o.FreePhysicalMemory/1MB,2);Write-Output "$t|$f"')
mp = mem.split('|') if '|' in mem else ['0', '0']
d['mem_total'] = float(mp[0]) if mp[0] else 0
d['mem_free'] = float(mp[1]) if mp[1] else 0
d['mem_used'] = round(d['mem_total'] - d['mem_free'], 2)
d['mem_pct'] = round(d['mem_used'] / d['mem_total'] * 100, 1) if d['mem_total'] > 0 else 0
chips = ps(
"Get-CimInstance Win32_PhysicalMemory|ForEach-Object{Write-Output \"$($_.Manufacturer)|$([math]::Round($_.Capacity/1GB,0))GB|$($_.Speed)MHz|$($_.PartNumber.Trim())\"}")
d['mem_chips'] = [c.strip() for c in chips.split('\n') if c.strip()] if chips else []
# 7. 磁盘
dk = ps(
"Get-CimInstance Win32_LogicalDisk -Filter 'DriveType=3'|ForEach-Object{$t=[math]::Round($_.Size/1GB,1);$f=[math]::Round($_.FreeSpace/1GB,1);$u=[math]::Round($t-$f,1);$p=if($t-gt 0){[math]::Round($u/$t*100,1)}else{0};Write-Output \"$($_.DeviceID)|$($_.VolumeName)|$t|$f|$u|$p\"}")
d['disks'] = []
for line in (dk.split('\n') if dk else []):
p = line.strip().split('|')
if len(p) >= 6:
d['disks'].append({'drive': p[0], 'label': p[1], 'total': p[2], 'free': p[3], 'used': p[4], 'pct': p[5]})
# 8. GPU
d['gpu'] = ps("(Get-CimInstance Win32_VideoController).Name")
d['gpu_driver'] = ps("(Get-CimInstance Win32_VideoController).DriverVersion")
unsigned = ps("(Get-CimInstance Win32_PnPSignedDriver -EA SilentlyContinue|Where-Object{$_.IsSigned -eq $false}).Count")
d['unsigned_drivers'] = unsigned if unsigned and unsigned != '0' else '0'
# 9-11. 网络精简
d['net_adapters'] = []
adapters = ps(
"Get-NetAdapter -Physical -EA SilentlyContinue|ForEach-Object{Write-Output \"$($_.Name)|$($_.Status)|$($_.LinkSpeed)|$($_.MacAddress)\"}")
for line in (adapters.split('\n') if adapters else []):
p = line.strip().split('|')
if len(p) >= 4:
d['net_adapters'].append({'name': p[0], 'status': p[1], 'speed': p[2], 'mac': p[3]})
ips = ps(
"Get-NetIPAddress -AddressFamily IPv4 -EA SilentlyContinue|Where-Object{$_.IPAddress -ne '127.0.0.1' -and $_.PrefixOrigin -ne 'WellKnown'}|Sort-Object InterfaceAlias|ForEach-Object{Write-Output \"$($_.InterfaceAlias)|$($_.IPAddress)|$($_.PrefixLength)|$($_.PrefixOrigin)\"}")
d['ipv4_addrs'] = []
for line in (ips.split('\n') if ips else []):
p = line.strip().split('|')
if len(p) >= 4:
d['ipv4_addrs'].append({'iface': p[0], 'ip': p[1], 'prefix': p[2], 'origin': p[3]})
d['gateway'] = ps("(Get-NetRoute -DestinationPrefix '0.0.0.0/0' -EA SilentlyContinue|Select-Object -First 1).NextHop")
d['dns'] = ps("(Get-DnsClientServerAddress -AddressFamily IPv4 -EA SilentlyContinue|Where-Object{$_.ServerAddresses}|Select-Object -First 1).ServerAddresses -join ', '")
conn_out = run_command('netstat -an')
if conn_out['success']:
lines = conn_out['stdout'].split('\n')
d['conn_established'] = sum(1 for l in lines if 'ESTABLISHED' in l)
d['conn_listening'] = sum(1 for l in lines if 'LISTENING' in l)
d['conn_timewait'] = sum(1 for l in lines if 'TIME_WAIT' in l)
else:
d['conn_established'] = d['conn_listening'] = d['conn_timewait'] = 0
# 10. 监听端口(精简+进程说明)
ports = ps(
"Get-NetTCPConnection -State Listen -EA SilentlyContinue|Select-Object LocalAddress,LocalPort,@{N='Process';E={(Get-Process -Id $_.OwningProcess -EA SilentlyContinue).ProcessName}}|Sort-Object LocalPort|ForEach-Object{Write-Output \"$($_.LocalPort)|$($_.Process)|$($_.LocalAddress)\"}",
timeout=45)
d['listen_ports'] = []
seen = set()
PORT_DESC = {
'135': 'RPC端点映射', '139': 'NetBIOS会话', '445': 'SMB文件共享', '902': 'VMware远程控制台',
'912': 'VMware认证', '3128': 'HTTP代理', '3389': '远程桌面(RDP)', '5040': 'Windows推送通知',
'5357': 'Web服务发现', '6000': 'X Window', '7680': '传递优化(WUDO)', '7897': '***代理',
'8080': 'HTTP代理', '8443': 'HTTPS', '22': 'SSH', '80': 'HTTP', '443': 'HTTPS',
'53': 'DNS', '1433': 'SQL Server', '3306': 'MySQL', '5432': 'PostgreSQL', '6379': 'Redis',
'8475': '百度网盘', '10000': '百度网盘云检测', '33331': '*** Verge', '35600': 'ToDesk远控',
'37600': 'ToDesk远控', '49664': '系统服务(动态)', '49665': '系统服务(动态)',
'49666': '系统服务(动态)', '49667': '系统服务(动态)', '49668': '系统服务(动态)',
'49669': '系统服务(动态)', '49684': '系统服务(动态)',
}
for line in (ports.split('\n') if ports else []):
p = line.strip().split('|')
if len(p) >= 2 and p[0] not in seen:
seen.add(p[0])
addr = p[2] if len(p) >= 3 else ''
scope = '本机' if addr in ('127.0.0.1', '::1') else '全部' if addr in ('0.0.0.0', '::') else addr
desc = PORT_DESC.get(p[0], '')
d['listen_ports'].append({'port': p[0], 'process': p[1], 'scope': scope, 'desc': desc})
# 12. 共享
shares = ps("Get-SmbShare -EA SilentlyContinue|ForEach-Object{Write-Output \"$($_.Name)|$($_.Path)|$($_.Description)|$($_.CurrentUsers)\"}")
d['shares'] = []
for line in (shares.split('\n') if shares else []):
p = line.strip().split('|')
if len(p) >= 4:
d['shares'].append({'name': p[0], 'path': p[1], 'desc': p[2], 'users': p[3]})
# 13. 进程Top10
out2 = run_command('tasklist /fo csv /nh')
procs = []
if out2['success']:
for line in out2['stdout'].strip().split('\n'):
p = line.strip().strip('"').split('","')
if len(p) >= 5:
try:
procs.append((p[0], int(p[4].replace('"', '').replace(' K', '').replace(',', ''))))
except:
pass
procs.sort(key=lambda x: x[1], reverse=True)
d['proc_count'] = len(procs)
d['proc_top10'] = procs[:10]
svc_count = ps("(Get-Service|Where-Object{$_.Status -eq 'Running'}).Count")
d['svc_running'] = svc_count if svc_count else 'N/A'
# 14. 启动项(精简)
startup = ps("""
$items=@()
'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run','HKCU:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run'|ForEach-Object{
if(Test-Path $_){(Get-ItemProperty $_ -EA SilentlyContinue).PSObject.Properties|Where-Object{$_.Name -notlike 'PS*'}|ForEach-Object{$items+="$($_.Name)|$($_.Value)"}}
}
$items -join "`n"
""")
d['startup_items'] = []
for line in (startup.split('\n') if startup else []):
p = line.strip().split('|', 1)
if len(p) == 2:
d['startup_items'].append({'name': p[0], 'cmd': p[1]})
# 15. 计划任务(非微软)
tasks = ps(
"Get-ScheduledTask -EA SilentlyContinue|Where-Object{$_.TaskPath -notlike '\\Microsoft\\*' -and $_.State -ne 'Disabled'}|ForEach-Object{$a=($_.Actions|ForEach-Object{$_.Execute}) -join ';';Write-Output \"$($_.TaskName)|$($_.State)|$a\"}",
timeout=45)
d['sched_tasks'] = []
for line in (tasks.split('\n') if tasks else []):
p = line.strip().split('|')
if len(p) >= 3:
d['sched_tasks'].append({'name': p[0], 'state': p[1], 'action': p[2]})
d['sched_total'] = ps("(Get-ScheduledTask -EA SilentlyContinue).Count")
# 16. 更新
updates = ps(
"Get-HotFix|Sort-Object InstalledOn -Desc -EA SilentlyContinue|Select-Object -First 5|ForEach-Object{Write-Output \"$($_.HotFixID)|$($_.Description)|$($_.InstalledOn.ToString('yyyy-MM-dd'))\"}")
d['updates'] = []
for line in (updates.split('\n') if updates else []):
p = line.strip().split('|')
if len(p) >= 3:
d['updates'].append({'id': p[0], 'type': p[1], 'date': p[2]})
fw_out = ps("Get-NetFirewallProfile | ForEach-Object { Write-Output \"$($_.Name)|$($_.Enabled)\" }")
d['fw_domain'] = d['fw_private'] = d['fw_public'] = 'OFF'
for line in (fw_out.split('\n') if fw_out else []):
p = line.strip().split('|')
if len(p) >= 2:
val = 'ON' if p[1].strip() in ('True', '1') else 'OFF'
if p[0].strip() == 'Domain':
d['fw_domain'] = val
elif p[0].strip() == 'Private':
d['fw_private'] = val
elif p[0].strip() == 'Public':
d['fw_public'] = val
# 17. 密码策略
PW_LABELS = {
'Force user logoff how long after time expires?': '超时后强制注销',
'Minimum password age (days)': '密码最短使用期限(天)',
'Maximum password age (days)': '密码最长使用期限(天)',
'Minimum password length': '密码最短长度',
'Length of password history maintained': '密码历史记录长度',
'Lockout threshold': '账户锁定阈值(次)',
'Lockout duration (minutes)': '账户锁定时长(分钟)',
'Lockout observation window (minutes)': '锁定观察窗口(分钟)',
'Computer role': '计算机角色',
}
pw = run_command('net accounts')
d['pw_policy'] = {}
if pw['success']:
for line in pw['stdout'].split('\n'):
if ':' in line:
k, v = line.split(':', 1)
k, v = k.strip(), v.strip()
if k and v and 'command' not in k.lower() and '成功' not in k:
label = PW_LABELS.get(k, k)
d['pw_policy'][label] = v
# 18. 审计策略
audit = run_command('auditpol /get /category:* 2>&1')
d['audit_available'] = audit['success']
# 19. BitLocker
bl = ps(
"try{Get-BitLockerVolume -EA Stop|ForEach-Object{Write-Output \"$($_.MountPoint)|$($_.ProtectionStatus)|$($_.VolumeStatus)\"}}catch{'unavailable'}")
d['bitlocker'] = []
if bl and bl != 'unavailable':
for line in bl.split('\n'):
p = line.strip().split('|')
if len(p) >= 3:
d['bitlocker'].append({'drive': p[0], 'protection': p[1], 'status': p[2]})
d['bitlocker_available'] = bl != 'unavailable' and bool(d['bitlocker'])
# 20. RDP
d['rdp_enabled'] = ps(
"if((Get-ItemProperty 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Terminal Server' -EA SilentlyContinue).fDenyTSConnections -eq 0){'已启用'}else{'已禁用'}")
d['rdp_nla'] = ps(
"if((Get-ItemProperty 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Terminal Server\\WinStations\\RDP-Tcp' -EA SilentlyContinue).UserAuthentication -eq 1){'已启用'}else{'已禁用'}")
d['rdp_port'] = ps(
"(Get-ItemProperty 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Terminal Server\\WinStations\\RDP-Tcp' -EA SilentlyContinue).PortNumber")
# 21. 用户
users_out = ps("Get-LocalUser|ForEach-Object{Write-Output \"$($_.Name)|$($_.Enabled)|$($_.LastLogon)\"}")
d['local_users'] = []
for line in (users_out.split('\n') if users_out else []):
p = line.strip().split('|')
if len(p) >= 3:
d['local_users'].append({'name': p[0], 'enabled': p[1], 'lastlogon': p[2]})
admins = ps("(Get-LocalGroupMember -Group 'Administrators' -EA SilentlyContinue).Name -join ', '")
d['admin_members'] = admins if admins else 'N/A'
d['current_user'] = ps("(quser 2>$null|Select-Object -Skip 1|ForEach-Object{$_ -replace '\\s{2,}','|'}).Trim()")
# 22. 已安装软件(含安装时间)
sw = ps("""
$apps=@()
'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*','HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*'|ForEach-Object{
Get-ItemProperty $_ -EA SilentlyContinue|Where-Object{$_.DisplayName}|ForEach-Object{
$date=$_.InstallDate
if($date -and $date.Length -eq 8){$date=$date.Substring(0,4)+'-'+$date.Substring(4,2)+'-'+$date.Substring(6,2)}
$apps+="$($_.DisplayName)|$($_.DisplayVersion)|$($_.Publisher)|$date"
}
}
Write-Output "COUNT:$($apps.Count)"
$apps|Sort-Object|Get-Unique|Select-Object -First 25|ForEach-Object{Write-Output $_}
""", timeout=45)
d['sw_count'] = 0
d['sw_list'] = []
for line in (sw.split('\n') if sw else []):
line = line.strip()
if line.startswith('COUNT:'):
d['sw_count'] = int(line.replace('COUNT:', ''))
elif '|' in line:
p = line.split('|')
if len(p) >= 4:
d['sw_list'].append({'name': p[0], 'ver': p[1], 'pub': p[2], 'date': p[3]})
elif len(p) >= 3:
d['sw_list'].append({'name': p[0], 'ver': p[1], 'pub': p[2], 'date': ''})
# 23. 时间同步
ts = run_command('w32tm /query /source 2>&1')
d['time_source'] = ts['stdout'].strip() if ts['success'] else '无法获取'
ts2 = run_command('w32tm /query /status 2>&1')
d['time_status'] = '正常' if ts2['success'] else '异常/未配置'
# 24. 还原点
rp = ps("try{$r=Get-ComputerRestorePoint -EA Stop;Write-Output $r.Count}catch{'unavailable'}")
d['restore_count'] = rp if rp and rp != 'unavailable' else '不可用'
# 25. 电源计划
pw_out = run_command('powercfg /getactivescheme')
d['power_plan'] = 'N/A'
if pw_out['success']:
for seg in pw_out['stdout'].split('('):
if ')' in seg:
d['power_plan'] = seg.split(')')[0].strip()
break
# 26. 系统日志(聚合分析)
def collect_log_grouped(logname: str) -> list:
raw = ps(f"""
Get-WinEvent -FilterHashtable @{{LogName='{logname}';Level=2}} -MaxEvents 200 -EA SilentlyContinue |
ForEach-Object {{
$msg = $_.Message.Split([char]10)[0]
if ($msg.Length -gt 120) {{ $msg = $msg.Substring(0, 120) }}
Write-Output "$($_.Id)|$($_.ProviderName)|$($_.TimeCreated.ToString('yyyy-MM-dd HH:mm'))|$msg"
}}
""", timeout=45)
groups = {}
for line in (raw.split('\n') if raw else []):
p = line.strip().split('|', 3)
if len(p) >= 4:
key = f"{p[0]}|{p[1]}"
if key not in groups:
groups[key] = {'id': p[0], 'source': p[1], 'count': 0,
'first': p[2], 'last': p[2], 'msg': p[3]}
groups[key]['count'] += 1
groups[key]['last'] = p[2] # 最新的先输出
if not groups[key]['first'] or p[2] < groups[key]['first']:
groups[key]['first'] = p[2]
result = sorted(groups.values(), key=lambda x: x['count'], reverse=True)
return result
d['log_sys_grouped'] = collect_log_grouped('System')
d['log_app_grouped'] = collect_log_grouped('Application')
d['log_sys_total'] = sum(g['count'] for g in d['log_sys_grouped'])
d['log_app_total'] = sum(g['count'] for g in d['log_app_grouped'])
# Defender 防病毒状态
defender = ps("""
try {
$s = Get-MpComputerStatus -EA Stop
Write-Output "enabled|$($s.AntivirusEnabled)|$($s.RealTimeProtectionEnabled)|$($s.AntivirusSignatureLastUpdated.ToString('yyyy-MM-dd HH:mm'))|$($s.AntivirusSignatureVersion)|$($s.FullScanEndTime.ToString('yyyy-MM-dd HH:mm'))|$($s.QuickScanEndTime.ToString('yyyy-MM-dd HH:mm'))"
} catch { Write-Output "unavailable" }
""")
d['defender'] = {}
if defender and defender.startswith('enabled'):
p = defender.split('|')
if len(p) >= 7:
d['defender'] = {
'antivirus': '已启用' if p[1] == 'True' else '已禁用',
'realtime': '已启用' if p[2] == 'True' else '已禁用',
'sig_date': p[3], 'sig_ver': p[4],
'full_scan': p[5], 'quick_scan': p[6]
}
# 物理磁盘信息
pdisk = ps(
"Get-PhysicalDisk -EA SilentlyContinue|ForEach-Object{Write-Output \"$($_.FriendlyName)|$($_.MediaType)|$([math]::Round($_.Size/1GB,0))GB|$($_.HealthStatus)|$($_.BusType)\"}")
d['physical_disks'] = []
for line in (pdisk.split('\n') if pdisk else []):
p = line.strip().split('|')
if len(p) >= 5:
d['physical_disks'].append({'name': p[0], 'type': p[1], 'size': p[2], 'health': p[3], 'bus': p[4]})
# 27. 性能
d['perf_cpu'] = d['cpu_load']
net_stat = run_command('netstat -e')
d['net_rx'] = d['net_tx'] = 'N/A'
if net_stat['success']:
for line in net_stat['stdout'].split('\n'):
if 'Bytes' in line or '字节' in line:
nums = [x.strip() for x in line.split() if x.strip().isdigit()]
if len(nums) >= 2:
d['net_rx'] = f"{int(nums[0]) / 1024 / 1024 / 1024:.2f} GB"
d['net_tx'] = f"{int(nums[1]) / 1024 / 1024 / 1024:.2f} GB"
return d
# ==================== HTML 生成(精简) ====================
def generate_html(d: Dict, timestamp: str) -> str:
cpu_pct = float(d['cpu_load']) if d['cpu_load'] and d['cpu_load'] != 'N/A' else 0
mem_pct = d['mem_pct']
max_disk_pct = max((float(dk['pct']) for dk in d['disks']), default=0)
today = datetime.datetime.now().strftime('%Y-%m-%d')
# 日志时间范围
all_log_times = []
for g in d.get('log_sys_grouped', []) + d.get('log_app_grouped', []):
all_log_times.extend([g['first'], g['last']])
all_log_times = sorted([t for t in all_log_times if t])
log_range_start = all_log_times[0] if all_log_times else 'N/A'
log_range_end = all_log_times[-1] if all_log_times else 'N/A'
# 总体评估
issues = []
if cpu_pct > 90:
issues.append(('高', 'CPU 使用率过高'))
if mem_pct > 90:
issues.append(('高', '内存使用率过高'))
if max_disk_pct > 90:
issues.append(('高', '磁盘空间不足'))
if d['license_status'] != '已授权':
issues.append(('中', '系统未激活'))
if d['fw_domain'] == 'OFF' or d['fw_private'] == 'OFF' or d['fw_public'] == 'OFF':
issues.append(('高', '防火墙未全部开启'))
if d['rdp_enabled'] == '已启用':
issues.append(('中', '远程桌面已开启'))
# 检查日志中的高严重性
for g in d.get('log_sys_grouped', []):
if g['id'] == '55' and 'Ntfs' in g['source']:
issues.append(('高', f'NTFS 文件系统损坏 ({g["count"]}次)'))
if g['id'] == '6008':
issues.append(('高', f'非正常关机 ({g["count"]}次)'))
if g['id'] == '41':
issues.append(('高', f'意外重启/蓝屏 ({g["count"]}次)'))
risk_level = '高' if any(i[0] == '高' for i in issues) else '中' if any(i[0] == '中' for i in issues) else '低'
risk_color = '#ea4335' if risk_level == '高' else '#f9ab00' if risk_level == '中' else '#34a853'
# 安全评分 (满分100,基于问题数量和严重程度)
base_score = 100
for sev, _ in issues:
if sev == '高':
base_score -= 15
elif sev == '中':
base_score -= 5
else:
base_score -= 2
security_score = max(0, min(100, base_score))
score_color = '#34a853' if security_score >= 80 else '#f9ab00' if security_score >= 60 else '#ea4335'
h = f"""<!DOCTYPE html>
<html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Windows 系统巡检报告 - {esc(d['hostname'])}</title>
<style>
@page{{margin:20mm 15mm}}
*{{margin:0;padding:0;box-sizing:border-box}}
body{{font-family:'Microsoft YaHei','Segoe UI',sans-serif;background:#eef0f2;color:#2c2c2c;line-height:1.7;font-size:13px}}
.ct{{max-width:1020px;margin:0 auto;background:#fff;box-shadow:0 1px 6px rgba(0,0,0,.08)}}
/* ---- 封面 ---- */
.cover{{padding:0;border-bottom:1px solid #e0e0e0}}
.cover-title{{background:#3c3c3c;padding:32px 50px 24px}}
.cover-title h1{{font-size:22px;font-weight:700;color:#fff;letter-spacing:1px;margin-bottom:2px}}
.cover-title .sub{{font-size:13px;color:#aaa;margin:0}}
.cover-meta{{padding:20px 50px 24px}}
.cover table{{width:auto;border:none;margin:0;font-size:13px;border-collapse:separate;border-spacing:0}}
.cover td{{border:none;padding:3px 0;color:#555;vertical-align:top;white-space:nowrap}}
.cover td:first-child{{font-weight:600;color:#1d1d1d;width:90px;padding-right:20px}}
.cover td:last-child{{color:#444}}
.cover .risk{{font-weight:700;color:{risk_color}}}
.score-circle{{display:inline-block;width:60px;height:60px;border-radius:50%;background:{score_color};color:#fff;text-align:center;line-height:60px;font-size:24px;font-weight:bold;margin-right:15px;float:left}}
.score-desc{{display:inline-block;line-height:1.4}}
/* ---- 目录 ---- */
.toc{{background:#f7f8f9;padding:12px 50px;border-bottom:1px solid #e0e0e0;display:flex;flex-wrap:wrap;gap:0}}
.toc a{{color:#555;text-decoration:none;font-size:12px;padding:3px 18px 3px 0;white-space:nowrap}}
.toc a:hover{{color:#1a73e8}}
/* ---- 正文 ---- */
.body{{padding:6px 50px 30px}}
/* 一级标题 - 深灰通栏 */
h2{{font-size:14px;color:#fff;background:#3c3c3c;margin:24px -50px 14px;padding:9px 50px;letter-spacing:.5px;font-weight:600}}
/* 二级标题 */
h3{{font-size:13px;color:#1d1d1d;margin:16px 0 6px;padding-left:10px;border-left:3px solid #3c3c3c;font-weight:600}}
/* ---- 表格 ---- */
table{{width:100%;border-collapse:collapse;margin:6px 0 16px;font-size:12.5px}}
th{{background:#f2f3f5;color:#555;font-size:12px;font-weight:600;text-align:left;padding:7px 12px;border:1px solid #dfe1e5}}
td{{padding:6px 12px;border:1px solid #dfe1e5;vertical-align:top}}
tr:nth-child(even){{background:#fafafa}}
.kv td:first-child{{background:#f2f3f5;font-weight:600;color:#444;width:160px;white-space:nowrap}}
/* ---- 标签 ---- */
.badge{{display:inline-block;padding:1px 8px;border-radius:3px;font-size:11px;font-weight:600;color:#fff}}
.g{{background:#34a853}}.y{{background:#e8a000}}.r{{background:#d93025}}.b{{background:#1a73e8}}.gr{{background:#9aa0a6}}
/* 风险框 */
.risk-box{{display:inline-block;border:2px solid {risk_color};border-radius:4px;padding:5px 16px;margin:6px 0}}
.risk-box .level{{font-size:16px;font-weight:700;color:{risk_color}}}
/* 资源卡片 */
.summary-cards{{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin:10px 0}}
.sc{{background:#f7f8f9;border:1px solid #e8e8e8;border-radius:4px;padding:10px 12px;text-align:center}}
.sc .v{{font-size:16px;font-weight:700;color:#2c2c2c}}.sc .l{{font-size:11px;color:#888;margin-top:2px}}
/* ---- 目录 ---- */
.toc-section{{padding:30px 50px;border-bottom:1px solid #e0e0e0}}
.toc-section h2{{background:none;color:#1d1d1d;margin:0 0 16px;padding:0 0 8px;font-size:15px;border-bottom:2px solid #3c3c3c}}
.toc-grid{{display:grid;grid-template-columns:1fr 1fr;gap:0;counter-reset:toc}}
.toc-item{{display:flex;align-items:baseline;padding:7px 0;border-bottom:1px dashed #ddd;text-decoration:none;color:#2c2c2c;font-size:13px;transition:color .15s}}
.toc-item:hover{{color:#1a73e8}}
.toc-item .num{{display:inline-block;width:28px;font-weight:700;color:#3c3c3c;flex-shrink:0}}
.toc-item .dots{{flex:1;border-bottom:1px dotted #ccc;margin:0 8px;height:1px;align-self:center}}
.toc-item .page{{font-size:12px;color:#999;flex-shrink:0}}
/* ---- 页脚 ---- */
.footer{{background:#3c3c3c;color:#999;font-size:11px;padding:16px 50px;text-align:center;line-height:1.8}}
.footer b{{color:#ddd}}
</style></head><body><div class="ct">
<!-- ===== 封面 ===== -->
<div class="cover">
<div class="cover-title">
<h1>Windows 系统巡检报告</h1>
<div class="sub">{esc(d['hostname'])} / {esc(d['os'])}</div>
</div>
<div class="cover-meta">
<table>
<tr><td>巡检日期</td><td>{today}</td></tr>
<tr><td>日志范围</td><td>{esc(log_range_start[:10])} ~ {esc(log_range_end[:10])}</td></tr>
<tr><td>报告生成</td><td>{timestamp}</td></tr>
<tr><td>日志来源</td><td>本机实时采集 (System / Application / Security)</td></tr>
<tr><td>巡检人员</td><td>{esc(d['user'])}</td></tr>
<tr><td>综合风险</td><td class="risk">{esc(risk_level)}</td></tr>
<tr><td>安全评分</td><td><div class="score-circle">{security_score}</div><div class="score-desc">满分100分<br>基于问题严重程度计算</div></td></tr>
</table>
</div>
</div>
<!-- ===== 目录 ===== -->
<div class="toc-section">
<h2>目 录</h2>
<div class="toc-grid">
<a class="toc-item" href="#s1"><span class="num">一</span>目标主机基本信息<span class="dots"></span><span class="page"></span></a>
<a class="toc-item" href="#s2"><span class="num">二</span>硬件资源状态<span class="dots"></span><span class="page"></span></a>
<a class="toc-item" href="#s3"><span class="num">三</span>网络配置与连接<span class="dots"></span><span class="page"></span></a>
<a class="toc-item" href="#s4"><span class="num">四</span>安全配置审计<span class="dots"></span><span class="page"></span></a>
<a class="toc-item" href="#s5"><span class="num">五</span>用户与权限<span class="dots"></span><span class="page"></span></a>
<a class="toc-item" href="#s6"><span class="num">六</span>进程与服务分析<span class="dots"></span><span class="page"></span></a>
<a class="toc-item" href="#s7"><span class="num">七</span>启动项与计划任务<span class="dots"></span><span class="page"></span></a>
<a class="toc-item" href="#s8"><span class="num">八</span>已安装软件<span class="dots"></span><span class="page"></span></a>
<a class="toc-item" href="#s9"><span class="num">九</span>事件日志分析<span class="dots"></span><span class="page"></span></a>
<a class="toc-item" href="#s10"><span class="num">十</span>风险评估与建议<span class="dots"></span><span class="page"></span></a>
</div>
</div>
<div class="body">
"""
# ===== 一、基本信息 =====
h += '<h2 id="s1"><span>一</span>、目标主机基本信息</h2>\n<table class="kv">\n'
rows = [
('计算机名', d['hostname']),
('操作系统', d['os']),
('系统架构', d['arch']),
('域/工作组', d.get('user', '').split('\\')[0] if '\\' in d.get('user', '') else 'N/A'),
('当前用户', d['user']),
('主板/机型', d['motherboard']),
('BIOS', d['bios']),
('安全启动', d['secure_boot']),
('许可证', f"{d['license_status']} ({d['license_type']}),过期: {d['license_expire']}"),
('运行时间', f"{d['uptime']}(启动于 {d['boot_time']})"),
('电源计划', d['power_plan']),
('时间同步', f"{d['time_status']},源: {d['time_source']}"),
]
for k, v in rows:
h += f'<tr><td>{esc(k)}</td><td>{esc(v)}</td></tr>\n'
h += '</table>\n'
# ===== 二、硬件资源 =====
h += '<h2 id="s2"><span>二</span>、硬件资源状态</h2>\n'
# 资源概览卡片
h += '<div class="summary-cards">\n'
h += f'<div class="sc"><div class="v">{pct_bar(cpu_pct, 80)}</div><div class="l">CPU 使用率</div></div>\n'
h += f'<div class="sc"><div class="v">{pct_bar(mem_pct, 80)}</div><div class="l">内存 {d["mem_used"]}GB / {d["mem_total"]}GB</div></div>\n'
h += f'<div class="sc"><div class="v">{pct_bar(max_disk_pct, 80)}</div><div class="l">磁盘最高使用率</div></div>\n'
h += '</div>\n'
h += f'<h3>2.1 处理器</h3>\n<table class="kv">\n'
h += f'<tr><td>CPU</td><td>{esc(d["cpu_name"])}</td></tr>\n'
h += f'<tr><td>核心/线程</td><td>{esc(d["cpu_cores"])} 核 / {esc(d["cpu_threads"])} 线程 @ {esc(d["cpu_freq"])} MHz</td></tr>\n'
h += f'<tr><td>当前负载</td><td>{esc(d["cpu_load"])}%</td></tr>\n'
h += '</table>\n'
h += '<h3>2.2 内存</h3>\n<table><tr><th>制造商</th><th>容量</th><th>频率</th><th>型号</th></tr>\n'
for chip in d['mem_chips']:
p = chip.split('|')
if len(p) >= 4:
h += f'<tr><td>{esc(p[0])}</td><td>{esc(p[1])}</td><td>{esc(p[2])}</td><td>{esc(p[3])}</td></tr>\n'
h += '</table>\n'
h += '<h3>2.3 磁盘存储</h3>\n<table><tr><th>盘符</th><th>卷标</th><th>总容量</th><th>可用</th><th>已用</th><th>使用率</th></tr>\n'
for dk in d['disks']:
pct = float(dk['pct'])
h += f'<tr><td><b>{esc(dk["drive"])}</b></td><td>{esc(dk["label"])}</td><td>{esc(dk["total"])} GB</td><td>{esc(dk["free"])} GB</td><td>{esc(dk["used"])} GB</td><td>{pct_bar(pct, 80)}</td></tr>\n'
h += '</table>\n'
if d['physical_disks']:
h += '<h3>2.4 物理磁盘健康</h3>\n<table><tr><th>名称</th><th>类型</th><th>容量</th><th>总线</th><th>健康状态</th></tr>\n'
for pd in d['physical_disks']:
hc = 'g' if pd['health'] == 'Healthy' else 'r'
hl = '健康' if pd['health'] == 'Healthy' else pd['health']
h += f'<tr><td>{esc(pd["name"])}</td><td>{esc(pd["type"])}</td><td>{esc(pd["size"])}</td><td>{esc(pd["bus"])}</td><td><span class="badge {hc}">{esc(hl)}</span></td></tr>\n'
h += '</table>\n'
h += f'<h3>2.5 GPU</h3>\n<table class="kv">\n'
h += f'<tr><td>显卡</td><td>{esc(d["gpu"])}</td></tr>\n'
h += f'<tr><td>驱动版本</td><td>{esc(d["gpu_driver"])}</td></tr>\n'
h += f'<tr><td>未签名驱动</td><td>{esc(d["unsigned_drivers"])} 个</td></tr>\n'
h += '</table>\n'
# ===== 三、网络 =====
h += '<h2 id="s3"><span>三</span>、网络配置与连接</h2>\n'
h += '<h3>3.1 网络适配器</h3>\n<table><tr><th>适配器</th><th>状态</th><th>速率</th><th>MAC 地址</th></tr>\n'
for a in d['net_adapters']:
st = f'<span class="badge g">Up</span>' if a['status'] == 'Up' else f'<span class="badge gr">{esc(a["status"])}</span>'
h += f'<tr><td>{esc(a["name"])}</td><td>{st}</td><td>{esc(a["speed"])}</td><td><code>{esc(a["mac"])}</code></td></tr>\n'
h += '</table>\n'
h += '<h3>3.2 IP 地址分配</h3>\n<table><tr><th>适配器</th><th>IPv4 地址</th><th>子网</th><th>来源</th></tr>\n'
origin_map = {'Manual': '手动', 'Dhcp': 'DHCP', 'WellKnown': '自动', 'RouterAdvertisement': '路由通告'}
for a in d['ipv4_addrs']:
h += f'<tr><td>{esc(a["iface"])}</td><td><b>{esc(a["ip"])}</b></td><td>/{esc(a["prefix"])}</td><td>{esc(origin_map.get(a["origin"], a["origin"]))}</td></tr>\n'
h += '</table>\n'
h += f'<table class="kv"><tr><td>默认网关</td><td>{esc(d["gateway"])}</td></tr>\n'
h += f'<tr><td>DNS 服务器</td><td>{esc(d["dns"])}</td></tr>\n'
h += f'<tr><td>网络流量</td><td>接收 {d["net_rx"]} / 发送 {d["net_tx"]}</td></tr>\n'
h += f'<tr><td>连接统计</td><td>已建立 {d["conn_established"]} | 监听 {d["conn_listening"]} | TIME_WAIT {d["conn_timewait"]}</td></tr>\n'
h += '</table>\n'
h += '<h3>3.3 监听端口</h3>\n<table><tr><th>端口</th><th>进程</th><th>监听范围</th><th>说明</th></tr>\n'
for p in d['listen_ports'][:30]:
h += f'<tr><td><b>{esc(p["port"])}</b></td><td>{esc(p["process"])}</td><td>{esc(p["scope"])}</td><td style="color:#666">{esc(p["desc"])}</td></tr>\n'
if len(d['listen_ports']) > 30:
h += f'<tr><td colspan="4" style="color:#888">... 共 {len(d["listen_ports"])} 个端口</td></tr>\n'
h += '</table>\n'
h += '<h3>3.4 共享文件夹</h3>\n<table><tr><th>名称</th><th>路径</th><th>说明</th><th>连接数</th></tr>\n'
for s in d['shares']:
h += f'<tr><td>{esc(s["name"])}</td><td>{esc(s["path"])}</td><td>{esc(s["desc"])}</td><td>{esc(s["users"])}</td></tr>\n'
h += '</table>\n'
# ===== 四、安全配置 =====
h += '<h2 id="s4"><span>四</span>、安全配置审计</h2>\n'
h += '<h3>4.1 防护状态</h3>\n<table class="kv">\n'
fw_text = f'域={d["fw_domain"]} 专用={d["fw_private"]} 公用={d["fw_public"]}'
fw_ok = all(v == 'ON' for v in [d['fw_domain'], d['fw_private'], d['fw_public']])
h += f'<tr><td>防火墙</td><td><span class="badge {"g" if fw_ok else "r"}">{esc(fw_text)}</span></td></tr>\n'
h += f'<tr><td>远程桌面 (RDP)</td><td>{esc(d["rdp_enabled"])}(NLA: {esc(d["rdp_nla"])},端口: {esc(d["rdp_port"])})</td></tr>\n'
bl_text = '、'.join(f'{b["drive"]} {b["protection"]} {b["status"]}' for b in
d['bitlocker']) if d['bitlocker_available'] else '未启用/不可用'
h += f'<tr><td>BitLocker 加密</td><td>{esc(bl_text)}</td></tr>\n'
h += f'<tr><td>审计策略</td><td>{"已配置" if d["audit_available"] else "需要管理员权限查看"}</td></tr>\n'
if d['defender']:
dd = d['defender']
av = f'<span class="badge {"g" if dd["antivirus"]=="已启用" else "r"}">{esc(dd["antivirus"])}</span>'
rt = f'<span class="badge {"g" if dd["realtime"]=="已启用" else "r"}">{esc(dd["realtime"])}</span>'
h += f'<tr><td>Defender 防病毒</td><td>{av} | 实时保护: {rt}</td></tr>\n'
h += f'<tr><td>病毒库版本</td><td>{esc(dd["sig_ver"])}(更新于 {esc(dd["sig_date"])})</td></tr>\n'
h += f'<tr><td>最近扫描</td><td>快速: {esc(dd["quick_scan"])} | 完整: {esc(dd["full_scan"])}</td></tr>\n'
h += '</table>\n'
h += '<h3>4.2 密码策略</h3>\n<table class="kv">\n'
for k, v in d['pw_policy'].items():
h += f'<tr><td>{esc(k)}</td><td>{esc(v)}</td></tr>\n'
h += '</table>\n'
h += '<h3>4.3 系统更新</h3>\n<table><tr><th>补丁号</th><th>类型</th><th>安装日期</th></tr>\n'
for u in d['updates']:
h += f'<tr><td><b>{esc(u["id"])}</b></td><td>{esc(u["type"])}</td><td>{esc(u["date"])}</td></tr>\n'
h += '</table>\n'
h += '<h3>4.4 系统维护</h3>\n<table class="kv">\n'
h += f'<tr><td>时间同步</td><td>{status_badge(d["time_status"], "green" if d["time_status"] == "正常" else "yellow")} 源: {esc(d["time_source"])}</td></tr>\n'
h += f'<tr><td>系统还原点</td><td>{esc(d["restore_count"])} 个</td></tr>\n'
h += f'<tr><td>电源计划</td><td>{esc(d["power_plan"])}</td></tr>\n'
h += '</table>\n'
# ===== 五、用户 =====
h += '<h2 id="s5"><span>五</span>、用户与权限</h2>\n'
h += '<h3>5.1 本地用户账户</h3>\n<table><tr><th>用户名</th><th>启用</th><th>最后登录</th></tr>\n'
for u in d['local_users']:
en = f'<span class="badge g">是</span>' if u['enabled'] == 'True' else f'<span class="badge gr">否</span>'
h += f'<tr><td>{esc(u["name"])}</td><td>{en}</td><td>{esc(u["lastlogon"])}</td></tr>\n'
h += '</table>\n'
h += f'<h3>5.2 管理员组成员</h3>\n<p style="padding:8px 0">{esc(d["admin_members"])}</p>\n'
# ===== 六、进程 =====
h += '<h2 id="s6"><span>六</span>、进程与服务分析</h2>\n'
h += f'<p>当前进程数: <b>{d["proc_count"]}</b> | 运行中服务: <b>{esc(d["svc_running"])}</b></p>\n'
h += '<h3>6.1 内存占用 Top 10</h3>\n<table><tr><th>进程名</th><th>内存占用</th></tr>\n'
for name, mem_kb in d['proc_top10']:
h += f'<tr><td>{esc(name)}</td><td><b>{mem_kb // 1024}</b> MB</td></tr>\n'
h += '</table>\n'
# ===== 七、启动项 =====
h += '<h2 id="s7"><span>七</span>、启动项与计划任务</h2>\n'
h += '<h3>7.1 注册表启动项</h3>\n<table><tr><th>名称</th><th>命令</th></tr>\n'
for s in d['startup_items']:
cmd = s['cmd'] if len(s['cmd']) <= 90 else s['cmd'][:87] + '...'
h += f'<tr><td><b>{esc(s["name"])}</b></td><td style="font-size:12px;word-break:break-all">{esc(cmd)}</td></tr>\n'
h += '</table>\n'
h += f'<h3>7.2 计划任务(非微软,共 {esc(d["sched_total"])} 个)</h3>\n<table><tr><th>任务名</th><th>状态</th><th>操作</th></tr>\n'
for t in d['sched_tasks']:
st = f'<span class="badge b">{esc(t["state"])}</span>' if t['state'] == 'Running' else esc(t['state'])
act = t['action'] if len(t['action']) <= 70 else t['action'][:67] + '...'
h += f'<tr><td>{esc(t["name"])}</td><td>{st}</td><td style="font-size:12px;word-break:break-all">{esc(act)}</td></tr>\n'
h += '</table>\n'
# ===== 八、软件 =====
h += f'<h2 id="s8"><span>八</span>、已安装软件(共 {d["sw_count"]} 个)</h2>\n'
h += '<table><tr><th>名称</th><th>版本</th><th>发布者</th><th>安装日期</th></tr>\n'
for s in d['sw_list'][:25]:
h += f'<tr><td>{esc(s["name"])}</td><td>{esc(s["ver"])}</td><td>{esc(s["pub"])}</td><td>{esc(s.get("date", ""))}</td></tr>\n'
if d['sw_count'] > 25:
h += f'<tr><td colspan="4" style="color:#888">... 共 {d["sw_count"]} 个,仅显示前 25 个</td></tr>\n'
h += '</table>\n'
# ===== 日志 =====
EVENT_KB = {
'10001|Microsoft-Windows-DistributedCOM': ('低', 'DCOM 服务器启动失败,通常是 Windows 小组件/UWP 应用权限问题,已知问题,不影响正常使用'),
'10016|Microsoft-Windows-DistributedCOM': ('低', 'DCOM 权限警告,Windows 内置组件权限配置不一致,可安全忽略'),
'7023|Service Control Manager': ('中', '服务异常停止,需检查对应服务是否恢复正常运行'),
'7031|Service Control Manager': ('中', '服务意外终止并已触发恢复操作'),
'7034|Service Control Manager': ('中', '服务意外终止,未配置恢复操作'),
'7030|Service Control Manager': ('低', '服务标记为交互式但系统不允许,通常不影响功能'),
'7009|Service Control Manager': ('中', '服务启动超时,可能是启动顺序或资源不足导致'),
'7000|Service Control Manager': ('高', '服务启动失败,需排查服务依赖和配置'),
'1796|Microsoft-Windows-TPM-WMI': ('低', 'Secure Boot 更新失败,Legacy BIOS 模式下正常现象,不影响使用'),
'55|Ntfs': ('高', 'NTFS 文件系统损坏,建议尽快运行 chkdsk /f 修复'),
'153|disk': ('高', '磁盘 I/O 错误,可能是磁盘故障前兆,需密切关注'),
'11|disk': ('高', '磁盘控制器错误,建议检查磁盘健康和连接线缆'),
'41|Microsoft-Windows-Kernel-Power': ('高', '意外重启(蓝屏/断电),需排查电源或硬件问题'),
'1001|Windows Error Reporting': ('低', '应用崩溃报告已生成,查看具体应用日志定位问题'),
'1000|Application Error': ('中', '应用程序崩溃,查看故障模块定位根因'),
'86|Microsoft-Windows-CertificateServicesClient-CertEnroll': ('低', '证书自动注册失败,通常因无法访问 Azure 证书服务,不影响正常使用'),
'36874|Schannel': ('中', 'TLS 握手失败,可能是客户端不支持的协议版本'),
'36888|Schannel': ('低', 'TLS 连接警告,通常由对端关闭连接触发'),
'6008|EventLog': ('高', '上次系统关机不正常(非正常断电或蓝屏)'),
'1014|Microsoft-Windows-DNS-Client': ('低', 'DNS 解析超时,通常是临时网络问题'),
'4198|Microsoft-Windows-TCPIP': ('中', '检测到 IP 地址冲突'),
'10010|Microsoft-Windows-DistributedCOM': ('低', 'DCOM 服务器未在超时时间内注册,通常是 UWP 应用延迟启动'),
}
def assess_severity(evt):
key = f'{evt["id"]}|{evt["source"]}'
if key in EVENT_KB:
return EVENT_KB[key]
if evt['count'] >= 20:
return ('中', evt['msg'])
return ('低', evt['msg'])
def severity_badge(level):
colors = {'高': 'r', '中': 'y', '低': 'gr'}
return f'<span class="badge {colors.get(level, "gr")}">{esc(level)}</span>'
def render_log_table(grouped, log_name):
if not grouped:
return f'<p style="color:#888;padding:8px">无错误事件</p>\n'
total = sum(g['count'] for g in grouped)
all_times = []
for g in grouped:
all_times.extend([g['first'], g['last']])
all_times = [t for t in all_times if t]
time_range = f",时间范围 {min(all_times)} ~ {max(all_times)}" if all_times else ""
out = f'<p style="margin-bottom:10px;font-size:13px"><b>{esc(log_name)}</b>: 共 <b>{total}</b> 条错误,{len(grouped)} 类事件{time_range}</p>\n'
out += '<table>\n'
out += '<tr><th style="min-width:200px">问题</th><th style="width:60px">次数</th><th style="width:70px">严重程度</th><th>时间范围</th><th>说明</th></tr>\n'
for g in grouped:
sev, desc = assess_severity(g)
evt_label = f'{esc(g["source"])} (Event {esc(g["id"])})'
time_col = f'{esc(g["last"])}' if g['first'] == g['last'] else f'{esc(g["first"])} ~ {esc(g["last"])}'
out += f'<tr><td style="font-size:12px">{evt_label}</td>'
out += f'<td style="text-align:center"><b>{g["count"]}</b></td>'
out += f'<td style="text-align:center">{severity_badge(sev)}</td>'
out += f'<td style="font-size:12px;white-space:nowrap">{time_col}</td>'
out += f'<td style="font-size:12px">{esc(desc)}</td></tr>\n'
out += '</table>\n'
return out
# ===== 九、事件日志 =====
h += '<h2 id="s9"><span>九</span>、事件日志安全分析</h2>\n'
h += render_log_table(d['log_sys_grouped'], '系统日志')
h += render_log_table(d['log_app_grouped'], '应用日志')
# ===== 十、风险评估 =====
h += '<h2 id="s10"><span>十</span>、风险评估与建议</h2>\n'
h += f'<div class="risk-box"><span style="font-size:13px;color:#555">综合风险等级:</span><span class="level">{esc(risk_level)}</span></div>\n'
if issues:
h += '<h3>10.1 发现的问题</h3>\n<table><tr><th>序号</th><th>风险等级</th><th>问题描述</th></tr>\n'
for i, (lv, desc) in enumerate(issues, 1):
lv_cls = 'r' if lv == '高' else 'y' if lv == '中' else 'gr'
h += f'<tr><td>{i}</td><td><span class="badge {lv_cls}">{esc(lv)}</span></td><td>{esc(desc)}</td></tr>\n'
h += '</table>\n'
h += '<h3>10.2 安全建议</h3>\n'
suggestions = []
if any('NTFS' in i[1] for i in issues):
suggestions.append(('高', '立即对损坏的磁盘卷运行 <code>chkdsk /f</code> 进行修复,并备份重要数据'))
if any('非正常关机' in i[1] for i in issues):
suggestions.append(('高', '排查非正常关机原因:检查电源稳定性、硬件故障、蓝屏转储文件 (C:\\Windows\\MEMORY.DMP)'))
if any('防火墙' in i[1] for i in issues):
suggestions.append(('高', '启用所有配置文件的 Windows 防火墙,命令: <code>netsh advfirewall set allprofiles state on</code>'))
if any('远程桌面' in i[1] for i in issues):
suggestions.append(('中', '如非必要建议关闭 RDP;如需保留,确保启用 NLA 且使用强密码'))
if d.get('pw_policy', {}).get('密码最短长度', '0') == '0':
suggestions.append(('中', '密码最短长度为 0,建议设置为至少 8 位: <code>net accounts /minpwlen:8</code>'))
if d.get('pw_policy', {}).get('密码历史记录长度', '') in ('None', '无'):
suggestions.append(('低', '未启用密码历史记录,建议设置: <code>net accounts /uniquepw:5</code>'))
if d.get('secure_boot', '') and ('不支持' in d['secure_boot'] or 'Legacy' in d['secure_boot']):
suggestions.append(('低', '未启用安全启动 (Secure Boot),建议在 BIOS 中启用 UEFI + Secure Boot'))
if d.get('time_status', '') != '正常':
suggestions.append(('低', '时间同步异常,建议配置 NTP: <code>w32tm /config /manualpeerlist:ntp.aliyun.com /syncfromflags:manual /update</code>'))
if d.get('restore_count', '') in ('不可用', '0'):
suggestions.append(('低', '无可用系统还原点,建议启用系统保护并创建还原点'))
if not suggestions:
suggestions.append(('', '当前未发现需要立即处理的安全问题,系统状态良好'))
h += '<table><tr><th>优先级</th><th>建议措施</th></tr>\n'
for lv, desc in suggestions:
if lv:
lv_cls = 'r' if lv == '高' else 'y' if lv == '中' else 'gr'
h += f'<tr><td><span class="badge {lv_cls}">{esc(lv)}</span></td><td>{desc}</td></tr>\n'
else:
h += f'<tr><td></td><td>{desc}</td></tr>\n'
h += '</table>\n'
h += f"""
</div><!-- end .body -->
<div class="footer">
<b>Windows 系统巡检报告</b><br>
巡检日期: {today} • 主机: {esc(d['hostname'])} • 生成时间: {timestamp}<br>
本报告由自动化巡检工具生成,仅供内部参考
</div>
</div></body></html>"""
return h
def save_reports(html_content: str, base_name: str):
"""同时保存 HTML 和 Word (.doc) 报告,内容相同"""
# 保存 HTML
html_file = f"{base_name}.html"
with open(html_file, 'w', encoding='utf-8') as f:
f.write(html_content)
print(f"✓ HTML 报告: {os.path.abspath(html_file)}")
# 保存 Word 兼容文件 (实际仍是 HTML 格式,但扩展名为 .doc,Word 可打开)
word_file = f"{base_name}.doc"
with open(word_file, 'w', encoding='utf-8') as f:
f.write(html_content)
print(f"✓ Word 报告: {os.path.abspath(word_file)}")
def main():
print("=" * 50)
print(" Windows 系统巡检 (27项精简版) - 双报告模式")
print("=" * 50)
ts = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
print(f"开始巡检: {ts}\n")
if os.name != 'nt':
print("错误: 仅适用于 Windows")
return
print("正在采集数据...")
data = collect_all()
print("生成 HTML 报告内容...")
html_content = generate_html(data, ts)
timestamp_str = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
base_name = f"System_Inspection_Report_{timestamp_str}"
save_reports(html_content, base_name)
print(f"\n巡检完成时间: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
if __name__ == '__main__':
main()