先看结论与判断条件
- 紧急发布应是预先定义的受控通道,具备触发条件、决策人、最小门禁、证据格式和退出规则,不是事故发生后临时删除测试。
- 候选摘要、源码和依赖、构建 provenance、签名证书、applicationId、versionCode 与加固配置必须绑定,换包后旧回执立即失效。
- 缩减范围依据实际变更和影响路径:Native、依赖、Manifest、签名、协议或加固配置变化都会追加不同门禁,不能只看修改行数。
- 安装、更新、启动、变更核心路径、加固目标、至少一个目标设备和停止推进方案属于底线;静态构建成功不能替代设备语义。
- 延后测试必须记录 gate_id、原因、风险、approver、owner、expires_at 和 supplement_plan,逾期未补测时升级为发布阻断或事件复盘项。
- Track API 返回成功只证明控制面接受状态,不证明审核、分发、兼容或质量通过;渠道回执、线上状态和业务监控要分别取证。
先把紧急通道设计成正式发布路径
紧急修复最容易出问题的时刻,是团队第一次在事故中讨论哪些测试可以不做。正确做法是在平时建立 emergency lane,写明触发条件、incident owner、security owner、release owner、允许的变更类型、不可删除门禁、可延后范围、最长债务期限和停止推进条件。事故发生时只需要套用规则并记录例外,不重新发明流程。
紧急不等于所有高优先级需求。触发条件应是明确业务或安全影响,需要在标准发布周期之前恢复服务、阻断持续损失或修复渠道拒绝。纯排期压力、营销日期或测试资源不足,不应自动获得紧急豁免。触发记录要包含 incident_id、影响、受影响版本、修复目标和授权决策人。
通道的目标是压缩等待与低相关测试,而不是降低候选真实性。并行准备构建证明、签名、测试数据和发布材料可以节省时间;提前维护核心回归、设备快照和回滚候选也可以缩短路径。删除候选核对、跳过设备验证或用旧包结果替代新包,只会把事故从服务端转移到客户端。
| 环节 | 标准路径 | 紧急路径 | 不可改变 |
|---|---|---|---|
| 触发 | 计划版本与完整排期 | 符合预定义事件条件 | 需要 owner 和审批记录 |
| 测试广度 | 目标完整矩阵 | 按变更影响选择最小矩阵 | 变更核心路径和目标设备必须执行 |
| 测试顺序 | 可串行等待 | 构建、证据和设备准备并行 | 每项回执仍绑定最终候选 |
| 延后项 | 通常发布前完成 | 允许有限风险债务 | 有批准、期限、owner 和补测计划 |
| 发布范围 | 按常规渠道策略 | 从受控范围开始并设停止条件 | 签名、版本和渠道身份正确 |
| 事后动作 | 常规监控与回顾 | 补测、债务关闭和事件复盘 | 不能用线上无投诉替代补测 |
冻结事件、变更和最终候选身份
紧急修复开始后先冻结基线版本、修复提交和允许变更文件。每次构建记录 source_sha256、dependency_lock_sha256、variant、applicationId、versionCode、hardening_config_sha256、toolchain_id、signing_cert_sha256 和 artifact_sha256。候选一旦变化,就产生新的证据链;旧候选的设备结果不能通过相同版本名继续使用。
SLSA Provenance v1.1 可以把最终产物 subject 与 builder、buildType、externalParameters 和依赖材料绑定,适合证明紧急候选来自哪个提交、配置和构建者。in-toto Attestation Statement v1 可进一步把候选摘要与测试、签名和审批 predicate 绑定。两者都不证明运行时安全,声明签发者与内容仍需验证。
变更说明不能只列 Git diff。还要记录生成代码、依赖解析、资源与 Manifest 合并、R8 规则、Native 库、服务端协议和加固配置是否变化。某个 Java 方法只改一行,如果它改变授权协议或 JNI 调用,影响面可能很大;大段文案资源变化则可能与核心保护无关。门禁由影响机制决定,不按行数决定。
- 事件、修复目标、基线版本和授权 owner 已冻结
- 源码、依赖、变体、工具链和配置摘要入清单
- 最终 artifact 摘要与 provenance subject 一致
- 签名证书、applicationId 和 versionCode 可核对
- 生成代码、资源、Manifest、R8 和 Native 变化已盘点
- 任何换包都会废止旧候选未完成的门禁引用
按变化类型生成最小而完整的测试矩阵
变更分类至少覆盖 business_logic、dependency、native、manifest_resource、backend_protocol、signing_identity 和 hardening_config。每类变化对应不同风险域。业务逻辑影响目标断言和邻接路径,依赖影响初始化与传递行为,Native 影响 ABI 和加载,Manifest 与资源影响组件和权限,签名影响更新身份,加固配置影响保护范围和兼容。
多个类别同时出现时,门禁取并集而不是选择最轻的一类。例如一次修复既升级 Native SDK 又调整加固排除范围,需要执行 ABI 加载、目标业务、异常路径、保护覆盖和兼容回归。紧急流程可以减少设备数量或外围路径,但不能因为主要目标是修复业务缺陷,就忽略随包进入的依赖与配置变化。
变化分类还要考虑未变化证明。若声称签名、依赖或加固配置未变,应提供摘要比较,而不是口头确认。摘要相同只说明对应文件字节一致,仍要核对构建是否使用了它。对于无法证明的字段,最小矩阵按发生变化处理,宁可增加相关测试,也不要把未知状态当稳定状态。
| 变化类型 | 主要风险 | 必须追加 | 不能仅靠 |
|---|---|---|---|
| 业务逻辑 | 修复与邻接路径语义改变 | 目标断言、反例和核心业务闭环 | 单元测试或编译成功 |
| 依赖或工具链 | 初始化、传递依赖和输出变化 | 解析差异、启动、关键 SDK 与设备 smoke | 锁文件名称相同 |
| Native | ABI、加载、符号和线程边界变化 | 目标 ABI 加载、JNI 核心路径和异常退出 | 模拟器单一结果 |
| Manifest 或资源 | 组件、权限、链接和资源选择变化 | 最终合并结果、入口和设备资源路径 | 源码 diff |
| 签名或应用身份 | 安装更新和平台服务身份改变 | 签名、覆盖更新、链接和后端配置 | 签名命令 exit 0 |
| 加固配置 | 保护范围与兼容语义变化 | 目标资产、启动、异常和关键设备回归 | 配置文件名相同 |
候选、签名、安装和核心路径属于不可删除门禁
任何紧急候选都要完成身份门禁:产物摘要、来源证明、applicationId、versionCode、签名证书和加固配置一致。How Android app updates work 说明覆盖更新需要保持 applicationId、兼容签名或轮换证明,并使用可接受的 versionCode。这些是平台更新条件,不等于业务防重放,但一旦错误,修复包可能根本无法覆盖用户现有版本。
设备门禁至少验证安装或覆盖更新、首次启动、修复目标、一个关键反例、加固目标和基本异常恢复。依赖真实 Android 运行时、组件和系统 API 的语义应通过 instrumented test 在设备端执行。单一设备通过不能代表完整矩阵,所以紧急结论必须标注设备、API、ABI 和未覆盖范围,并安排补测。
安全修复还要验证修复没有被加固配置排除或改变。如果修复位于高价值保护路径,保护检查和业务兼容必须针对最终签名候选重新执行;若临时降低保护范围,需要明确风险批准和恢复计划。不能以“先不上加固,后面再补”作为默认路径,因为这会改变发布候选的安全边界和后续差分。
| 门禁 | 最小回执 | 失败含义 | 处置 |
|---|---|---|---|
| 候选身份 | artifact、source、config 与 subject 摘要 | 测试对象无法确认 | 阻断 |
| 签名更新 | 证书、包名、版本和覆盖安装结果 | 现有用户可能无法更新 | 阻断 |
| 安装启动 | 目标设备安装、启动和阶段码 | 候选不可交付或不可进入 | 阻断 |
| 修复目标 | 正向断言与关键反例 | 事件原因未关闭或引入误判 | 阻断 |
| 加固目标 | 目标资产与配置覆盖回执 | 安全边界与审批不一致 | 阻断或正式风险接受 |
| 停止推进 | 负责人、触发条件和前向修复方案 | 上线后无法受控处置 | 阻断 |
延后项要成为有期限的风险债务
紧急路径可延后的是与当前变化关联较低、执行时间长且有替代监控的测试广度,例如外围功能全量回归、扩展设备矩阵、长时间稳定性或低风险地区渠道抽样。是否可延后由项目风险策略决定,不能建立永久豁免列表。涉及候选身份、签名、修复目标、加固目标和关键设备语义的门禁不得仅靠补测承诺。
每个 deferred gate 都要记录 gate_id、reason、risk_statement、approver、owner、expires_at、supplement_plan 和 blocking_if_overdue。审批人接受的是明确候选和时间窗内的残余风险,不能用群聊“先发”代替。到期前完成补测后,回执仍要绑定原紧急候选;若线上已切换到另一个版本,旧债务也应关闭或说明为何不再适用。
暂缓不应被写成通过。报告状态至少区分 passed、failed、deferred、inconclusive 和 not_applicable。deferred 有批准和补测,inconclusive 表示测试执行或证据不足,不能自动延期;not_applicable 要有范围理由。把未运行项目统一写绿,会让事件复盘无法知道实际承担了什么风险。
- 只有低关联测试广度进入 deferred 候选
- 每个延后项写明风险、审批人、owner 和到期时间
- 补测计划包含候选、设备、用例和证据输出
- 逾期处置明确为阻断、升级或事件复盘
- deferred、inconclusive 与 not_applicable 分开记录
- 线上无投诉不会自动把 deferred 改成 passed
受控发布、停止推进和前向修复要同时准备
紧急候选通过最小门禁后,应从受控范围开始发布,并预先定义继续、暂停和停止推进条件。条件可以来自安装失败、启动异常、修复目标失效、关键业务错误和渠道状态,但阈值必须由项目真实基线与风险策略决定。没有历史数据时不要虚构百分比,应使用可审计事件和人工审批。
Google Play Developer API track update 可以通过授权请求提交 Track 期望状态并返回 Track 资源。API 成功是控制面回执,不证明商店审核、用户分发、设备兼容或业务质量通过。发布系统应分别保存请求对象、返回资源、商店状态、真实安装和业务监控,避免把 HTTP 成功当上线成功。
回滚在移动发布中不一定等于安装旧版本。签名、versionCode、渠道和已安装用户状态会限制可用动作,因此紧急计划应优先准备合法的前向修复候选,并定义暂停继续分发、服务端降级或业务开关。任何修复候选仍需新摘要、新签名回执和最小门禁,不能复用紧急包结果。
- 受控发布范围和继续、暂停、停止条件明确
- Track 控制面、商店状态、安装和业务监控分别取证
- 没有基线时不虚构告警百分比或通用阈值
- 前向修复候选、签名和最小回归能够快速生成
- 服务端降级和客户端发布动作边界已说明
- 换候选后所有旧回执按摘要重新评估
用只读脚本生成并检查紧急最小门禁
下面的 Python 脚本读取脱敏紧急发布计划,根据 change_types 合并必须门禁,并检查候选摘要、provenance subject、gate 状态和 deferred 记录。基础门禁始终包含 candidate_identity、signing_update、install_launch、changed_path、hardening_scope、core_device、release_approval 和 stop_forward_fix。
变更类型会追加相关门禁,例如 native 追加 ABI 与 JNI,signing_identity 追加覆盖更新和平台服务,hardening_config 追加保护目标和兼容异常路径。required gate 不能标记 deferred;其他被延后项目必须具备 reason、risk_statement、approver、owner、expires_at 和 supplement_plan。脚本只输出 BLOCK 或 READY_FOR_EMERGENCY_REVIEW。
代码不会构建、签名、安装或调用发布接口,也不会判断业务严重性。它只验证计划结构和证据引用,真实 owner 仍需核对回执、设备和风险。日期字段要求非空但未用作通用时限判断,逾期规则由项目调度系统执行,避免在示例中虚构适用于所有事件的期限。
import json
import sys
from pathlib import Path
BASE_GATES = {
"candidate_identity",
"signing_update",
"install_launch",
"changed_path",
"hardening_scope",
"core_device",
"release_approval",
"stop_forward_fix",
}
CHANGE_GATES = {
"business_logic": {"target_assertion", "negative_case"},
"dependency": {"dependency_diff", "sdk_startup"},
"native": {"abi_load", "jni_core_path"},
"manifest_resource": {"merged_manifest", "resource_entry"},
"backend_protocol": {"contract_case", "offline_error"},
"signing_identity": {"upgrade_path", "platform_services"},
"hardening_config": {"protection_target", "compatibility_exception"},
}
DEFER_FIELDS = {"reason", "risk_statement", "approver", "owner", "expires_at", "supplement_plan"}
def stop(message):
raise SystemExit(f"invalid emergency plan: {message}")
def nonempty(value):
return isinstance(value, str) and bool(value.strip())
if len(sys.argv) != 2:
raise SystemExit("usage: python emergency_gate.py PLAN_JSON")
plan_path = Path(sys.argv[1]).resolve()
if not plan_path.is_file():
stop(f"file not found: {plan_path}")
payload = json.loads(plan_path.read_text(encoding="utf-8"))
candidate = payload.get("candidate")
if not isinstance(candidate, dict):
stop("candidate must be an object")
for field in ("artifact_sha256", "source_sha256", "hardening_config_sha256", "provenance_subject_sha256"):
value = candidate.get(field)
if not isinstance(value, str) or len(value) != 64:
stop(f"candidate.{field} is not a SHA-256 string")
if candidate["artifact_sha256"] != candidate["provenance_subject_sha256"]:
stop("provenance subject does not match the candidate")
change_types = payload.get("change_types")
if not isinstance(change_types, list) or not change_types:
stop("change_types must be a non-empty list")
unknown = sorted(set(change_types).difference(CHANGE_GATES))
if unknown:
stop(f"unknown change types: {unknown}")
required = set(BASE_GATES)
for change_type in change_types:
required.update(CHANGE_GATES[change_type])
gates = payload.get("gates")
if not isinstance(gates, list) or not gates:
stop("gates must be a non-empty list")
by_id = {}
for index, gate in enumerate(gates):
if not isinstance(gate, dict) or not nonempty(gate.get("gate_id")) or gate.get("status") not in {"passed", "failed", "deferred", "inconclusive"}:
stop(f"gate {index} is invalid")
if gate["gate_id"] in by_id:
stop(f"duplicate gate {gate['gate_id']}")
by_id[gate["gate_id"]] = gate
missing = sorted(required.difference(by_id))
blocked = bool(missing)
for gate_id in required.intersection(by_id):
if by_id[gate_id]["status"] != "passed" or not nonempty(by_id[gate_id].get("receipt_id")):
blocked = True
deferred_review = []
for gate in gates:
if gate["status"] != "deferred":
continue
absent = sorted(field for field in DEFER_FIELDS if not nonempty(gate.get(field)))
if gate["gate_id"] in required or absent:
blocked = True
deferred_review.append({"gate_id": gate["gate_id"], "missing": absent, "required": gate["gate_id"] in required})
output = {
"required_gates": sorted(required),
"missing_gates": missing,
"deferred_review": deferred_review,
"gate": "BLOCK" if blocked else "READY_FOR_EMERGENCY_REVIEW",
}
print(json.dumps(output, ensure_ascii=False, indent=2, sort_keys=True))发布后补测要关闭原候选债务并反哺常规流程
紧急版本上线后,第一项工作不是宣布事件结束,而是按 supplement_plan 完成延后矩阵。补测必须使用原紧急候选摘要和相同签名身份,才能关闭当时债务;若已经产生后续版本,要分别说明旧候选风险是否仍影响已安装用户,以及新候选需要哪些独立回归。
补测发现失败时,要依据预先定义的停止推进、服务端降级和前向修复动作处理,不能因版本已上线而降低严重性。事件复盘应比较标准流程与紧急流程,确认哪些并行化有效、哪些准备不足、哪些延后项实际高风险,并把结论更新到 emergency lane,而不是只追究执行速度。
NIST SP 800-218 SSDF 强调来源、构建、验证和变更证据,紧急发布也应保持同一原则。需要设计实际紧急加固通道时,可通过御盾中央平台提交脱敏事件类型、变更清单、候选身份、加固范围、核心设备、发布渠道和修复方案,先生成最小门禁,再由责任人批准风险债务。
- 补测回执绑定原紧急候选和原 deferred gate
- 后续版本不会覆盖旧候选已安装用户风险
- 补测失败触发预定义停止或前向修复动作
- 事件复盘更新变化分类、设备准备和并行流程
- 风险债务关闭记录由 owner 与 approver 共同确认
- 紧急通道定期演练并保持候选证据模板可用
事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| 安全发布应保留来源、构建、验证与变更证据,并把供应链风险纳入流程。 | NIST SP 800-218 SSDF 把来源追溯、构建完整性、验证、维护和供应链实践纳入安全开发。 | SSDF 是组织级实践框架,不定义 App 加固产品功能,也不规定紧急发布的具体测试数量。 |
| 构建证明应绑定产物主体、构建者、构建类型、外部参数与依赖材料。 | SLSA Provenance v1.1 定义 subject、builder、buildType、externalParameters 和 resolvedDependencies 等字段。 | provenance 只能证明记录的构建关系,不能单独证明紧急候选运行时安全或兼容。 |
| 供应链声明应把产物摘要与有类型的声明负载绑定。 | in-toto Attestation Statement v1 使用 subject 摘要、predicateType 和 predicate 组织证明。 | 声明格式不保证签发者可信或内容真实,仍需验证签名、subject 和具体门禁。 |
| Android 更新需要保持 applicationId、兼容签名或轮换证明,并使用可接受的 versionCode。 | How Android app updates work 说明应用身份、versionCode、签名证书和轮换证明对更新的要求。 | 平台更新条件不等于业务防重放策略,也不证明紧急修复目标或加固范围正确。 |
| 发布系统可通过受授权的 Track 更新提交状态并获得返回资源。 | Google Play Developer API track update 描述 edits.tracks.update 的请求与 Track 返回对象。 | API 更新成功不证明商店审核、真实分发、设备兼容或业务质量门禁通过。 |
| 依赖真实 Android 运行时、组件和系统 API 的语义应通过设备端测试验证。 | Android instrumented tests 说明 instrumented test 在 Android 环境运行并可访问真实框架和组件。 | 单一设备通过不能代表完整 API、ABI 和厂商矩阵,紧急流程仍需明确补测范围。 |
| 紧急发布可以缩减测试广度,但不能删除候选、签名、核心路径、加固范围和发布控制门禁。 | 工程判断:这些门禁分别保证测试对象、更新身份、修复目标、安全边界和上线处置可验证,无法由其他成功状态替代。 | 具体最小矩阵、设备选择和风险阈值必须依据项目变更、用户范围和历史基线确定。 |
| 延后测试应形成有批准、责任人、期限和补测计划的风险债务。 | 工程判断:未运行测试若只标记通过,会掩盖真实风险并使上线后无法追踪原候选的未覆盖范围。 | 没有项目候选和真实回执时,不能声明紧急版本已通过、风险已关闭或发布效果已验证。 |
工程常见问题
紧急修复是否可以先不加固,发布后再补?
不能作为默认方案。去除加固会改变安全边界和候选身份;若确需降低范围,必须正式批准风险并建立恢复与补测计划。
只修改一行代码,是否只测这个函数就够了?
不一定。要看它是否影响协议、依赖、JNI、签名、Manifest 或加固范围,门禁按影响机制而不是修改行数决定。
紧急发布最少要保留哪些测试?
至少保留候选来源、签名更新、安装启动、修复目标与反例、加固目标、目标设备和停止推进或前向修复证据。
未完成的设备矩阵可以全部标记 deferred 吗?
只有低关联范围可以延后,且每项需要风险批准、owner、期限和补测计划;关键设备与变更相关路径不能仅靠承诺。
Track API 返回成功后是否可以认为紧急版本上线成功?
不能。它只是控制面回执,还要核对商店状态、真实安装、业务监控和质量门禁,并保留停止推进条件。
准备紧急加固发布评审需要哪些材料?
准备事件与变更分类、候选摘要、provenance、签名、加固配置、最小设备矩阵、deferred 债务、审批和修复方案。