本工具通过 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. 运行脚本
# 下载脚本后,在命令提示符(管理员)中执行
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 个) |
| 事件日志分析 |
系统与应用的错误事件聚合、严重程度判定、知识库解读 |
| 风险评估与建议 |
发现的问题清单、综合风险等级、安全评分、具体修复建议 |
输出示例
注意事项
- 执行策略:若遇到 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'{esc(text)}'
def pct_bar(pct, width=100):
color = '#34a853' if pct < 70 else '#f9ab00' if pct < 90 else '#ea4335'
return f' {pct}%'
# ==================== 数据采集(精简) ====================
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"""
Windows 系统巡检报告 - {esc(d['hostname'])}
Windows 系统巡检报告
{esc(d['hostname'])} / {esc(d['os'])}
"""
# ===== 一、基本信息 =====
h += '
一、目标主机基本信息
\n
\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'| {esc(k)} | {esc(v)} |
\n'
h += '
\n'
# ===== 二、硬件资源 =====
h += '
二、硬件资源状态
\n'
# 资源概览卡片
h += '
\n'
h += f'
{pct_bar(cpu_pct, 80)}
CPU 使用率
\n'
h += f'
{pct_bar(mem_pct, 80)}
内存 {d["mem_used"]}GB / {d["mem_total"]}GB
\n'
h += f'
{pct_bar(max_disk_pct, 80)}
磁盘最高使用率
\n'
h += '
\n'
h += f'
2.1 处理器
\n
\n'
h += f'| CPU | {esc(d["cpu_name"])} |
\n'
h += f'| 核心/线程 | {esc(d["cpu_cores"])} 核 / {esc(d["cpu_threads"])} 线程 @ {esc(d["cpu_freq"])} MHz |
\n'
h += f'| 当前负载 | {esc(d["cpu_load"])}% |
\n'
h += '
\n'
h += '
2.2 内存
\n
| 制造商 | 容量 | 频率 | 型号 |
\n'
for chip in d['mem_chips']:
p = chip.split('|')
if len(p) >= 4:
h += f'| {esc(p[0])} | {esc(p[1])} | {esc(p[2])} | {esc(p[3])} |
\n'
h += '
\n'
h += '
2.3 磁盘存储
\n
| 盘符 | 卷标 | 总容量 | 可用 | 已用 | 使用率 |
\n'
for dk in d['disks']:
pct = float(dk['pct'])
h += f'| {esc(dk["drive"])} | {esc(dk["label"])} | {esc(dk["total"])} GB | {esc(dk["free"])} GB | {esc(dk["used"])} GB | {pct_bar(pct, 80)} |
\n'
h += '
\n'
if d['physical_disks']:
h += '
2.4 物理磁盘健康
\n
| 名称 | 类型 | 容量 | 总线 | 健康状态 |
\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'| {esc(pd["name"])} | {esc(pd["type"])} | {esc(pd["size"])} | {esc(pd["bus"])} | {esc(hl)} |
\n'
h += '
\n'
h += f'
2.5 GPU
\n
\n'
h += f'| 显卡 | {esc(d["gpu"])} |
\n'
h += f'| 驱动版本 | {esc(d["gpu_driver"])} |
\n'
h += f'| 未签名驱动 | {esc(d["unsigned_drivers"])} 个 |
\n'
h += '
\n'
# ===== 三、网络 =====
h += '
三、网络配置与连接
\n'
h += '
3.1 网络适配器
\n
Up{esc(a["status"])}| 适配器 | 状态 | 速率 | MAC 地址 |
\n'
for a in d['net_adapters']:
st = f'' if a['status'] == 'Up' else f''
h += f'| {esc(a["name"])} | {st} | {esc(a["speed"])} | {esc(a["mac"])} |
\n'
h += '
\n'
h += '
3.2 IP 地址分配
\n
| 适配器 | IPv4 地址 | 子网 | 来源 |
\n'
origin_map = {'Manual': '手动', 'Dhcp': 'DHCP', 'WellKnown': '自动', 'RouterAdvertisement': '路由通告'}
for a in d['ipv4_addrs']:
h += f'| {esc(a["iface"])} | {esc(a["ip"])} | /{esc(a["prefix"])} | {esc(origin_map.get(a["origin"], a["origin"]))} |
\n'
h += '
\n'
h += f'
| 默认网关 | {esc(d["gateway"])} |
\n'
h += f'| DNS 服务器 | {esc(d["dns"])} |
\n'
h += f'| 网络流量 | 接收 {d["net_rx"]} / 发送 {d["net_tx"]} |
\n'
h += f'| 连接统计 | 已建立 {d["conn_established"]} | 监听 {d["conn_listening"]} | TIME_WAIT {d["conn_timewait"]} |
\n'
h += '
\n'
h += '
3.3 监听端口
\n
| 端口 | 进程 | 监听范围 | 说明 |
\n'
for p in d['listen_ports'][:30]:
h += f'| {esc(p["port"])} | {esc(p["process"])} | {esc(p["scope"])} | {esc(p["desc"])} |
\n'
if len(d['listen_ports']) > 30:
h += f'| ... 共 {len(d["listen_ports"])} 个端口 |
\n'
h += '
\n'
h += '
3.4 共享文件夹
\n
| 名称 | 路径 | 说明 | 连接数 |
\n'
for s in d['shares']:
h += f'| {esc(s["name"])} | {esc(s["path"])} | {esc(s["desc"])} | {esc(s["users"])} |
\n'
h += '
\n'
# ===== 四、安全配置 =====
h += '
四、安全配置审计
\n'
h += '
4.1 防护状态
\n
{esc(dd["antivirus"])}{esc(dd["realtime"])}\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'| 防火墙 | {esc(fw_text)} |
\n'
h += f'| 远程桌面 (RDP) | {esc(d["rdp_enabled"])}(NLA: {esc(d["rdp_nla"])},端口: {esc(d["rdp_port"])}) |
\n'
bl_text = '、'.join(f'{b["drive"]} {b["protection"]} {b["status"]}' for b in
d['bitlocker']) if d['bitlocker_available'] else '未启用/不可用'
h += f'| BitLocker 加密 | {esc(bl_text)} |
\n'
h += f'| 审计策略 | {"已配置" if d["audit_available"] else "需要管理员权限查看"} |
\n'
if d['defender']:
dd = d['defender']
av = f''
rt = f''
h += f'| Defender 防病毒 | {av} | 实时保护: {rt} |
\n'
h += f'| 病毒库版本 | {esc(dd["sig_ver"])}(更新于 {esc(dd["sig_date"])}) |
\n'
h += f'| 最近扫描 | 快速: {esc(dd["quick_scan"])} | 完整: {esc(dd["full_scan"])} |
\n'
h += '
\n'
h += '
4.2 密码策略
\n
\n'
for k, v in d['pw_policy'].items():
h += f'| {esc(k)} | {esc(v)} |
\n'
h += '
\n'
h += '
4.3 系统更新
\n
| 补丁号 | 类型 | 安装日期 |
\n'
for u in d['updates']:
h += f'| {esc(u["id"])} | {esc(u["type"])} | {esc(u["date"])} |
\n'
h += '
\n'
h += '
4.4 系统维护
\n
\n'
h += f'| 时间同步 | {status_badge(d["time_status"], "green" if d["time_status"] == "正常" else "yellow")} 源: {esc(d["time_source"])} |
\n'
h += f'| 系统还原点 | {esc(d["restore_count"])} 个 |
\n'
h += f'| 电源计划 | {esc(d["power_plan"])} |
\n'
h += '
\n'
# ===== 五、用户 =====
h += '
五、用户与权限
\n'
h += '
5.1 本地用户账户
\n
是否| 用户名 | 启用 | 最后登录 |
\n'
for u in d['local_users']:
en = f'' if u['enabled'] == 'True' else f''
h += f'| {esc(u["name"])} | {en} | {esc(u["lastlogon"])} |
\n'
h += '
\n'
h += f'
5.2 管理员组成员
\n
{esc(d["admin_members"])}
\n'
# ===== 六、进程 =====
h += '
六、进程与服务分析
\n'
h += f'
当前进程数: {d["proc_count"]} | 运行中服务: {esc(d["svc_running"])}
\n'
h += '
6.1 内存占用 Top 10
\n
| 进程名 | 内存占用 |
\n'
for name, mem_kb in d['proc_top10']:
h += f'| {esc(name)} | {mem_kb // 1024} MB |
\n'
h += '
\n'
# ===== 七、启动项 =====
h += '
七、启动项与计划任务
\n'
h += '
7.1 注册表启动项
\n
| 名称 | 命令 |
\n'
for s in d['startup_items']:
cmd = s['cmd'] if len(s['cmd']) <= 90 else s['cmd'][:87] + '...'
h += f'| {esc(s["name"])} | {esc(cmd)} |
\n'
h += '
\n'
h += f'
7.2 计划任务(非微软,共 {esc(d["sched_total"])} 个)
\n
{esc(t["state"])}| 任务名 | 状态 | 操作 |
\n'
for t in d['sched_tasks']:
st = f'' if t['state'] == 'Running' else esc(t['state'])
act = t['action'] if len(t['action']) <= 70 else t['action'][:67] + '...'
h += f'| {esc(t["name"])} | {st} | {esc(act)} |
\n'
h += '
\n'
# ===== 八、软件 =====
h += f'
八、已安装软件(共 {d["sw_count"]} 个)
\n'
h += '
| 名称 | 版本 | 发布者 | 安装日期 |
\n'
for s in d['sw_list'][:25]:
h += f'| {esc(s["name"])} | {esc(s["ver"])} | {esc(s["pub"])} | {esc(s.get("date", ""))} |
\n'
if d['sw_count'] > 25:
h += f'| ... 共 {d["sw_count"]} 个,仅显示前 25 个 |
\n'
h += '
\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'
{esc(level)}'
def render_log_table(grouped, log_name):
if not grouped:
return f'
无错误事件
\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'
{esc(log_name)}: 共 {total} 条错误,{len(grouped)} 类事件{time_range}
\n'
out += '
\n'
out += '| 问题 | 次数 | 严重程度 | 时间范围 | 说明 |
\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'| {evt_label} | '
out += f'{g["count"]} | '
out += f'{severity_badge(sev)} | '
out += f'{time_col} | '
out += f'{esc(desc)} |
\n'
out += '
\n'
return out
# ===== 九、事件日志 =====
h += '
九、事件日志安全分析
\n'
h += render_log_table(d['log_sys_grouped'], '系统日志')
h += render_log_table(d['log_app_grouped'], '应用日志')
# ===== 十、风险评估 =====
h += '
十、风险评估与建议
\n'
h += f'
综合风险等级:{esc(risk_level)}
\n'
if issues:
h += '
10.1 发现的问题
\n
| 序号 | 风险等级 | 问题描述 |
\n'
for i, (lv, desc) in enumerate(issues, 1):
lv_cls = 'r' if lv == '高' else 'y' if lv == '中' else 'gr'
h += f'| {i} | {esc(lv)} | {esc(desc)} |
\n'
h += '
\n'
h += '
10.2 安全建议
\n'
suggestions = []
if any('NTFS' in i[1] for i in issues):
suggestions.append(('高', '立即对损坏的磁盘卷运行
chkdsk /f 进行修复,并备份重要数据'))
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 防火墙,命令:
netsh advfirewall set allprofiles state on'))
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 位:
net accounts /minpwlen:8'))
if d.get('pw_policy', {}).get('密码历史记录长度', '') in ('None', '无'):
suggestions.append(('低', '未启用密码历史记录,建议设置:
net accounts /uniquepw:5'))
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:
w32tm /config /manualpeerlist:ntp.aliyun.com /syncfromflags:manual /update'))
if d.get('restore_count', '') in ('不可用', '0'):
suggestions.append(('低', '无可用系统还原点,建议启用系统保护并创建还原点'))
if not suggestions:
suggestions.append(('', '当前未发现需要立即处理的安全问题,系统状态良好'))
h += '
| 优先级 | 建议措施 |
\n'
for lv, desc in suggestions:
if lv:
lv_cls = 'r' if lv == '高' else 'y' if lv == '中' else 'gr'
h += f'| {esc(lv)} | {desc} |
\n'
else:
h += f' | {desc} |
\n'
h += '
\n'
h += f"""
"""
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()