概览 — 这个页面做什么
安全诊断 会对已注册的服务器运行 rootkit 痕迹扫描以及系统加固扫描,把每次结果换算为 0–100 分及 A–F 等级,在一个页面上呈现整个服务器群的安全态势。每一项检查都带有「检查什么/为什么/如何修复」的说明;凡是 状态为失败且严重度为致命(fail + crit) 的发现项,都会 自动升级为 问题处理 工单(会去重,并在后续检查通过后自动关闭)。
核心组成:
- 探针
infr_seccheck_probe.cjs— 在目标主机上 本地、只读 运行的 Node 脚本(不需要凭据)。它执行 36 项检查并输出 JSON。 - 正本目录
App\Support\SecurityCheckbook— 拥有 9 个类别、检查键、权重与严重度(与语言无关,可安全缓存)。说明文本位于resources/lang/{ko,en}/seccheck.php。 - 引擎
App\Services\Security\DiagnosticsService— 摄入 → 评分 → 工单升级/关闭 → 仪表盘聚合。 - 存储
infr_sec_scan(迁移000766)— 一行 = 一台主机的一次扫描;findings以 JSON 形式内联存储。
以 root 身份 运行探针可启用
/etc/shadow、sudoers、sshd -T等特权检查。非 root 时,特权检查会降级为skip,并从评分的分母中排除(不扣分)。
何时使用
- 在把一台新服务器投入生产之前进行 加固基线验证。
- 对疑似受害主机的 rootkit/后门痕迹 做快速分诊(隐藏进程、
ld.so.preload注入、网卡处于混杂模式等)。 - 形成周期性的合规节奏 — 用每日 cron 运行探针,通过 API 上报器 推送分数,自动积累趋势。
- 审计准备 — 一眼看出哪些主机在跑已停止支持(EOL)的操作系统、有未安装的安全更新,或者允许 root 通过 SSH 登录。
页面构成
| 区域 | 内容 |
|---|---|
| KPI 条 | 群体平均分 · 已扫描主机数 · 未关闭的致命工单 · 失败检查总数 · 最近一次扫描时间 |
| 等级分布 | A/B/C/D/F 主机数量的直方图 |
| 类别得分 | 9 个类别的汇总(群体平均、目录顺序、色调) |
| 分数趋势 | 按时间顺序排列的最近 60 次扫描 |
| 按等级排列的主机 | 主机卡片网格 — 按分数从低(风险最高)到高排序。每张卡片显示等级,pass/warn/fail/crit 数量,接入来源,扫描时间。点击 → 报告弹窗 |
| 报告弹窗 | 总分与等级 + 摘要(OS/内核)+ 每项检查的详情(检查什么/为什么/如何修复/证据)+ 已升级工单的链接 |
顶部工具栏: [运行扫描](多服务器远程运行)· [粘贴结果] · 刷新 · 自动刷新开关 · 全屏。
如何使用
四条接入通道
infr_sec_scan.source 用来区分一次扫描是怎么接收进来的:
| source | 入口 | 方式 |
|---|---|---|
emulate |
[运行扫描] 弹窗 | 选择实例 → 远程部署并运行探针。带有 instance_id 的粘贴或远程仿真应用同样记为 emulate |
paste |
[粘贴结果] 弹窗 | 粘贴直接在目标主机上生成的探针 JSON,不绑定实例 |
api |
POST /api/v1/security/scan |
每台主机的 cron 运行探针 → 通过 API 密钥推送(上报器 通道) |
manual |
兜底 | source 取值无效时的默认值 |
1) 多服务器远程运行 — 异步轮询
同步地跑完全部主机会超出 PHP 的执行时间,因此流程是 登记 → 发射后不管的 worker → 轮询。
- [运行扫描] → 勾选目标实例(每台都会标出可达/不可达)。
POST /security/diagnostics/run→ 创建一条进度记录并立即返回run_id(Cache,TTL 30 分钟)。- 前端 不等待 地调用
POST /security/diagnostics/run-worker/{runId}→ worker 在set_time_limit(0)和ignore_user_abort下依次跑完所有主机(某一台失败不会中断其余)。 - 前端轮询
GET /security/diagnostics/run-status/{runId}来刷新进度(done/total/ok)。
标记为 不可达 的实例没有匹配到
run_report采集器节点,因此无法远程运行。请直接在主机上运行探针,然后通过 [粘贴结果] 提交。远程运行的接线方式参见 远程仿真。
2) 粘贴/JSON 摄入
在目标主机上运行探针,并粘贴它输出的 JSON。
# 在目标主机上(推荐: root)
node infr_seccheck_probe.cjs --min
--min 输出单行 JSON。输出结构:
{
"host": "web-01",
"generated_at": "2026-08-13T03:30:00Z",
"os": "Rocky Linux 9.3",
"kernel": "5.14.0-362.el9",
"root": true,
"checks": [
{ "key": "ssh_root_login", "category": "ssh", "status": "fail", "severity": "crit", "detail": "PermitRootLogin yes", "evidence": "/etc/ssh/sshd_config:32" },
{ "key": "svc_firewall", "category": "services", "status": "pass", "severity": "crit", "detail": "firewalld active" }
],
"summary": {}
}
由 POST /security/diagnostics/ingest 接收。绑定实例会记为 source=emulate;不绑定则记为 source=paste。
3) API 上报器(自动化)
每台主机的 cron 运行探针,并把结果推送进 InfraGuard。API 密钥在 管理 ▸ API 密钥 中签发;该密钥必须带有 scan.write 权限范围。
30 3 * * * node /opt/infr/infr_seccheck_probe.cjs --min | curl -sS -X POST https://infraguard.example.com/api/v1/security/scan -H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" --data @-
响应(201):
{ "ok": true, "scan_id": 812, "host": "web-01", "score": 74, "grade": "C" }
评分 — SecurityCheckbook
分数是每项检查的 权重与状态 的加权比值。
状态系数(STATUS_FACTOR) — 只有这些计入;skip/info 会被排除在分母之外:
| 状态 | 系数 |
|---|---|
pass |
1.0 |
warn |
0.6 |
fail |
0.0 |
skip · info |
排除 |
严重度乘数(SEV_MUL) — 一次失败所隐含的后果越重,对分数的影响越大:
| fail_severity | 乘数 |
|---|---|
crit |
1.6 |
warn |
1.0 |
info |
0.5 |
单项检查的权重 w = check_weight(1–3) × severity_multiplier;总分 = round(100 × Σ(w × status_factor) / Σw)。类别得分在各自类别内使用同一个公式。
等级与色调:
| 分数 | 等级 | 色调 |
|---|---|---|
| ≥ 90 | A | good |
| ≥ 80 | B | good |
| ≥ 70 | C | warn |
| ≥ 60 | D | warn |
| < 60 | F | crit |
(等级为 A/B/C/D/F;色调是 good ≥ 80 · warn ≥ 60 · crit < 60。)
9 个类别 · 36 项检查
每个类别都有用于展示的权重和图标;实际评分使用的是上面的 单项检查权重 × 严重度乘数。
| 类别 | 标签 | 检查数 | 重点检查(失败时的含义) |
|---|---|---|---|
rootkit |
Rootkit/恶意软件 | 6 | rk_known_files·rk_ld_preload·rk_hidden_proc(crit) |
patch |
补丁/更新 | 3 | pt_security_updates·pt_eol_os(crit) |
ssh |
SSH 配置 | 5 | ssh_root_login·ssh_empty_pass(crit) |
accounts |
账号/权限 | 5 | acc_uid0·acc_empty_pass(crit) |
network |
网络暴露面 | 3 | net_legacy_services·net_db_exposed(crit) |
filesystem |
文件系统权限 | 5 | fs_shadow_perm(crit)·fs_world_writable·fs_suid(warn) |
hardening |
系统加固 | 5 | hd_selinux·hd_aslr·hd_syncookies(warn) |
services |
服务/防火墙 | 2 | svc_firewall(crit)·svc_unnecessary(info) |
logging |
审计/日志 | 2 | log_auditd·log_syslog(warn) |
未知的键会被并入 other 类别。每项检查的「检查什么/为什么/如何修复」文本由 seccheck.check.* 渲染到报告弹窗中。
致命发现项 → 升级为问题处理
每次摄入时,reconcileCases() 都会核对该主机的工单。
- 只有
status = fail且severity = crit的发现项才会升级。 去重键是内部的fingerprint。 - 若已存在未关闭的工单,则递增
occurrence_count++并更新last_occurred_at;否则 创建一条新工单:infr_incident(area=security,source=detection,entity_type=seccheck,entity_key=host,severity=crit,status=open)以及一条infr_incident_event(kind=created)。 - 自动关闭: 若某条原本未关闭的工单在本次扫描中不再是 fail+crit(已修复或不再被检出),则会被置为
status=resolved,写入resolved_at和一条自动关闭说明。
已升级的检查会在报告弹窗中带有「已升级为工单」徽章和一个 [查看工单] 链接。工单在 安全 ▸ 问题处理 看板上处理。
删除一次扫描(
POST /security/diagnostics/delete)不会 删除由它升级出的工单 — 两者相互独立。
数据来源
| 来源 | 作用 |
|---|---|
infr_sec_scan(000766) |
扫描结果(每台主机一条)。score·grade·pass/warn/fail/crit/total_count·category_scores·summary·findings JSON。索引 (host,scanned_at)·(instance_id,scanned_at)·grade·score |
infr_instance |
目标实例(按 alias/hostname 把 host 解析为 instance_id) |
infr_incident / infr_incident_event |
升级出的致命工单及其时间线 |
infr_api_key |
API 上报器的认证(权限范围 scan.write) |
| Cache | 仪表盘聚合缓存 20 秒(infr:sec:diag:build)· 异步运行的进度记录(TTL 1800 秒) |
infr_seccheck_probe.cjs |
数据来源探针(本地、只读) |
- 聚合纪律: 在最近 500 次扫描中,每台主机只保留最新的一次(去重)→ 得到群体列表、平均分、类别汇总、趋势、未关闭致命工单数。
- 探针给出的类别/严重度只是提示;服务端会通过
SecurityCheckbook把它们 重新归一化为正本值。
访问权限
- 查看: 需要具备安全角色区域的访问权限(在当前分层中,staff = 5 / admin = 10;demo = 1 看不到安全区域)。可查看仪表盘与报告。
- 写入(运行/摄入/删除): 需要会话的
infr_user_level >= 2(canWrite())。低于该级别时,run·ingest·delete返回 403(seccheck.toast.forbidden)。 - API 上报器: 需要
infr.apikey中间件,外加一个带scan.write权限范围的密钥;否则返回 403forbidden_scope。 - 审计: 每一次写入都会记入
infr_audit_log—sec.diag_run(目标数量)、sec.diag_ingest(scan_id·score·source)、sec.diag_delete(扫描 id)。可在 审计日志 中复核。
关于总体的级别模型(infr.level:N 中间件),参见 RBAC 级别。
排障与 FAQ
Q. 实例在 [运行扫描] 中显示「不可达」。
它没有匹配到 run_report 采集器节点,因此没有远程运行的接线。请直接在主机上运行探针,并通过 [粘贴结果] 提交。
Q. 出现「No checks found」错误。
粘贴的 JSON 没有 checks(或 items)数组,或者其中的条目缺少 key。请原样粘贴 infr_seccheck_probe.cjs --min 的输出。
Q. 失败数量不多,分数却很低。
这是严重度乘数造成的。一次 crit 失败的权重是 warn 的 1.6 倍。用类别得分和报告弹窗查看是哪条发现项在拉低分数。
Q. 所有特权检查都是 skip。
探针是以非 root 账号运行的。/etc/shadow、sudoers、sshd -T 这类检查需要 root。skip 会被排除在分母之外(不扣分),但要得到完整诊断,请以 root(或通过网关)运行。
Q. 一次运行要花些时间 — 可以关掉浏览器吗?
可以。worker 是发射后不管的,即使请求中断,也会在 ignore_user_abort 下继续运行。重新打开后轮询 run-status,或者等等级出现在仪表盘上再看即可。进度记录会保留 30 分钟。
Q. 一条工单自己消失了。 如果后续扫描不再把该发现项报告为 fail+crit(已修复或未检出),引擎会自动关闭该工单。这是预期行为。
相关内容
- 安全 ▸ 问题处理 — 已升级致命工单的处理流程
- 远程仿真 — 多服务器探针部署与运行的接线
- API 密钥 — 上报器通道使用的密钥(权限范围
scan.write) - 资产 ▸ 实例 — 目标操作系统实例台账
- 安全 ▸ 概览 — 完整的防御漏斗叙事
- 审计日志 — 运行/摄入/删除的历史
