业务故事站
P3 Apollo

安全合规:SBOM、漏洞扫描与策略门禁

制品上线前,安全合规工程师要回答三个问题:里面有什么(SBOM)、有没有已知漏洞(扫描)、能不能放行(策略门禁)。Apollo 把这三个答案做成了内置的轻量能力,配合 bundle 签名信任锚与合规报告,让供应链安全"左移"到部署之前。看完这 4 个故事,你就能自己走通"建制品 → SBOM → 扫描 → 门禁 → 合规报告"的完整闭环。

安全合规 平台运维 应用开发 SBOM 漏洞扫描 SecurityGate 共 4 个故事

能 / 不能速览

✅ 这个主题能做
  • 为制品生成 SBOM 物料清单(SPDX 2.3 / CycloneDX 1.5),同制品重复生成幂等覆盖
  • 触发内置漏洞扫描:明文密钥 / latest 标签 / 缺 sha256 / 未签名 bundle 四类规则 + 内置漏洞库按包名版本匹配
  • 安全策略引擎:6 类规则(deny_latest_tag / require_signature / critical_vuln / no_plaintext_secret / require_resource_limits / custom)+ 5 个内置模板
  • 安全门禁 SecurityGate:决策 deny 返回 403,并已接入 GitOps 同步激活前自动评估(部署左移)
  • 项目合规报告:策略数 / 评估决策分布 / 扫描制品数 / 漏洞条目数
  • 安全策略按 id 的读 / 写端点已补项目成员校验,跨项目访问返回 403(越权探测会留审计)
⛔ 这个主题做不了
  • 扫描器是内置模拟(zy_builtin),漏洞库仅 7 条示例,不是真实 Trivy,不能代替真实扫描
  • SBOM 是自研生成器,基于制品元数据,不是真实 Syft 的完整依赖树
  • OPA(Rego) 策略语言未实现,策略是自研 Go 规则引擎
  • 门禁当前拦截 GitOps 同步激活路径;部署编排(rollout)侧直接门禁仍属规划
  • 无传输层 TLS、无 KMS/Vault 托管密钥(生产需自行加固)
  • 门禁拒绝与权限不足同为 403,不靠响应体看不能区分;页面所有请求固定作用于项目 1(多项目切换未实现)

适用角色

本主题面向三个角色:

  • 安全合规工程师(刘经理):核心使用者——生成 SBOM、触发扫描、维护策略、做门禁评估、看合规报告。
  • 平台运维 / SRE(陈工):operator 角色可执行扫描与门禁评估(security:execute),但不可改策略。
  • 应用开发(阿强):把制品登记进仓库、补 sha256 / 修复版本,配合安全侧把漏洞清零。

能力速览(能做什么)

SBOM 物料清单

POST /artifacts/:id/sbom 生成 SPDX 2.3 / CycloneDX 1.5;同一制品仅一行,重复生成覆盖更新。

漏洞扫描

POST /artifacts/:id/scan:内置 4 条规则 + 7 条示例 CVE,回写制品扫描状态,结果落 data/security/reports。

安全策略引擎

security_policies 表定义规则,Evaluate 汇总 allow/warn/deny;每次评估落审计轨迹。

安全门禁 SecurityGate

deny 决策返回 403(带 violations 明细);已接入 GitOps Sync 激活前自动评估,deny → 保持 draft + sync failed。

合规报告

GET /projects/:id/compliance-report:策略数 / 启用数 / 近 30 天评估与决策分布 / 扫描与漏洞统计。

审计留痕

SECURITY_SBOM_GENERATE / SECURITY_SCAN_RUN / SECURITY_POLICY_CREATE / SECURITY_GATE_EVALUATE 等事件落审计日志。

界面操作指引(不写代码怎么用)

下面按「打开哪个页面 → 点哪里 → 填什么 → 看到什么」的顺序给出完整操作路径,业务用户照做即可。每步末尾的「API 等价」是给工程师对照用的,可以忽略。

1 打开「安全合规」页,看 Tab1 的合规总览卡 Apollo 控制台 → 左侧菜单「安全合规」→ Tab「合规总览」
  • 怎么做:点左侧菜单「安全合规」(/apollo/security),默认停在「合规总览」Tab。
  • 会看到:一屏九张卡——策略数、启用策略、评估次数(近 30 天)、deny / warn / allow 决策分布、扫描制品数、含漏洞制品、漏洞条目数。**如果加载失败,本区会显示独立的错误态并给一个「重试」按钮**,而不是显示一排假的 0。

API 等价:GET /api/v1/projects/1/compliance-report

2 用「安全门禁」表单手动跑一次评估 安全合规 → 合规总览 → 安全门禁 → 「执行评估」
  • 怎么做:在「安全门禁」表单里选「资源类型」(制品 / 部署 / 配置),填「资源 id」,在「资源数据」文本框填资源快照 JSON(可含 tags / digest / signer_id / vulns / environment 等键),点「执行评估」。
  • 会看到:按策略汇总出 allow / warn / deny 决策与违规明细。判定为 deny 时页面会以独立错误态渲染决策与违规列表,可点「重试」再跑。

API 等价:POST /api/v1/security-gate/evaluate

3 在「制品安全」Tab 选制品并「生成 SBOM」 安全合规 → 制品安全 Tab → 制品下拉 → 「生成 SBOM」
  • 怎么做:切到「制品安全」Tab,在制品下拉里选一个制品(页面挂载后会默认选中第一个),点「生成 SBOM」。
  • 会看到:提示「SBOM 生成成功(format=spdx)」,下方结果块展示 SPDX 物料清单。同一制品重复生成是覆盖更新,只有一行。

API 等价:POST /api/v1/artifacts/:id/sbom

4 点「触发扫描」看漏洞报告与项目漏洞报告 安全合规 → 制品安全 Tab → 「触发扫描」
  • 怎么做:在「制品安全」Tab 点「触发扫描」,等扫描完成后查看下方两个区块。
  • 会看到:① 该制品的漏洞报告块(状态徽标 clean / vulnerable / scanning / error,Critical / High / Medium / Low 计数与 findings 明细表);② 项目漏洞报告(项目 #1)汇总。扫描是自研内置模拟,命中规则来自内置的示例 CVE。

API 等价:POST /api/v1/artifacts/:id/scan、GET /api/v1/artifacts/:id/vulnerabilities、GET /api/v1/projects/1/vulnerabilities

5 在「安全策略」Tab 用内置模板一键落地策略 安全合规 → 安全策略 Tab → 内置模板 → 「应用」
  • 怎么做:切到「安全策略」Tab,在内置策略模板区点某模板的「应用」(有二次确认),把它实例化成项目 #1 的策略;也可以点页头「新建安全策略」自己填名称、分类、规则类型、级别、生效环境与描述。
  • 会看到:提示「模板 "…" 应用成功(策略 id=…)」,策略表与合规卡同步刷新。策略行可随时启停(点一下即切换)、编辑、删除。

API 等价:POST /api/v1/security-policies/templates/:name/apply?project_id=1

6 看门禁如何拦住部署(拒绝记录) 安全合规 → 合规总览(评估轨迹);部署与漂移 / Git 仓库 → 激活与同步
  • 怎么做:无需刻意操作——门禁已接入 GitOps 同步激活前的自动评估。想复盘就去「合规总览」看近 30 天决策分布,以及各产品「审计日志」里 SECURITY_GATE_EVALUATE 记录。
  • 会看到:被判定 deny 时,期望状态会保持 draft、同步标记 failed(同步失败记录可在「Git 仓库」页看到),带 Critical 漏洞的制品上不了线。

API 等价:GET /api/v1/projects/1/compliance-report

提示:门禁评估接口本身需要 security:execute 权限,而部署链路上的自动门禁是后端进程内部调用、不经 HTTP 鉴权,与本页登录态无关;SBOM / 漏洞扫描是自研内置模拟(非真实 Trivy / Syft,OPA 未实现),漏洞为内置示例 CVE,不要当作真实扫描结论。

调整指南(怎么调整)

  • 换 SBOM 格式:生成时传 format=cyclonedx(缺省 spdx);同一制品切换格式会覆盖旧记录,不会并存。
  • 调扫描规则命中:给制品补 sha256、去掉 latest 标签、为 bundle 填 signer_id,可消除 RULE-NO-SHA256 / RULE-LATEST-TAG / RULE-UNSIGNED-BUNDLE。
  • 改策略严重级:severity=deny 的违规直接拦截,warn 只提示不拦截;模板应用后可在"安全策略"Tab 编辑。
  • 限定生效环境:策略的 Environments 标签限定环境(空=全部环境生效)。
  • 看门禁结果:门禁拒绝时返回 decision=deny 与 violations 明细,按违规策略逐条修复再重新评估。

做得好的场景

"部署前安全检查"被做成了看得见、可审计、能拦截的闭环:
  • 上线前发现漏洞:制品扫出 critical 漏洞,门禁在部署前拦下,而不是上线后救火。
  • 供应链透明:SBOM + 扫描报告 + 策略评估,等保/SOC 2 审计时有据可查。
  • 安全策略一处定义处处生效:5 个内置模板一键应用,deny/warn 分级,环境限定生效。
  • 左移门禁自动执行:GitOps 同步激活前自动评估,人工忘记检查也不漏。

限制与不足

以下是明确的边界,使用前先知道:
  • 内置模拟扫描器:漏洞库仅 7 条示例,规则是启发式检查,不能代替真实 Trivy/Grype。
  • SBOM 基于制品元数据:不是真实依赖树扫描,组件级依赖不展开。
  • OPA(Rego) 未实现:策略是自研规则枚举,不支持 Rego 策略语言。
  • 门禁覆盖范围:已接入 GitOps Sync 激活路径;部署编排(rollout)侧与流水线 deploy 动作的直接门禁属规划。
  • 机密字段用 AES-256-GCM:design 承诺的 SM4 机密字段回迁是规划(平台 SM4 底座已就绪)。
  • 策略端点鉴权只到项目级:按 id 的策略端点已补项目成员校验(跨项目 403),但项目内成员均可改策略;页面请求固定打向项目 1(PROJECT_ID 硬编码)。
  • 门禁 403 语义不细分:策略 deny 与权限不足返回同一状态码,只能靠响应体的 decision / violations 字段区分。
  • 激活门禁只查全局策略:GitOps 同步激活前的自动评估按项目 0 取策略,项目级策略不参与该路径;合规报告的评估 / 扫描统计为近 30 天窗口。

场景故事

故事 1 生成 SBOM:上线前先回答"这个包里有什么"
背景
刘经理是安全合规工程师。明天要上线的 nginx 制品(版本 1.24.0)需要一份物料清单,审计要核对"里面到底有什么、版本号是什么"。他登录 Apollo 安全合规页(/apollo/security),在"扫描与漏洞"Tab 里先确认制品存在,然后点"生成 SBOM"。
传统做法对比
以前物料清单靠开发在 README 里手写,版本一换清单就过时;等审计来查时,开发要翻部署记录逐条对版本,一次核对少说半天。
角色
刘经理(安全合规工程师,需 security:execute 权限,operator/developer 以上均可)。
操作步骤
  1. 登录 Apollo(端口 18082,admin / admin1),进入"安全合规"页
  2. 先登记制品:POST /api/v1/projects/1/artifacts,name=nginx、version=1.24.0、type=docker_image
  3. 选中该制品,点击"生成 SBOM"(POST /api/v1/artifacts/:id/sbom)
  4. 查看返回的 SBOM 记录与原文
系统响应
生成接口返回:
POST /api/v1/artifacts/1/sbom
{ "code": 0, "data": {
  "artifact_id": 1, "format": "spdx",
  "sbom_data": { "spdxVersion": "SPDX-2.3",
    "name": "sbom-nginx-1.24.0",
    "documentNamespace": "https://apollo.zytech.local/sbom/1/nginx/1.24.0",
    "packages": [{ "name": "nginx", "versionInfo": "1.24.0",
      "checksums": [{ "algorithm": "SHA256", "checksumValue": "..." }] }] },
  "generated_at": "..." }, "request_id": "req_..." }
结果洞察
SBOM 包含制品名、版本、sha256 校验和与命名空间,符合 SPDX 2.3 结构,可以直接给审计当凭据。同一制品重复生成或切换格式(cyclonedx)都会整行覆盖——每个制品永远只有一份最新清单,不会出现"两份对不上"。
调整建议
制品没填 sha256 时 SBOM 的 checksums 为空,建议登记制品时就带上 sha256;审计要求 CycloneDX 格式时在生成请求里传 format=cyclonedx。
动手试一试
登录:http://127.0.0.1:18082,账号 admin / admin1。页面路径:Apollo → 安全合规 /apollo/security → Tab"扫描与漏洞"。输入内容:POST /api/v1/projects/1/artifacts 登记 nginx 1.24.0,然后 POST /api/v1/artifacts/:id/sbom。预期结果:返回 artifact_id=1、format=spdx、sbom_data.spdxVersion="SPDX-2.3"。
限制提示
SBOM 是自研生成器,基于制品元数据(name/version/sha256),不是真实 Syft 扫描出的完整依赖树——审计口径要与实现对齐,不要把它当依赖清单用。
故事 2 漏洞扫描:nginx 1.24.0 命中 critical 漏洞
背景
SBOM 生成完,刘经理点"触发扫描"(POST /api/v1/artifacts/1/scan),想在上线前确认 nginx 1.24.0 有没有已知漏洞。扫描器是内置的(标识 zy_builtin),会跑内置规则检查 + 内置小型漏洞库匹配。
传统做法对比
以前要靠人工去 NVD / 厂商公告逐个比对版本号,一天下来不一定找全;更常见的是"先上线、出事再补"——等漏洞曝光时,这个版本已经在生产跑了几个星期。
角色
刘经理(安全合规,security:execute 触发扫描)+ 阿强(应用开发,随后按报告升级版本)。
操作步骤
  1. 在制品列表选中 nginx 1.24.0
  2. 点击"触发扫描"(POST /api/v1/artifacts/1/scan)
  3. 查看返回的扫描报告状态与分级计数
  4. 到"制品漏洞"视图查看 findings 明细
系统响应
扫描返回:
POST /api/v1/artifacts/1/scan
{ "code": 0, "data": {
  "artifact_id": 1, "scanner": "zy_builtin",
  "status": "vulnerable",
  "total_critical": 1, "total_high": 0,
  "total_medium": 0, "total_low": 0,
  "scan_summary": { "total_findings": 1, "critical": 1,
    "rules_triggered": ["CVE-2024-27080"], "scanner": "zy_builtin" },
  "scan_result_path": "data/security/reports/report_1.json",
  "scanned_at": "..." }, "request_id": "req_..." }
// findings:CVE-2024-27080(critical),受影响包 nginx 1.24.0,修复版本 1.24.1
结果洞察
内置漏洞库按"包名+版本"匹配到 nginx CVE-2024-27080(critical),修复版本 1.24.1。报告同时回写了制品的 vulnerability_scan_status=vulnerable,项目漏洞视图会把它标红,谁看谁知道"这个制品不能直接上"。
调整建议
把"升级到 1.24.1"作为修复单发给阿强;同时核对制品 metadata 里有没有 password=/secret= 之类的明文,那会命中 RULE-PLAINTEXT-SECRET(high)——顺手把 sha256 补全,避免额外的 RULE-NO-SHA256 低风险噪音。
动手试一试
登录:admin / admin1。页面路径:/apollo/security → Tab"扫描与漏洞"。输入内容:登记 nginx 1.24.0 后 POST /api/v1/artifacts/1/scan。预期结果:status=vulnerable、total_critical=1、findings 含 CVE-2024-27080(修复版本 1.24.1)。
限制提示
扫描器是内置模拟,漏洞库只有 7 条示例 CVE,覆盖范围有限——只适合演示与基线检查,生产上线请接入真实 Trivy/Grype(规划中)。
故事 3 安全门禁:带 Critical 漏洞的制品被部署前拦下
背景
扫描确认 nginx 1.24.0 带 critical 漏洞,但阿强说"修复要下周,这周先上线顶着"。刘经理不答应——他先应用内置模板"Critical 漏洞拒绝"(deny 级),然后对制品做一次门禁评估,演示部署会被拦下来。
传统做法对比
以前"先上线顶着"就是放行,安全团队事后补流程,漏洞窗口期最长可达数周;门禁把"部署前必须无 critical 漏洞"变成硬约束,谁也不能口头放行。
角色
刘经理(安全合规,security:write 建策略 + security:execute 评估门禁);陈工(平台运维,operator 角色,可执行扫描/门禁但不能改策略)。
操作步骤
  1. 进入"安全策略"Tab,查看模板清单(GET /api/v1/security-policies/templates)
  2. 应用模板:POST /api/v1/security-policies/templates/Critical%20漏洞拒绝/apply?project_id=1
  3. 打开"安全门禁手动评估"表单,提交带 critical 漏洞的资源快照
  4. 观察 deny 决策与违规明细,再提交无漏洞快照对比 allow
系统响应
带 critical 漏洞的评估被拒绝:
POST /api/v1/security-gate/evaluate
{ "project_id": 1, "resource_type": "artifact", "resource_id": "1",
  "resource_data": { "tags": ["v1.24.0"], "sha256": "abc123",
    "vulns": [{ "severity": "critical" }] } }
→ 403 { "code": 40301,
  "message": "部署被安全策略拒绝: Critical 漏洞拒绝: 制品存在 critical 级漏洞",
  "data": { "decision": "deny", "policy_count": 1,
    "violations": [{ "policy_name": "Critical 漏洞拒绝",
      "rule_type": "critical_vuln", "severity": "deny",
      "reason": "制品存在 critical 级漏洞" }] } }
去掉 vulns 重新评估 → decision=allow。
结果洞察
门禁决策语义清晰:任一 deny 策略命中 → deny(403);无 deny 有 warn → warn;否则 allow。拒绝时接口同时回传 violations 明细,前端能展示"是哪条策略、什么原因"而不是一句干巴巴的拒绝文案。每次评估都落审计轨迹(SECURITY_GATE_EVALUATE)。
调整建议
除手动评估外,门禁已接入 GitOps 同步激活前自动评估:deny 时声明保持 draft、整次 sync 置 failed——这就是"部署左移"。正式环境建议把"禁止生产 latest 标签""部署必须签名""禁止明文密钥"等模板一起应用,形成完整基线。
动手试一试
登录:admin / admin1。页面路径:/apollo/security → Tab"合规总览"门禁表单 / Tab"安全策略"。输入内容:应用"Critical 漏洞拒绝"模板 → 带 vulns critical 评估。预期结果:403,decision=deny、violations 列出"Critical 漏洞拒绝";去掉 vulns 后评估返回 allow。
限制提示
门禁当前自动拦截的是 GitOps 同步激活路径(手动/Webhook/cron/流水线 DeployTrigger 全入口);部署编排(rollout)侧的直接门禁仍属规划。策略引擎是自研规则,OPA(Rego) 未实现。拒绝返回 403,与「权限不足」同码——要确认是策略拦下(还是权限不够),得看响应体的 decision / violations。
故事 4 合规报告:项目安全态势一页看清
背景
季度安全例会,刘经理要给管理层和审计交一份"default 项目最近的安全态势":有多少策略在生效、评估拦了几次、扫了多少制品、还有多少漏洞挂着。他直接打开合规总览 Tab。
传统做法对比
以前安全汇报靠手工汇总 Excel,扫描结果、策略、审计三张表对不齐,数字口径各说各话;现在一次接口调用拿到统一口径的汇总数,汇报当场出数据。
角色
刘经理(安全合规,security:read 即可看报告);平台管理员(确认账号有权限点,viewer 以上都能看)。
操作步骤
  1. 打开"合规总览"Tab(或直接 GET /api/v1/projects/1/compliance-report)
  2. 查看顶部指标卡:策略数 / 启用策略 / 评估次数 / deny / warn / allow
  3. 查看扫描制品数 / 含漏洞制品 / 漏洞条目数
系统响应
合规报告返回:
GET /api/v1/projects/1/compliance-report
{ "code": 0, "data": {
  "project_id": 1,
  "total_policies": 1, "enabled_policies": 1,
  "evaluations_30d": 2, "allow_count": 1,
  "denied_count": 1, "warn_count": 0,
  "scanned_artifacts": 1, "vulnerable_artifacts": 1,
  "vuln_count": 1 }, "request_id": "req_..." }
前端按字段渲染成 9 张指标卡,一目了然。
结果洞察
这份报告把"策略生效情况、门禁拦了几次、还有多少带漏洞制品"压缩到一张图上:denied_count=1 说明门禁确实拦过一次,vulnerable_artifacts=1 说明还有待修复项。等阿强把 nginx 升到 1.24.1 重新扫描后,报告数字会自动更新——汇报口径和数据一致。
调整建议
把合规报告纳入例行检查:每次发版前看一次 vulnerable_artifacts 是否归零;deny 次数上升说明策略在起作用,但也要警惕"策略过严拖慢交付",可把某些项从 deny 降到 warn 观察。
动手试一试
登录:admin / admin1。页面路径:/apollo/security → Tab"合规总览"。输入内容:GET /api/v1/projects/1/compliance-report。预期结果:返回 total_policies / enabled_policies / evaluations_30d / allow_count / denied_count / scanned_artifacts / vulnerable_artifacts / vuln_count 等指标。
限制提示
评估与扫描统计口径来自内置模拟扫描器与策略评估审计(近 30 天窗口),deny 数据只统计经门禁/评估产生的记录;真实扫描器接入后数据会变,口径以当时代码为准。合规总览加载失败时会显式提示「加载失败」并可重试,不再静默显示 0 值——看到 0 就是真的 0。

常见问题

SBOM 和 bundle 签名是什么关系?

bundle 签名(SM3 清单 + SM2 签名 + 白名单信任锚)回答"这个包是谁打的、有没有被改";SBOM 回答"这个包里面有什么、什么版本"。两者互补:签名管来源与完整性,SBOM 管物料成分,都是供应链安全的组成部分。bundle 签名详见"Bundle 签名与验签"主题。

扫描器是真实的 Trivy 吗?

不是。内置扫描器标识 zy_builtin,漏洞库只有 7 条示例 CVE(nginx/log4j/openssl/spring-core/jackson/guava/mysql),加上 4 条启发式规则(明文密钥/latest/缺 sha256/未签名 bundle)。它用于演示、演示基线检查和规则联动,真实生产请等真实 Trivy 接入(规划中)。

SecurityGate 为什么返回 403?

门禁决策为 deny 时返回 403 AUTHZ_ERROR,同时回传 decision/violations 明细(哪条策略、什么原因),前端可精确展示拦截原因。allow/warn 则正常返回结果。另外门禁已接入 GitOps 同步激活前自动评估:deny 时声明保持 draft、整次 sync 置 failed。

为什么 OPA(Rego) 没实现?

OPA(Rego) 需要引入外部策略引擎,与"单机优先、不过度封装"的路线不符。当前用自研 Go 规则(RuleType 枚举 + Evaluate 汇总)覆盖了常见安全基线;若未来需要复杂策略语言,可在规划中接入 OPA。

扫描结果会清空吗?

不会。扫描报告、漏洞条目、策略、评估轨迹都持久保存在平台库(vulnerability_reports / vulnerability_findings / security_policies / policy_evaluation_results);同一制品重复扫描会生成新报告,查询取最近一次。演示制品需要自己创建。

主题小结

一句话:安全合规 = SBOM(里面有什么)+ 漏洞扫描(有没有已知漏洞)+ 策略门禁(能不能放行)+ 合规报告(态势如何)。四件套全部内置、可审计、门禁已左移到 GitOps 同步激活前。记住边界:扫描器是内置模拟(非真实 Trivy)、SBOM 是元数据生成(非 Syft)、OPA(Rego) 未实现、rollout 侧门禁仍规划、门禁 403 与权限不足不可区分、页面固定作用于项目 1。