1. 页面概览

1.1 是什么

「安全合规」页(对应前端源码 action/web/src/views/apollo/SecurityPage.vue,页面标题「Apollo 安全合规」)是 LightApollo 的安全与合规操作台(对齐 TAD-10)。页面用三个 Tab 拆成三块能力:合规总览(overview)用九张 summary 卡汇总项目安全态势并提供一个可手动调用的「安全门禁评估」表单;扫描与漏洞(scan)围绕制品做 SBOM 物料清单生成、漏洞扫描触发与扫描结果查看;安全策略(policies)管理策略清单与内置策略模板。整页回答的是「上生产前怎么证明安全」:先登记制品 → 生成 SBOM → 触发扫描 → 建立安全策略 → 提交资源快照做门禁预演 → 在「部署与漂移」页真正激活部署时,由同一套策略引擎在激活前强制执行门禁。 理解本页要先建立一个双评估入口的概念。安全策略引擎(deny_latest_tag / require_signature / critical_vuln / no_plaintext_secret / require_resource_limits / custom 六类规则)存在两个调用点:一是本页 Tab1 里的「安全门禁手动评估」表单,它是演示/预演入口,提交一段资源快照 JSON,后端按当前启用的策略求值并返回 allow / warn / deny 决策与命中违规清单;二是部署链路上的真实门禁——后端在「期望状态从 draft 激活为 active」之前(见第 5.4 节)对声明执行同一套 Evaluate,决策为 deny 时直接以 403 拒绝激活,把带病声明拦在生产之前。本页的手动评估能让你在不上真实部署的情况下,把资源快照喂给策略引擎观察决策,是理解「为什么那条部署会被拦下来」的最短路径。 页面的数据流大致为「读(Tab1 汇总 / Tab2 扫描结果 / Tab3 策略与模板)→ 写(新建策略、启停、删除、应用模板、触发 SBOM/扫描、门禁评估)」。输入主要是策略表单字段与资源快照 JSON、以及所选制品 id;输出是后端返回的决策/违规/报告 JSON 在卡片、徽标与表格中的渲染。项目 id 暂用常量 1(PROJECT_ID=1,default 项目),本页所有列表都挂在项目 1 下,后续才支持从 URL 取项目参数。 典型使用链路(演示视角的完整闭环):① 先在「制品管理」页登记制品(拿到 artifact id);② 回本页 Tab2 选中该制品点「生成 SBOM」再点「触发扫描」,观察漏洞报告与分级计数;③ 到 Tab3「应用」内置模板(如「禁止生产 latest 标签」)或手动新建 deny 级策略;④ 回 Tab1 在资源快照里放 {"tags":["latest"]} 之类触发 deny 的内容点「执行评估」,观察 decision=deny 与违规行;⑤ 到「部署与漂移」页激活关联该制品的期望状态声明,若声明 digest 关联了扫描出的 Critical 漏洞或声明带 latest 标签,真实部署同样会被拒绝——两个入口共享同一套策略与审计,形成「策略→门禁→审计」闭环。

1.2 核心价值/能力表

能力说明对应页面操作
合规总览九张 summary 卡汇总策略数/启用数/近 30 天评估与决策计数/扫描与漏洞统计Tab「合规总览」顶部卡片区(GET /projects/1/compliance-report)
门禁手动评估提交资源快照 JSON,由启用策略求值出 allow/warn/deny 并列出违规策略Tab1「安全门禁手动评估(Security Gate)」表单「执行评估」
SBOM 生成对制品生成 SPDX 格式物料清单(同制品单条幂等覆盖)Tab2「生成 SBOM」
漏洞扫描触发内置扫描器(zy_builtin)对制品做规则与 CVE 命中检查并回写状态Tab2「触发扫描」
漏洞报告查看单制品分级计数(Critical/High/Medium/Low)+ 条目表(severity 徽标/vuln_id/包名/版本/描述)Tab2 漏洞报告区
项目漏洞汇总项目下全部制品的扫描状态与分级汇总行Tab2「项目漏洞报告(项目 #1)」列表
安全策略管理新建/编辑/删除策略,切换启用态,生效环境 JSON 限定Tab3 安全策略表
内置模板应用一键应用 5 类安全基线模板,幂等创建并默认启用Tab3「内置策略模板(5 类安全基线)」行「应用」

1.3 一句话总结

本页把「制品 SBOM/扫描 → 安全策略 → 门禁决策 → 合规汇总」串成部署前的安全闭环,让策略与漏洞状态在浏览器里可观察、可预演,并强制阻断带病制品进入生产。

2. 访问入口

2.1 路由与菜单

路由 path/apollo/security
路由 nameApolloSecurity
meta.titleApollo 安全合规
侧边栏入口ApolloLayout 侧边栏「安全合规」,位于「告警自愈」之后、「配置管理」之前
前端源码action/web/src/views/apollo/SecurityPage.vue
API 客户端action/web/src/api/apolloClient.js
路由注册action/web/src/router/index.js/apollo 子路由 children 下,component: ApolloSecurityPage(import 的 SecurityPage),挂 ApolloLayout

相邻页(侧边栏顺序):告警自愈 alerts.md(前)、配置管理 configs.md(后)、制品管理 artifacts.md(Tab2 的制品下拉数据来源)、部署与漂移 deployments.md(真实部署门禁在此页的激活/sync 动作上触发)、用户管理 admin-users.md(权限点授给角色后决定本页可见性)。

2.2 认证与权限

2.3 端口与 API 前缀

3. 界面布局

┌──────────────────────────────────────────────────────────────┐
│ Apollo 安全合规                             [新建安全策略]       │
│ alert-info:安全合规(TAD-10)…部署前强制安全门禁…               │
│ [操作结果提示条(v-if alert.message,可「关闭」)]               │
├──────────────────────────────────────────────────────────────┤
│ Tab:合规总览 | 扫描与漏洞 | 安全策略                          │
├──────────────────────────────────────────────────────────────┤
│ ┌ Tab1 合规总览 ────────────────────────────────────────────┐ │
│ │ 九张 summary 卡:策略数/启用策略/评估次数(近30天)/deny决策/ │ │
│ │  warn决策/allow决策/扫描制品数/含漏洞制品/漏洞条目数         │ │
│ │ ┌ 安全门禁手动评估(Security Gate)卡片 ────────────────┐ │ │
│ │ │ 项目id(禁用=1)/资源类型select/资源id/resource_data JSON│ │ │
│ │ │ [执行评估] → 评估结果区:decision 徽标 + policy_count + │ │ │
│ │ │  evaluated_at + 违规表(策略名/规则类型/级别/原因)      │ │ │
│ │ └───────────────────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ ┌ Tab2 扫描与漏洞 ──────────────────────────────────────────┐ │
│ │ 制品漏洞扫描(SBOM / Scan):制品下拉[生成 SBOM][触发扫描] │ │
│ │ SBOM 结果块(format/生成时间/sbom_data JSON)              │ │
│ │ 漏洞报告块:状态徽标 + C/H/M/L 卡 + 漏洞条目表              │ │
│ │ 项目漏洞报告(项目 #1):制品行汇总表                      │ │
│ └────────────────────────────────────────────────────────────┘ │
│ ┌ Tab3 安全策略 ────────────────────────────────────────────┐ │
│ │ 安全策略(n):ID/名称/分类/规则类型/级别/生效环境/描述/    │ │
│ │   启用(开关按钮)/操作(编辑/删除)                          │ │
│ │ 内置策略模板(5 类安全基线):模板名称/分类/规则类型/级别/  │ │
│ │   描述/操作(应用)                                         │ │
│ └────────────────────────────────────────────────────────────┘ │
├──────────────────────────────────────────────────────────────┤
│ ┌ 新建/编辑安全策略弹窗(modal,@click.self 可关)───────────┐ │
│ │ 新建安全策略 | 编辑安全策略 #id(项目 #1)       [关闭]     │ │
│ │ 名称* 分类* 规则类型* 级别* 生效环境JSON(可选) 描述         │ │
│ │ [取消] [创建/保存]                                         │ │
│ └────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘

各板块职责:

板块职责
页头 + 顶部 alert-info标题「Apollo 安全合规」、按 Tab 变化的「新建安全策略」、TAD-10 功能说明、全局操作结果提示(可关闭)
Tab 切换条合规总览 / 扫描与漏洞 / 安全策略 三个面板切换
合规总览 summary 行九张卡渲染 compliance-report 的九项计数;已修复(本批) 请求失败改渲染独立错误态 + 「重试」(SecurityPage.vue:60-67/657-660),不再静默保留 0 值
安全门禁手动评估卡片收集资源类型/资源 id/资源快照 JSON,调用门禁接口展示 decision 与违规表
制品漏洞扫描卡片制品下拉 + SBOM 生成/触发扫描,联动展示 SBOM 与漏洞报告(watch 制品自动加载)
项目漏洞报告卡片项目 #1 下全部制品的扫描状态与分级汇总行
安全策略卡片项目 #1 的策略表:行内启停/编辑/删除,顶部新建
内置策略模板卡片5 类安全基线模板表:行内「应用」(幂等创建并默认启用)
新建/编辑策略弹窗策略字段表单(名称/分类/规则类型/级别/生效环境/描述)

4. 交互元素

4.1 页头、顶部说明与操作结果提示条

控件位置含义必填/默认/可用条件操作效果触发后端调用边界与细节
页面标题页头 h2页面身份标识常驻Apollo 安全合规与源码 h2 逐字一致
「新建安全策略」页头右侧(btn-primary)打开新建策略弹窗始终可点与 Tab3 空态内嵌的「新建安全策略」同走 openCreatePolicy无(本地弹窗)在 Tab 1/2 下也显示且可用;Tab 切换不隐藏
顶部 alert-info标题下TAD-10 能力说明常驻只读文案文字与源码逐字一致(见附录 A)
操作结果提示条alert-info 下展示最近一次操作结果alert.message 才显示;type 取 info/success/error成功用 success、失败用 error、默认 info 显示消息单槽覆盖:新结果替换旧提示
提示条「关闭」提示条右侧清空提示提示可见时可用alert.message=''纯本地状态

页面状态用一组 ref 维护:loading(四个布尔位 policies/templates/vuln/projVulns,注意无 compliance 位)、busy(当前操作键字符串,非空即全局互斥——所有行内/页头按钮 :disabled="!!busy")、alert(单槽提示)、summarygateForm/gateResultartifacts/selectedArtifactId/lastSbom/vulnReport/findings/projVulnspolicies/templatespolicyModal/policyForm。注意 busy 是全局互斥位:任一写操作(评估/SBOM/扫描/保存策略/启停/删除/应用模板)进行中,其余全部操作按钮同时禁用;Tab 内的查看类操作与 Tab 切换不受 busy 影响。

4.2 Tab1:合规总览 summary 卡

页面挂载(onMounted)即调 GET /projects/1/compliance-report,把响应解包对象按别名兜底映射进 summary,由 2×? 排布(flex wrap)的九张卡渲染。各卡取值(后端字段在 5.3 列出):

卡片summary 字段语义
策略数total_policies项目 #1 与全局(project_id=0)的策略总数
启用策略enabled_policies其中 enabled=true 的数量
评估次数(近30天)evaluations_30d近 30 天落库的评估次数
deny 决策 / warn 决策 / allow 决策denied_count / warn_count / allow_count近 30 天评估决策分布
扫描制品数 / 含漏洞制品scanned_artifacts / vulnerable_artifacts项目下已扫描制品与检出漏洞的制品数
漏洞条目数vuln_count检出漏洞条目总数

失败行为:已修复(本批)——fetchCompliance 失败时写 complianceErrSecurityPage.vue:657-660,取 errMsg(err)),模板以 v-if="complianceErr" 渲染独立错误态(红色「加载失败:{msg}」+「重试」按钮,:60-67),与正常九张卡 v-else 互斥渲染,不再出现「失败提示与九张 0 值卡同屏」的误导;点「重试」重新调用 fetchCompliance。该接口不在任何 Tab 切换时重复请求,只在页面挂载与「策略写操作成功后」(新建/编辑/启停/删除/应用模板的成功路径)由页面主动重拉。

4.3 Tab1:安全门禁手动评估表单(Security Gate)

「安全门禁手动评估(Security Gate)」卡片是整页最重要的预演入口。卡片正文解释文案:模拟部署前门禁检查:提交资源快照(tags / digest / signer_id / config / vulns / environment),由启用策略求值汇总 allow / warn / deny 决策并记录审计轨迹。

控件/字段位置含义必填/默认/可用条件操作效果触发后端调用边界与细节
项目 id(project_id)表单第一行左当前评估项目恒为 1(disabled 输入框展示常量)只读展示随 payload 提交 project_id:1不可改;后端按 project 加载启用策略(含全局)
资源类型(resource_type)第一行右下拉被评估资源类别默认 artifact;可选 artifact(制品)/deployment(部署)/config(配置)决定资源快照的求值语义与审计中的 resource_type随 payload 提交值为枚举字符串 artifact/deployment/config,见 RESOURCE_TYPES
资源 id(resource_id)第二行输入资源标识选填(默认空串)trim 后随 payload 提交;空则后端不填随 payload 提交placeholder 如 artifact-3 或 deployment 目标名
资源数据(resource_data JSON)第三行 textarea rows=4资源快照对象选填(留空提交空对象)非空先 JSON.parse,语法错则拦截随 payload 提交 resource_data支持键(muted 原文):tags / sha256 / digest / signer_id / config(对象或字符串)/ vulns / environment;placeholder 示例见附录 A
「执行评估」表单右下(btn-primary submit)提交评估!busy 才可点;进行中显示 评估中...见下POST /security-gate/evaluate提交前清空上一次 gateResult

点「执行评估」后的行为(handleEvaluateGate):先把 resource_data trim 后 JSON.parse——语法错误走 showAlert('resource_data 不是合法 JSON:' + e.message, 'alert-error') 直接返回,不发请求。合法则组装 payload {project_id:1, resource_type, resource_id:trim 后, resource_data:对象} 提交。成功(allow/warn)或 HTTP 403(deny)都会把结果放进 gateResult 并展示评估结果区:

deny 边界(与直觉相反,务必知晓):后端把 deny 决策作为 HTTP 403 返回(envelope code:40301,body 携带 data:{decision,policy_count,violations,evaluated_at}),因此: 1. apolloClient 响应拦截器先对 403 执行 alert('无权限执行该操作')——浏览器会先弹一个系统级 alert; 2. 页面 catch 会识别 403 body:已修复(提交 412e9c48)err.response.status===403body.data.decision 存在时,把 body.data 赋给 gateResult 并提示 安全门禁评估完成:decision=deny(策略拦截),详见下方违规列表SecurityPage.vue:692-702)——deny 的违规表与普通 allow/warn 一样正常渲染; 3. 非上述形态(网络错/非 deny 业务错)仍走 showAlert('安全门禁评估失败:' + errMsg(err), 'alert-error')。 失败提示统一 安全门禁评估失败:{errMsg}(网络/非 deny 业务错时)或先弹「无权限执行该操作」(403 deny,随后即渲染违规表)。

4.4 Tab2:制品下拉与工具栏

控件位置含义必填/默认/可用条件操作效果触发后端调用边界与细节
制品下拉scan-bar 左侧 select选择要生成 SBOM/扫描/查报告的制品默认空(placeholder 选择制品...);有制品时页面挂载后自动选中第一个切换即清空 lastSbom 并 watch 自动拉该制品漏洞报告;切空则清空报告切换时 GET /artifacts/:id/vulnerabilitiesoption 文案 {{name}}({{version||'-'}} #{{id}});空态 muted 暂无制品(Tab 制品管理登记后此处可选)
「生成 SBOM」下拉右(btn-outline)生成 SPDX 物料清单!busy && selectedArtifactId 才可点;进行中显示 生成中...成功后 lastSbom 填充、提示 SBOM 生成成功(format=spdx)POST /artifacts/{id}/sbom body {format:'spdx'}同制品重复生成幂等覆盖;失败提示 生成 SBOM 失败:{msg}
「触发扫描」下拉右(btn-primary)触发内置漏洞扫描!busy && selectedArtifactId 才可点;进行中显示 扫描中...成功后提示 制品 #{id} 扫描:{msg}(msg 取响应 message||status||'扫描完成'),随即重拉该制品漏洞报告与项目漏洞汇总POST /artifacts/{id}/scan,成功后自动 GET /artifacts/:id/vulnerabilities + GET /projects/1/vulnerabilities失败提示 触发扫描失败:{msg}

制品列表本身由 onMounted 的 fetchArtifactsGET /projects/1/artifacts)加载,失败提示 加载制品列表失败:{msg}。制品 id 默认选中第一项后,watch 会立刻触发一次漏洞报告加载。

4.5 Tab2:SBOM 结果块与漏洞报告块

「生成 SBOM」成功后,在工具栏下方渲染 SBOM 块(.sbom-block):头行 SBOM:{{format||'-'}}(制品 #{{artifact_id || selectedArtifactId}}) + 生成时间 {{generated_at||created_at}}(格式化后),正文 JSON.stringify(sbom_data, null, 2) 的 code block 原文展示 SPDX JSON。注意:本页用「触发扫描」接口(POST /artifacts/:id/scan)后不自动展示 SBOM,SBOM 块只由「生成 SBOM」驱动;漏洞报告块与 SBOM 块相互独立,各自有值才渲染。 漏洞报告块(.vuln-block)数据来自 GET /artifacts/{selectedId}/vulnerabilities,页面对多形态响应做 parseVulnPayload 兼容(详见 5.3/5.4)。渲染内容:

区块内容细节
头行漏洞报告(制品 #{{artifact_id || selectedArtifactId}})右侧 muted 扫描器 {{scanner||'-'}}
状态与计数卡状态徽标 + Critical/High/Medium/Low + 漏洞条目status 徽标色:clean 绿 / vulnerable 红 / scanning 蓝 / error 橙(未识别灰);「漏洞条目」取 findings.length
扫描信息muted 扫描完成时间:{{scanned_at||created_at}}scan_result_path 时追加 · 结果文件 {{path}}(mono)
空态/表格findings 为空显示空态,否则表格status=scanning 显示 扫描进行中,请稍后刷新查看结果。;clean 显示 暂无漏洞条目(clean)。

findings 表格列:级别(severity 徽标 critical 红 / high 橙 / medium 黄 / low 蓝)/ 漏洞编号(mono,vuln_id)/ 组件包(package_name)/ 安装版本 / 修复版本 / 描述,空字段以 - 兜底。加载中显示 加载漏洞报告...;尚未扫描/未选制品的提示文案:选择制品后点击"触发扫描"生成漏洞报告;切换制品会自动加载已有报告。 后端对「未扫描」返回 404(制品尚未扫描,请先调用 POST /artifacts/:id/scan),页面提示 加载漏洞报告失败:{msg} 并把报告置空。

4.6 Tab2:项目漏洞报告(项目 #1)

卡片标题 项目漏洞报告(项目 #{{ PROJECT_ID }}),数据来自 onMounted 的 fetchProjVulnsGET /projects/1/vulnerabilities)。空态文案 暂无漏洞报告,先去 Tab 制品管理登记并扫描制品。;有数据渲染表格列:制品(artifactLabel + #id)/ 状态 / 扫描器 / Critical / High / Medium / Low / 扫描时间,各分级列空值显示 0。加载失败提示 加载项目漏洞报告失败:{msg}已修复(提交 412e9c48):后端该接口返回 {project_id, artifact_count, vulnerable_count, totals:{critical,high,medium,low}, artifacts:[{artifact_id,name,version,scan_status,report?,findings?}]};页面 fetchProjVulns 现优先 toList(data?.artifacts),并按需回退数组/reports/report 形态(SecurityPage.vue:793-813),再把每行的 report 字段平铺(statusreport.status || scan_status || statusscannerreport.scanner || scanner),项目漏洞汇总表因此能正常渲染。

4.7 Tab3:安全策略表

卡片标题 安全策略({{policies.length}}),右上内嵌 新建安全策略(btn-sm)。加载失败提示 加载安全策略失败:{msg};空态 暂无安全策略,点击"新建安全策略"或从下方模板快速应用(项目 #1)。。表格列:ID / 名称 / 分类 / 规则类型 / 级别 / 生效环境 / 描述 / 启用 / 操作

控件操作效果触发后端调用边界与细节
启停开关点击即切换:先打接口,成功后按响应 enabled(缺省按操作方向)提示 安全策略 #id 已启用/停用 并刷新策略列表与合规汇总;失败提示 启用/停用安全策略 #id 失败:{msg}POST /security-policies/{id}/enable|disable无二次确认,点击即生效;按钮文案反映的是当前态,点击行为是切到反态
「编辑」打开编辑弹窗回填当前行字段无(弹窗)见 4.9;保存走 PUT
「删除」confirm(见附录 A)再调接口;成功后提示 安全策略已删除 并刷新两表DELETE /security-policies/{id}confirm 文案含策略名与 id;删除不可恢复

4.8 Tab3:内置策略模板与「应用」

卡片标题 内置策略模板(5 类安全基线)。数据来自 onMounted 的 fetchTemplatesGET /security-policies/templates),空态 暂无模板。。表格列:模板名称 / 分类 / 规则类型 / 级别 / 描述 / 操作(应用)。5 类模板逐字名称与描述见附录 B(源码 security.ListTemplates())。

控件含义必填/默认/可用条件操作效果触发后端调用边界与细节
「应用」把模板实例化为一条策略!busy 才可点confirm(文案见附录 A),再调接口;成功提示 模板 "{name}" 应用成功(策略 id=..) 并刷新策略表与合规卡;失败 应用模板 "{name}" 失败:{msg}POST /security-policies/templates/{encodeURIComponent(name)}/apply?project_id={PROJECT_ID}已修复(提交 412e9c48):后端按 ?project_id 决定策略归属(缺省 0=全局),页面现显式带 ?project_id=${PROJECT_ID}SecurityPage.vue:977)→ 模板落地为项目 #1 策略,与 confirm 文案「到项目 #1」一致

卡片底部 muted:应用模板按 (project_id, name) 幂等创建策略并默认启用。——同模板重复「应用」不会建第二条,而是返回已存在的策略(HTTP 201 Created,幂等命中视为已就绪)。

4.9 新建/编辑安全策略弹窗

弹窗头:{{ mode==='create' ? '新建安全策略' : '编辑安全策略 #'+policy.id }}(项目 #{{ PROJECT_ID }})。字段:

字段控件/校验说明
名称(name)*input,requiredplaceholder 如 禁止生产 latest 标签;保存前 trim 后判空,空则 showAlert('策略名称(name)必填','alert-error')
分类(category)*selectimage/config/compliance/deploy 四选一;默认 image
规则类型(rule_type)*select六类 RULE_TYPES;默认 deny_latest_tag
级别(severity,deny/warn)*select默认 deny;选项文案 deny(拒绝,硬拦截) / warn(告警,不拦截)
生效环境(environments JSON,可选)inputplaceholder ["prod","staging"],留空则全部环境;保存时先 JSON.parse 成功且数组非空才带 payload.environments,parse 失败则把逗号分隔输入 split 成数组兜底
描述(description)textarea rows=3placeholder 如:制品/镜像不得使用 latest 等可变标签,强制不可变版本溯源;trim 后非空才带

底部按钮:取消(btn-outline,仅关弹窗)与提交按钮(busy==='savePolicy' 显示 保存中...;否则 create 模式显示 创建、edit 模式显示 保存)。提交效果:

场景操作效果触发后端调用
新建成功后按返回 id 提示 安全策略创建成功(id=..),关弹窗,刷新策略表与合规卡POST /projects/1/security-policies
编辑成功后提示 安全策略 #id 更新成功,关弹窗,刷新两表PUT /security-policies/{id}
失败保存安全策略失败:{msg}(如 name 冲突 409)以上任一

弹窗为本地模态,点遮罩(@click.self)或「关闭」可放弃未保存内容,不触发请求。

5. 后端关联

5.1 API 客户端

action/web/src/api/apolloClient.js:axios 实例 baseURL /apollo-api/v1、timeout 30000ms、请求拦截器对所有请求附加 Authorization: Bearer <token>(token 仅取 apollo_token,不回退 aip_token)。响应拦截器:HTTP 2xx 且 code===0 时把 response.data 解包为业务数据;HTTP 2xx 但 code!==0 拒绝;4xx/5xx 提取 body.message || body.error 并把 message 别名写入 error.response.data.error 后 reject——401 清 token 并跳 /apollo/login(登录页自身 401 不跳,避免死循环),403 alert('无权限执行该操作')。页面各 catch 统一以 err.response?.data?.message || err.response?.data?.error || err.message(errMsg)展示后端错误。 403 双提示是本页常态:门禁 deny 的 HTTP 403 先触发拦截器系统级 alert('无权限执行该操作'),再被页面 catch 解析 body.data 并提示 安全门禁评估完成:decision=deny(策略拦截),详见下方违规列表SecurityPage.vue:692-702)——演示时会看到浏览器原生 alert + 页内 alert 条两个提示,属预期现象。

5.2 端点表

以下端点后端路由见 action/products/apollo/server/server.go(security 段约 944-961 行),全部带权限中间件(第 6 章详述)。前端统一用 apolloClient,因此 URL 前缀为 /apollo-api/v1;下表写后端实际路径(前缀 /api/v1)。

方法路径权限请求体/Query页面触发点
GET/projects/:id/compliance-reportsecurity:read-onMounted + 策略写成功后
POST/security-gate/evaluatesecurity:execute{project_id, resource_type, resource_id, resource_data}「执行评估」
GET/projects/:id/artifactsartifact:read-制品下拉
POST/artifacts/:id/sbomsecurity:execute{format:'spdx'}「生成 SBOM」
POST/artifacts/:id/scansecurity:execute-「触发扫描」
GET/artifacts/:id/vulnerabilitiessecurity:read-漏洞报告(watch/扫描后自动)
GET/projects/:id/vulnerabilitiessecurity:read-项目漏洞报告列表
GET/POST/projects/:id/security-policiessecurity:read/writePOST body {name,category,rule_type,severity,description?,environments?}策略表加载 / 新建
GET/PUT/DELETE/security-policies/:idread/write/writePUT body 同上编辑/删除
POST/security-policies/:id/enablesecurity:execute-启停开关 enable
POST/security-policies/:id/disablesecurity:execute-启停开关 disable
GET/security-policies/templatessecurity:read-模板表加载
POST/security-policies/templates/:name/applysecurity:executeQuery ?project_id=(缺省 0=全局)模板「应用」

5.3 响应结构示例

以下示例取自后端 handler 构造与既有测试期望(security_handlers.gosecurity/ 包、stage_d_smoke_test.go),页面看到的实际 JSON 即 envelope 解包后的 dataGET /projects/1/compliance-report(envelope 解包后)

{
  "project_id": 1,
  "total_policies": 6,
  "enabled_policies": 5,
  "evaluations_30d": 12,
  "allow_count": 7,
  "denied_count": 3,
  "warn_count": 2,
  "scanned_artifacts": 4,
  "vulnerable_artifacts": 2,
  "vuln_count": 5
}

字段即九张 summary 卡的取值;total_policies/enabled_policies 统计口径为 project_id=? OR project_id=0(项目 + 全局合并),其余计数取自项目维度。前端 fetchCompliance 对每个字段做别名兜底(total_policies→policy_count→policies 等),当前后端字段与之匹配。 POST /security-gate/evaluate(决策 allow/warn,HTTP 200)

{
  "project_id": 1,
  "resource_type": "artifact",
  "resource_id": "artifact-3",
  "decision": "warn",
  "policy_count": 5,
  "violations": [
    {
      "policy_name": "部署必须签名",
      "rule_type": "require_signature",
      "severity": "warn",
      "reason": "资源缺少 signer_id,无法验证签名"
    }
  ],
  "evaluated_at": "2026-09-07T05:00:00.123Z"
}

decisiondeny(任一 deny 级违规命中)> warn(仅 warn 级违规)> allow(全部通过)。policy_count 是参与求值的启用策略数(含环境过滤前计数),violations 为命中违规数组。 POST /security-gate/evaluate(决策 deny,HTTP 403 + fail envelope)

{
  "code": 40301,
  "message": "部署被安全策略拒绝: 禁止生产 latest 标签: 制品使用了 latest 标签,禁止部署",
  "data": {
    "decision": "deny",
    "policy_count": 5,
    "violations": [
      {
        "policy_name": "禁止生产 latest 标签",
        "rule_type": "deny_latest_tag",
        "severity": "deny",
        "reason": "制品使用了 latest 标签,禁止部署"
      }
    ],
    "evaluated_at": "2026-09-07T05:01:00.123Z"
  },
  "request_id": "req_..."
}

这是本页最需要理解的形态差异:deny 决策不放在 HTTP 200 的 envelope 里,而是用 HTTP 403 承载(code 与 middleware 的 40301 一致),data 中照常携带完整评估明细。页面拦截器因此走 403 分支(系统 alert + 走 catch);已修复(提交 412e9c48) catch 会把 403 body 的 data 写入 gateResult 并渲染违规表(SecurityPage.vue:692-702)——见 4.3。 GET /artifacts/3/vulnerabilities(envelope 解包后,report 与 findings 为同级字段)

{
  "artifact_id": 3,
  "report": {
    "id": 11,
    "artifact_id": 3,
    "scanner": "zy_builtin",
    "status": "vulnerable",
    "total_critical": 1,
    "total_high": 1,
    "total_medium": 0,
    "total_low": 1,
    "scanned_at": "2026-09-07T05:02:00.123Z",
    "created_at": "2026-09-07T05:02:00.123Z"
  },
  "findings": [
    {
      "id": 31,
      "report_id": 11,
      "vuln_id": "CVE-2024-27080",
      "severity": "critical",
      "package_name": "nginx",
      "installed_version": "1.22.0",
      "fixed_version": "1.24.1",
      "description": ""
    },
    {
      "id": 32,
      "report_id": 11,
      "vuln_id": "RULE-LATEST-TAG",
      "severity": "low",
      "package_name": "demo-web",
      "installed_version": "1.2.0",
      "description": "制品使用 latest 标签,无法溯源到不可变版本"
    }
  ]
}

关键形态点findingsreport 平级,而非嵌套在 report 里。页面 parseVulnPayload 现优先命中 data.report 分支(SecurityPage.vue:752-774),把 vulnReport 置为嵌套的 report 对象、findings 取平级的 data.findings(平级读不到时回退 data.report.findings)——status/count 出自 report、findings 表照常渲染,两处均正确(见 4.5)。report.status 枚举 scanning/clean/vulnerable/errorreport.scan_result_path(omitempty,扫描报告落盘路径)未扫描或为 error 时可缺省。未扫描访问本接口后端 404:{code:40400..., message:"制品尚未扫描,请先调用 POST /artifacts/:id/scan"}GET /projects/1/vulnerabilities(envelope 解包后,汇总对象)

{
  "project_id": 1,
  "artifact_count": 4,
  "vulnerable_count": 2,
  "totals": { "critical": 1, "high": 1, "medium": 0, "low": 1 },
  "artifacts": [
    {
      "artifact_id": 3,
      "name": "demo-web",
      "version": "1.2.0",
      "scan_status": "vulnerable",
      "report": { "id": 11, "scanner": "zy_builtin", "status": "vulnerable" },
      "findings": [ { "id": 31, "vuln_id": "CVE-2024-27080", "severity": "critical", "package_name": "nginx" } ]
    }
  ]
}

已修复(提交 412e9c48):页面 fetchProjVulns 现读 data.artifacts 并把每行的 report 平铺(statusreport.status || scan_statusscannerreport.scanner || scannerSecurityPage.vue:793-813),汇总表按「制品/状态/扫描器/Critical/High/Medium/Low/扫描时间」正常渲染。 GET /projects/1/security-policies(数组,单项 SecurityPolicy)GET /security-policies/templates(数组) 见 5.4 后的模板清单;策略行关键字段:id/project_id/name/category/rule_type/severity/description/environments/enabled/created_at/updated_at(rule_config/environments omitempty,environments 空数组时页面显示「全部环境」)。

5.4 关键机制

六类规则与三档决策聚合。策略引擎(security/policy.go)把启用的策略逐条对资源数据求值:deny_latest_tag(tags 含可变标签即违规)、require_signature(signer_id 与 digest 须非空)、critical_vuln(vulns 中含 critical 级即违规)、no_plaintext_secret(config 文本含 password=/secret=/api_key= 模式即违规)、require_resource_limits(config 未声明 resources.limits 即违规)、custom(正则/匹配模式自定义,见 policy.go evaluateRule)。每条命中生成一条 Violation{policy_name,rule_type,severity,reason};聚合规则是任一 deny 级违规→deny;否则任一 warn 级违规→warn;全过→allow。参与求值的策略 = 项目 + 全局(project_id=? OR project_id=0)中 enabled=true 者,并按资源 environment 做环境过滤(策略 environments 空 = 全部环境生效)。注意 project_id=0(全局)查询时只取全局策略——这正是门禁评估/激活路径与页面项目 #1 语义不同的根源(见下文)。 每次评估都落审计与历史Evaluate 尾部调 persistEvaluation:把评估结果(project/resource/decision/违规)写入 policy_evaluation_results 表——合规总览的近 30 天计数与决策分布即由此统计。门禁 deny 还会在安全门禁的两种入口(手动 evaluate、部署链路)各落一条审计动作(SECURITY_GATE_EVALUATE denied / SECURITY_POLICY_APPLY_TEMPLATE 等,见第 6 章),与「审计日志」页贯通。 部署链路上的真实门禁(左移门禁)。后端把同一策略引擎接进期望状态激活动作,形成「先证安全再上线」的强制检查点:gitops.SyncEnginegitops/service.go,激活新声明 draft→active 前,约 202 行)与期望状态激活 handler(server/handlers.gohandleActivateDesiredState,draft→active 前,286 行)都会调用 server 层注入的 apolloSecurityGate.EvaluateDeploymentserver/server.go 166-232 行)。该适配器把声明组装成 EvaluateRequest:ResourceType=deploymentResourceID="{projectID}:{声明名}"(如 1:order-api,跨项目同名声明审计可区分)、ResourceData 携带 digest/signer_id/config(整份声明)/vulns——其中 vulns 按 decl.Digest 匹配 artifacts.sha256storage_path="bundle://{digest}" 关联的漏洞严重级(findVulnsByDigest),查不到扫描结果不误拦。决策 deny → 返回 403 AUTHZ_ERROR,激活被拒、声明保持 draft(Sync 场景整次 sync 置 failed 并把已处理声明并入失败历史供回滚);warn 不拦截。该门禁还覆盖审批语义的分工:require_approval 通道的审批是「软门禁」(未过审不激活但 sync 成功),deny 是「硬门禁」(整次 sync 失败)。注意激活 handler 用 projectID=0 评估(server/handlers.go:308,DesiredState 为全局资源),故只有全局策略参与;已修复(提交 412e9c48)后模板「应用」落地的项目 #1 策略与页面新建的项目 #1 策略一样,都不会拦截手动激活路径——本页已无任何入口可创建全局策略(见第 6 章边界)。回滚(Rollback)属应急恢复不走门禁。 SBOM 与扫描的实现。SBOM 生成器产出 SPDX-2.3(请求 format 目前恒 spdx)JSON,写入 sbom 表(字段见 5.3),同制品单条幂等覆盖。扫描器 zy_builtinsecurity/scanner.go)是内置模拟实现(非真实 Trivy/Syft):先建 report(status=scanning)并回写制品 vulnerability_scan_status=scanning,随后跑四条内置规则——RULE-PLAINTEXT-SECRET(metadata/storage_path/tags 含明文密钥模式,high)、RULE-LATEST-TAG(tags 含 latest,low)、RULE-NO-SHA256(缺 sha256,low)、RULE-UNSIGNED-BUNDLE(bundle 制品 metadata 无 signer_id,medium)——再按制品 metadata 声明的 packages 依赖列表对 7 条小型 CVE 库(nginx CVE-2024-27080、log4j-core CVE-2021-44228、openssl CVE-2023-0286、spring-core CVE-2022-22965、jackson-databind CVE-2020-36518、guava CVE-2023-2976、mysql CVE-2022-27456,均为 critical/high/medium/low 样例)做版本命中;完成把 report 置 clean/vulnerable 并回写制品状态、扫描报告落盘 data/security/reports。也就是说「触发扫描」得到的漏洞报告内容是规则命中 + 样例库命中的合成物,可复现但非真实漏洞库。 模板应用的幂等与策略命名空间POST /security-policies/templates/{name}/apply 后端(handleApplyPolicyTemplate)按 ?project_id(缺省 0)解析归属;ApplyTemplate(project_id,name) 唯一约束创建策略并 enabled=true,已存在则幂等返回现有策略(HTTP 201)。模板策略的 default_config 会写入 rule_config(如 deny_latest_tag 的 {tag:"latest", message:"制品使用了 latest 标签,禁止部署"}),供 evaluateRule 取用。已修复(提交 412e9c48):页面调用 apply 时显式带 ?project_id=${PROJECT_ID}SecurityPage.vue:977),模板落地为项目 #1 策略而非全局。 compliance-report 统计口径:策略数统计 project_id=? OR project_id=0(合并项目+全局);scanned_artifacts 按项目下已扫描制品计数、vulnerable_artifacts 按检出漏洞的制品计数、vuln_count 累加漏洞条目。总览卡因此会比「安全策略(项目 #1)」表内的行数多出全局策略数——两处数字不必一致,属设计而非缺陷。

6. 权限与安全

7. 常见问题与排错

以下问题均能从 SecurityPage.vue 的 catch/alert 文案、后端 handler/策略引擎代码与既有测试复现。 1. 点「执行评估」先弹系统 alert「无权限执行该操作」、再看到 deny 违规表:原因是决策为 deny 时后端用 HTTP 403 返回(含 data 明细),拦截器对 403 统一弹「无权限」。处理:这是预期行为不是权限问题——页面已能正常展示 deny 的违规表(提交 412e9c48,SecurityPage.vue:692-702):系统 alert 后页内即渲染 decision=deny 与违规清单,无需再 curl 直调。 2. 评估总是 allow,期望被 deny 却没有:原因可能是①目标策略未启用;②策略 environments 限定了环境而资源快照的 environment 不匹配;③策略挂在项目 #1 而快照求值时…——注意页面评估 payload 恒带 project_id=1,后端会加载项目+全局策略,若你在「安全策略」Tab 建的策略 enabled=false 则不参与。处理:确认策略 enabled 开关为「已启用」、environments 留空或与快照 environment 匹配、severity 为 deny;再用 {"tags":["latest"],"environment":"prod"} 命中 deny_latest_tag 模板复现。 3. 合规总览的「策略数」比 Tab3 表格行数多:原因是总览统计口径是「项目 + 全局策略」合并(project_id=1 OR project_id=0),而 Tab3 只列项目 #1;模板「应用」落地的全局策略会计入总览但不显示在策略表。处理:属设计差异,可用「安全策略(n)」表的 n 与总览对照确认全局策略数 = 差值。 4. 漏洞报告显示为空态或扫描进行中,刷新也不变:status=scanning 时报告尚未生成(扫描进行中,请稍后刷新查看结果。),clean 时本就无条目(暂无漏洞条目(clean)。)。处理:等后端扫描完成(本页扫描器同步执行,通常立即完成)后重进制品或再点「触发扫描」刷新;仍为空则确认制品下拉已选中、且该制品确实执行过扫描(未扫描后端 404)。 5. 「漏洞报告」「项目漏洞报告」为空:属正常空态/加载中。已修复(提交 412e9c48):项目漏洞报告列表现解析后端 artifacts 键并平铺 report 字段(SecurityPage.vue:793-813)、制品漏洞报告优先读 data.report:752-774),有数据即渲染。处理:若仍为空,确认该制品确实执行过扫描(未扫描后端 404)、或状态为 scanning(尚未出结果)。 6. 「应用」模板提示失败:原因是模板名经 encodeURIComponent 编码后路径不匹配(含中文等需编码字符但后端未按编码解)或后端 security 服务未接线返回 404/500。处理:确认后端路由 POST /security-policies/templates/:name/apply 存在(server.go 954 行);重复应用为幂等返回既有策略(仍 201),可在策略表确认是否已创建;已修复(提交 412e9c48):调用带 ?project_id=${PROJECT_ID},模板落地为项目 #1 策略(SecurityPage.vue:977)。 7. 删除/启停策略按钮点了没反应或报「无权限」:原因是 busy 非空(有其它写操作进行中)时按钮禁用不可点;或当前 apollo_token 主体缺 security:write/execute 权限点。处理:等其它操作完成再看按钮态;到「角色管理」页把 security:* 权限点授给当前角色后重进。 8. 扫描后制品状态与报告不一致:扫描是同步的——POST /scan 返回时 report 已定态(clean/vulnerable)。若 Tab2 漏洞报告状态徽标还是旧值,多为 fetchVulnReport 早于扫描写回。处理:再点一次「触发扫描」(成功后自动重拉报告),或刷新页面看制品 vulnerability_scan_status。 9. 想验证真实部署也会被 deny 拦截:在制品管理登记制品并扫描出 critical 漏洞(或声明 digest 关联 bundle),应用「Critical 漏洞拒绝」模板到全局,再到「期望状态/部署与漂移」页激活引用该 digest 的声明。处理:激活应被 403 拒绝并保持 draft;在审计日志搜 SECURITY_GATE_EVALUATE(denied)确认拦截,而非只看本页手动评估。 10. curl 直调门禁接口怎么写curl -X POST http://127.0.0.1:18082/api/v1/security-gate/evaluate -H "Authorization: Bearer {apollo_token}" -H "Content-Type: application/json" -d '{"project_id":1,"resource_type":"artifact","resource_id":"demo","resource_data":{"tags":["latest"]}}'——deny 时返回 403 的 envelope(data 含 violations),allow/warn 返回 200。

8. 已知缺陷与边界

缺陷/边界说明
门禁 deny 违规表已修复(提交 412e9c48):页面 catch 在 err.response.status===403body.data.decision 存在时把 403 body 的 data 赋给 gateResult 并提示 deny(SecurityPage.vue:692-702),违规清单与 allow/warn 一样正常渲染。保留:拦截器仍会对 403 先弹系统级 alert('无权限执行该操作')(见下条「无权限」误报)
项目漏洞报告恒为空已修复(提交 412e9c48)fetchProjVulns 现优先 toList(data.artifacts) 并平铺每行 report 字段(status 取 report.status || scan_statusSecurityPage.vue:793-813),有数据即渲染
应用模板落地全局策略已修复(提交 412e9c48):调用现带 ?project_id=${PROJECT_ID}SecurityPage.vue:977),模板落地项目 #1,与 confirm 文案一致
页面策略均为项目 #1PROJECT_ID=1 常量硬编码(SecurityPage.vue:472);模板应用修复后也落项目 #1,本页无入口创建全局策略。项目级策略不参与激活门禁(激活按 projectID=0 只查全局,server/handlers.go:308)——要拦截手动激活需在全局作用域创建策略,页面无法完成
扫描器为内置模拟zy_builtin 是规则命中 + 7 条样例 CVE 库,非真实 Trivy/Syft;漏洞库小、无依赖图、无历史增量。SBOM 为 SPDX 格式的 JSON 文本,本页以 pre 展示不语法着色
门禁评估的「无权限」误报deny 的 HTTP 403 被拦截器当作权限拒绝弹系统级 alert('无权限执行该操作'),与真实 40301 无法区分;演示场景需人工识别。属拦截器与门禁语义耦合(后端已注释说明保留 403 语义)
无分页/无导出策略、模板、项目漏洞汇总均为全量返回;findings 表不分页;扫描结果与 SBOM 无下载按钮(需脚本调接口)
合规卡失败静默已修复(本批)fetchCompliance 失败改渲染独立错误态「加载失败:{msg}」+「重试」按钮(SecurityPage.vue:60-67/657-660),与九张卡 v-else 互斥,不再静默保留 0 值

附录 A:页面文案速查(与 SecurityPage.vue 逐字一致)

场景源码取值/文案
页面标题Apollo 安全合规
顶部说明安全合规(TAD-10):对制品生成 SBOM 物料清单、执行漏洞扫描;安全策略引擎按 deny_latest_tag / require_signature / critical_vuln / no_plaintext_secret / require_resource_limits / custom 规则在部署前强制安全门禁,合规总览汇总项目安全态势。
Tab合规总览 / 扫描与漏洞 / 安全策略
门禁卡标题/说明安全门禁手动评估(Security Gate)模拟部署前门禁检查:提交资源快照(tags / digest / signer_id / config / vulns / environment),由启用策略求值汇总 allow / warn / deny 决策并记录审计轨迹。
门禁表单标签项目 id(project_id) / 资源类型(resource_type)* / 资源 id(resource_id) / 资源数据(resource_data JSON);resource_data placeholder 见 4.3
resource_data 支持键tags / sha256 / digest / signer_id / config(对象或字符串)/ vulns / environment
资源类型选项artifact(制品) / deployment(部署) / config(配置)
评估按钮/文案执行评估 / 评估中...;成功 安全门禁评估完成:decision={dec};失败 安全门禁评估失败:{msg};JSON 错 resource_data 不是合法 JSON:{e.message}
评估结果区评估结果:命中启用策略 {n} 条 · 评估时间 {t}全部启用策略通过,未命中违规。
扫描卡标题制品漏洞扫描(SBOM / Scan);空态 暂无制品(Tab 制品管理登记后此处可选)
扫描按钮生成 SBOM / 生成中... / 触发扫描 / 扫描中...;SBOM 成功 SBOM 生成成功(format=spdx)
空态文案报告 扫描进行中,请稍后刷新查看结果。 / 暂无漏洞条目(clean)。;项目列表 暂无漏洞报告,先去 Tab 制品管理登记并扫描制品。;提示 选择制品后点击"触发扫描"生成漏洞报告;切换制品会自动加载已有报告。
策略表空态暂无安全策略,点击"新建安全策略"或从下方模板快速应用(项目 #1)。
启停/删除confirm 确定删除安全策略 "{name}"(#{id})吗?;成功 安全策略 #id 已启用/停用 / 安全策略已删除
模板卡标题 内置策略模板(5 类安全基线);confirm 确定应用策略模板 "{name}" 到项目 #1 吗?(幂等创建并默认启用);成功 模板 "{name}" 应用成功(策略 id=..);底部 应用模板按 (project_id, name) 幂等创建策略并默认启用。
新建/编辑弹窗新建安全策略 / 编辑安全策略 #{id}(项目 #1);必填校验 策略名称(name)必填;创建 安全策略创建成功(id=..);保存 安全策略 #{id} 更新成功 / 保存安全策略失败:{msg}

附录 B:5 类内置策略模板(与 security.ListTemplates() 一致)

模板名称分类规则类型级别描述
禁止生产 latest 标签imagedeny_latest_tagdeny制品/镜像不得使用 latest 等可变标签,强制不可变版本溯源
部署必须签名deployrequire_signaturewarn部署的 bundle/制品必须携带签名(digest 与 signer_id 均非空)
Critical 漏洞拒绝compliancecritical_vulndeny制品存在 critical 级漏洞时拒绝部署
禁止明文密钥configno_plaintext_secretdeny部署配置不得包含 password=/secret=/api_key= 明文密钥
部署必须设置资源限制deployrequire_resource_limitswarn部署配置必须声明 resources.limits(CPU/内存上限)

分类取值:image / config / compliance / deploy;RULE_TYPES 六项长名(前端下拉逐字):deny_latest_tag(禁止 latest 标签) / require_signature(部署必须签名) / critical_vuln(Critical 漏洞拒绝) / no_plaintext_secret(禁止明文密钥) / require_resource_limits(必须设置资源限制) / custom(自定义模式匹配)