先看结论与判断条件
- 评估输入必须是发布变体实际解析出的直接与传递依赖快照,build.gradle 中只改一个版本号不代表最终只变化一个组件。
- SDK 的初始化入口、ContentProvider、反射名称、注解扫描、序列化模型和消费者保留规则变化,会直接影响加固后的类名、入口和启动兼容。
- 新增权限、组件、网络端点、数据类型或 required reason API 会改变隐私与合规边界,清单更新不能替代运行时数据行为验证。
- 新增或替换 Native 库需要重新核对 ABI、装载、JNI、运行库和符号边界,但本流程只计算影响范围,不替代专项 SO 验收。
- 公开 SDK 索引和平台要求可辅助判断版本、可靠性、安全与声明责任,但不能覆盖私有 SDK,也不能证明升级候选已经安全或兼容。
- 任何依赖版本、内容摘要、初始化、权限、隐私、Native 或构建规则变化都应生成新候选,并让证据与候选摘要、变体和复测范围绑定。
加固配置依赖的是解析结果,不是一个版本号
第三方 SDK 升级看似只是把 3.1 改成 3.2,实际可能同时替换传递依赖、资源、Manifest 合并项、消费者混淆规则、Native 库和初始化组件。加固配置通常依赖类名、包路径、反射入口、JNI 桥、动态加载和敏感代码范围,只要解析图变化,原来的排除、保留和保护规则就可能失去依据。
Android Gradle dependency resolution 说明直接与传递依赖会形成版本解析图,最终版本受依赖声明、约束和冲突选择影响。评估必须针对实际发布 variant 保存 resolved coordinate、选择原因、依赖路径和归档摘要,不能用 debug 依赖树或源码声明代替 release 解析结果。
重新评估不等于每次升级都推翻全部配置。正确做法是冻结旧快照与新快照,先计算增加、删除、版本变化和同版本内容摘要漂移,再按变化类型选择受影响门禁。本文没有具体 SDK、候选与设备回执,只提供影响计算方法,不声称御盾或任何产品已对某次升级完成兼容验证。
| 字段 | 回答的问题 | 来源 | 缺失风险 |
|---|---|---|---|
| variant | 评估哪个发布配置 | 构建变体 | 用 debug 代替 release |
| coordinate/version | 解析了哪个组件版本 | 依赖解析图 | 只看到声明版本 |
| artifactSha256 | 实际内容是否变化 | AAR、JAR 或 framework | 同版本内容漂移遗漏 |
| dependencyPath | 谁传递引入 | 选择路径 | 无法定位升级所有者 |
| metadata | 初始化、权限、Native 与隐私有什么 | 归档和构建产物 | 影响范围计算失真 |
先比较完整解析图,再定位直接和传递变化
一项直接 SDK 升级可能让多个传递库升降级,也可能因为版本约束导致另一个模块选择不同版本。旧新快照应以 group、name、classifier、variant attributes 和解析版本组成稳定键,再比较 artifactSha256。仅比较 Maven 坐标会漏掉仓库覆盖、动态版本或缓存内容变化。
Debug Android dependency resolution 建议从具体变体的依赖树和实际解析结果定位重复类与版本冲突。它支持排查构建阶段的问题,但构建成功不代表加固后的运行时冲突已经消失。升级评估要保存 selection reason 和完整依赖路径,明确某个变化是直接声明、约束选择还是传递替换。
变化应分类为 added、removed、version-changed、content-changed 和 relationship-changed。removed 也需要复核,因为旧加固配置可能还保留已删除包名,造成无效规则或掩盖真实入口;relationship-changed 表示组件版本未变但所有者路径变化,责任与初始化顺序仍可能不同。
| 差异 | 可能原因 | 首要检查 | 不能直接推断 |
|---|---|---|---|
| added | 新增传递依赖或功能 | 入口、权限、规则和 Native | 一定被业务调用 |
| removed | 替换或功能收敛 | 清理配置并回归消费者 | 没有残留引用 |
| version-changed | 直接升级或冲突选择 | 发布说明与二进制差异 | 新版本一定兼容 |
| content-changed | 同版本重新发布或仓库漂移 | 阻断并确认来源 | 坐标可代表内容 |
| relationship-changed | 依赖路径或约束变化 | 所有者与加载顺序 | 运行行为保持不变 |
初始化、反射和消费者规则会改变类变换边界
SDK 可以通过 Application 回调、ContentProvider、App Startup、Manifest 组件、注解扫描或显式初始化进入进程。升级后若入口类、authority、meta-data 或初始化顺序变化,旧排除规则可能漏掉新入口,旧保留规则也可能指向已删除类。评估必须从合并 Manifest 与实际归档提取入口,而不是只读接入文档。
反射、序列化、JNI 查类和动态加载通常依赖字符串名称或生成代码。SDK 新增反射命名空间、模型类、插件入口或 ServiceLoader 配置时,类重命名和裁剪策略需要重新验证。消费者 ProGuard/R8 规则的增加、删除或改动也应进入差异报告,因为它们可能改变最终保留面,且“构建不报错”不能证明运行路径已覆盖。
配置修正要保持最小范围。发现一个新反射入口,不应直接排除整个 SDK 或应用包;应定位真实调用链、所需成员和失败条件,并让测试触发该入口。过宽规则会减少保护覆盖并积累历史债务,过窄规则则可能导致启动、回调或序列化失败,两者都要通过候选对比和设备回归判断。
| 对象 | 差异输入 | 加固动作 | 回归路径 |
|---|---|---|---|
| Manifest 组件 | 组件名、authority、meta-data | 更新入口保留 | 冷启动与组件触发 |
| 显式初始化 | API、顺序与参数 | 核对调用与保护范围 | 成功、重复和失败初始化 |
| 反射入口 | 字符串类名和成员 | 最小 keep 或映射 | 真实反射调用 |
| 序列化模型 | 字段、注解和生成器 | 核对重命名规则 | 读写与升级数据 |
| 消费者规则 | consumer rules 摘要 | 逐条审阅变化 | release 构建和运行 |
权限、组件和数据行为变化要进入隐私复核
SDK 升级可能通过 Manifest 合并新增权限、组件、查询项或服务配置,也可能改变采集数据、网络端点和后台行为。评估要比较旧新合并 Manifest、平台隐私声明和运行时网络或数据观察,区分 SDK 声明、应用实际启用和业务允许范围。仅看到新权限存在不能证明它已经被使用,也不能忽略它带来的审核责任。
Apple third-party SDK requirements 强调开发者对所集成 SDK 的代码与数据行为负责,部分常用 SDK 还涉及签名和 privacy manifest 要求。该要求面向 Apple 平台,不覆盖 Android 依赖,也不代表名单中的 SDK 已通过安全评估。跨平台项目应分别维护 Android 与 Apple 产物证据,再在产品数据清单层汇合。
Apple privacy manifests 要求 App 与第三方 SDK 的数据收集和 required reason API 进入有效声明。SDK 升级后应比较 PrivacyInfo.xcprivacy 的 API 类别、理由、数据类型和跟踪声明,并与实际功能复核。manifest 是声明与审核材料,不是运行时数据隔离;声明完整仍不能证明 SDK 没有超出范围传输数据。
| 变化 | 静态证据 | 运行复核 | 结论边界 |
|---|---|---|---|
| 新增权限 | 合并 Manifest 差异 | 触发条件与拒权路径 | 存在不等于已使用 |
| 新增组件 | 导出、权限与 intent | 真实启动和访问控制 | 注册不等于可达 |
| 数据类型变化 | SDK 与产品声明 | 采集、传输和删除路径 | 文档不等于行为 |
| required reason API | privacy manifest | 调用场景与理由 | 声明不等于隔离 |
| 网络端点变化 | 配置与二进制线索 | 受控网络观察 | 一次观察不覆盖全部路径 |
Native 库变化会扩大 ABI、装载和 JNI 回归范围
SDK 新版本可能首次携带 SO、替换既有 Native 库、改变支持 ABI、引入共享 C++ 运行库或调整 JNI 注册。依赖差异快照应记录每个归档内的 Native 路径、ABI、SHA-256 和库名;只比较 AAR 总摘要无法直接告诉测试团队哪些架构与装载路径受影响。
本流程的目标是计算需要重新验收的范围,而不是替代预编译 SO 专项。发现 Native 集合变化后,应路由到 ABI 完整性、ELF 依赖、SONAME、符号、JNI、异常和目标设备装载检查。静态看到文件存在不证明它会被加载,也不能从一个 ABI 的结果外推其他发布 ABI。
若升级删除某个 SO,也要检查 Java 或 Kotlin 层是否仍调用 System.loadLibrary、动态功能是否仍引用它,以及打包规则是否残留 pickFirst 或 exclude。删除能够减少产物内容,却可能留下延迟触发的加载错误。加固配置中的 Native 白名单、保护目标和符号材料应同步清理并生成新候选。
| 变化 | 必须记录 | 触发检查 | 不能仅凭 |
|---|---|---|---|
| 新增 SO | ABI、路径、摘要 | 装载、JNI、符号与运行库 | 归档存在 |
| SO 内容变化 | 旧新 SHA-256 | ABI 契约和设备回归 | 文件名相同 |
| ABI 集变化 | 新增与缺失架构 | 发布范围与真机矩阵 | 一个 ABI 通过 |
| 运行库变化 | libc++ 链接与依赖 | 异常、RTTI 和装载 | 构建成功 |
| SO 删除 | 旧加载入口与规则 | 延迟路径和配置清理 | 包体变小 |
平台索引和厂商材料只能作为评审输入
Google Play SDK Index 可以为 SDK 评审提供版本采用、权限、可靠性、安全和数据处理等指引。升级前后应记录索引状态和供应方发布说明,识别弃用版本、权限变化或已知政策信息。SDK Index 不覆盖全部私有、开源或内部 SDK,也不能替代实际解析图与二进制清单。
供应方 changelog、迁移指南、SBOM、签名和隐私材料可以解释变化,但不能替代应用内验证。文档未提到传递依赖变化,不代表解析结果没有变化;文档声称兼容,也不代表应用的反射、加固、网络和生命周期路径已经通过。每条外部结论都要与本地可观察差异建立对应关系。
评审记录应区分 source-fact、local-observation 和 engineering-decision。source-fact 是平台或供应方明确说明,local-observation 是构建产物和设备回执,engineering-decision 是依据业务风险决定复测范围。把三者分开,能避免将索引标签写成实际候选结论,也能解释为什么某些无公开材料的私有 SDK仍需更严格验证。
| 资料 | 可支持 | 不能支持 | 本地补证 |
|---|---|---|---|
| SDK Index | 版本与平台指引 | 完整私有 SDK 覆盖 | 解析图和产物清单 |
| changelog | 供应方声明的变化 | 未声明部分未变化 | 二进制差异 |
| 迁移指南 | 预期接入动作 | 应用已正确迁移 | 构建与设备回归 |
| privacy manifest | 声明的 API 与数据 | 运行时数据隔离 | 行为与网络复核 |
| 供应方测试报告 | 其环境内结果 | 本应用与加固候选通过 | 同候选本地证据 |
用变化类型计算最小但完整的复测集合
差异分析的输出不应只是“升级了三个库”,而要为每项变化生成 affectedGates。解析图变化触发 release 构建与核心路径,初始化变化触发冷启动、重复初始化和失败恢复,反射与规则变化触发对应动态入口,权限与隐私变化触发拒权、数据和声明审核,Native 变化触发所有受影响 ABI 的专项门禁。
复测集合可以合并相同门禁,但不能因为变化多就用一次冒烟测试替代。每个 gate 要记录触发它的 dependency key、差异字段、业务路径、目标环境和证据要求。若无法判断某项元数据是否改变,应标记 needs-inventory,不得默认 unchanged。测试结果必须绑定新候选摘要,旧候选回执只作为基线。
配置变更也应可追溯。对 keep、exclude、加固目标、Native 规则或隐私声明的每次调整,记录原因、owner、差异来源和批准;生成候选后重新导出快照,确认配置处理的是实际新依赖,而不是文档中的预期版本。范围过大或过小都应在代码审查和运行回归中暴露。
| 变化类别 | 最低门禁 | 附加条件 | 阻断状态 |
|---|---|---|---|
| 解析版本或摘要 | release 构建、核心路径 | 传递关系变化 | 差异未解释 |
| 初始化或组件 | 冷启动、重建、失败恢复 | 自动初始化 | 入口未覆盖 |
| 反射或规则 | 真实动态调用与序列化 | 生成代码变化 | 运行入口未触发 |
| 权限或隐私 | 拒权、数据和声明复核 | 新增网络端点 | 责任未批准 |
| Native 集合 | 目标 ABI 专项门禁 | 运行库或 JNI 变化 | 任一发布 ABI 缺证 |
用快照差异脚本生成可复核的升级门禁清单
下方脚本读取旧新两个规范化依赖快照。每个组件包含 key、version、artifactSha256、direct、dependencyPaths,以及 initializers、permissions、nativeLibraries、consumerRulesSha256、reflectionNamespaces 和 privacyDeclarations。脚本验证快照属于同一平台和变体,再报告组件增删、版本、内容与元数据变化,并生成需要重新执行的 gate。
脚本不会读取 Gradle、Xcode 或二进制本身,快照必须由受控构建步骤生成并与候选关联。它也不判断 SDK 安全性、权限必要性或隐私声明真实性;输出 affectedGates 是评审路由,不是候选失败结论。任何字段缺失、摘要不合法、重复 key 或平台变体错配都会明确失败,避免用不完整输入得到虚假 unchanged。
NIST SP 800-218 SSDF 要求安全发布保留来源、构建、验证和变更证据,并关注供应链风险。升级记录应保存旧新快照摘要、依赖锁、构建来源、配置 diff、新候选、复测回执和批准范围。需要评估实际第三方 SDK 升级时,准备解析快照和变更材料,再通过御盾中央平台提交申请。
| 状态 | 含义 | 允许动作 | 禁止外推 |
|---|---|---|---|
| snapshot-invalid | 输入不完整或身份错配 | 修复快照 | SDK 存在故障 |
| changes-detected | 存在需要评审的差异 | 执行 affectedGates | 候选一定不兼容 |
| needs-inventory | 关键元数据无法确认 | 补齐产物清单 | 字段未变化 |
| retest-defined | 差异均有复测路由 | 生成新候选并执行 | 实际回归已通过 |
| release-review-ready | 新候选证据闭合 | 提交发布批准 | 产品无未知风险 |
- 旧新快照来自同一平台、同一发布 variant,并绑定各自候选 SHA-256
- 直接与传递依赖均保存解析版本、内容摘要、路径和选择依据
- 初始化、权限、Native、反射、消费者规则和隐私声明字段不可缺省
- 每项变化输出 affectedGates、业务路径、owner 和所需证据
- 配置更新后重新生成新候选和快照,不把旧候选回执复制为通过
- 所有未覆盖平台、ABI、设备、私有 SDK 和运行路径明确列为限制
#!/usr/bin/env python3
import hashlib
import json
import re
import sys
from pathlib import Path
SHA256_RE = re.compile(r"^[0-9a-f]{64}$")
LIST_FIELDS = (
"initializers", "permissions", "nativeLibraries",
"reflectionNamespaces", "privacyDeclarations", "dependencyPaths",
)
FIELD_GATES = {
"initializers": {"startup", "lifecycle"},
"permissions": {"permission-data-review", "denial-path"},
"nativeLibraries": {"native-abi-specialist", "device-matrix"},
"reflectionNamespaces": {"reflection-serialization", "release-runtime"},
"privacyDeclarations": {"privacy-review", "data-behavior"},
"dependencyPaths": {"dependency-resolution", "ownership-review"},
"consumerRulesSha256": {"shrinker-rules", "release-runtime"},
}
def fail(message, code=2):
print(message, file=sys.stderr)
raise SystemExit(code)
def sha256_file(path):
digest = hashlib.sha256()
with path.open("rb") as handle:
for block in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def load_json(path, label):
try:
value = json.loads(path.read_text(encoding="utf-8"))
except (OSError, UnicodeError, json.JSONDecodeError) as exc:
fail("cannot read " + label + ": " + str(exc))
if not isinstance(value, dict):
fail(label + " must be a JSON object")
return value
def require_text(record, field, context):
value = record.get(field)
if not isinstance(value, str) or not value.strip():
fail(context + " is missing " + field)
return value.strip()
def normalize_list(record, field, context):
value = record.get(field)
if not isinstance(value, list):
fail(context + " is missing array field " + field)
if any(not isinstance(item, str) or not item.strip() for item in value):
fail(context + " has invalid " + field)
return sorted(set(item.strip() for item in value))
def normalize_component(record):
if not isinstance(record, dict):
fail("every dependency must be an object")
key = require_text(record, "key", "dependency")
version = require_text(record, "version", key)
artifact_sha = require_text(record, "artifactSha256", key).lower()
rules_sha = require_text(record, "consumerRulesSha256", key).lower()
if not SHA256_RE.fullmatch(artifact_sha) or not SHA256_RE.fullmatch(rules_sha):
fail(key + " has an invalid SHA-256 field")
direct = record.get("direct")
if not isinstance(direct, bool):
fail(key + " direct must be boolean")
normalized = {
"key": key,
"version": version,
"artifactSha256": artifact_sha,
"consumerRulesSha256": rules_sha,
"direct": direct,
}
for field in LIST_FIELDS:
normalized[field] = normalize_list(record, field, key)
return normalized
def index_snapshot(snapshot, label):
platform = require_text(snapshot, "platform", label)
variant = require_text(snapshot, "variant", label)
candidate_sha = require_text(snapshot, "candidateSha256", label).lower()
if not SHA256_RE.fullmatch(candidate_sha):
fail(label + " candidateSha256 is invalid")
dependencies = snapshot.get("dependencies")
if not isinstance(dependencies, list):
fail(label + " dependencies must be an array")
index = {}
for item in dependencies:
normalized = normalize_component(item)
key = normalized["key"]
if key in index:
fail(label + " has duplicate dependency key: " + key)
index[key] = normalized
return {"platform": platform, "variant": variant, "candidateSha256": candidate_sha, "dependencies": index}
def compare_component(old, new):
changes = {}
gates = {"release-build", "core-regression"}
for field in ("version", "artifactSha256", "direct", "consumerRulesSha256", *LIST_FIELDS):
if old[field] != new[field]:
changes[field] = {"old": old[field], "new": new[field]}
gates.update(FIELD_GATES.get(field, {"dependency-resolution"}))
if old["version"] == new["version"] and old["artifactSha256"] != new["artifactSha256"]:
gates.add("source-integrity-review")
return changes, sorted(gates)
def compare(old, new):
results = []
old_items = old["dependencies"]
new_items = new["dependencies"]
for key in sorted(old_items.keys() - new_items.keys()):
results.append({"key": key, "kind": "removed", "affectedGates": ["config-cleanup", "dependency-resolution", "release-build", "core-regression"]})
for key in sorted(new_items.keys() - old_items.keys()):
gates = {"dependency-resolution", "release-build", "core-regression", "inventory-review"}
for field in LIST_FIELDS:
if new_items[key][field]:
gates.update(FIELD_GATES[field])
results.append({"key": key, "kind": "added", "affectedGates": sorted(gates)})
for key in sorted(old_items.keys() & new_items.keys()):
changes, gates = compare_component(old_items[key], new_items[key])
if changes:
results.append({"key": key, "kind": "changed", "changes": changes, "affectedGates": gates})
return results
def main():
if len(sys.argv) != 3:
print("Usage: compare_sdk_snapshots.py OLD_SNAPSHOT_JSON NEW_SNAPSHOT_JSON", file=sys.stderr)
raise SystemExit(2)
old_path = Path(sys.argv[1])
new_path = Path(sys.argv[2])
if not old_path.is_file() or not new_path.is_file():
print("both snapshot files must exist", file=sys.stderr)
raise SystemExit(2)
old = index_snapshot(load_json(old_path, "old snapshot"), "old snapshot")
new = index_snapshot(load_json(new_path, "new snapshot"), "new snapshot")
if old["platform"] != new["platform"] or old["variant"] != new["variant"]:
fail("snapshots must use the same platform and release variant", 3)
changes = compare(old, new)
result = {
"status": "changes-detected" if changes else "no-normalized-change",
"platform": old["platform"],
"variant": old["variant"],
"oldSnapshotSha256": sha256_file(old_path),
"newSnapshotSha256": sha256_file(new_path),
"oldCandidateSha256": old["candidateSha256"],
"newCandidateSha256": new["candidateSha256"],
"changes": changes,
"boundary": "Snapshot diff defines review scope; it does not prove SDK safety or candidate compatibility.",
}
print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
if changes:
raise SystemExit(20)
if __name__ == "__main__":
main()事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| 直接与传递依赖形成版本解析图,解析结果变化可能影响运行兼容。 | Android Gradle dependency resolution 说明 Android 构建依赖解析。 | 依赖树只能缩小范围,不能证明某个 SDK 是唯一根因。 |
| 重复类与版本冲突应从变体对应的依赖树和实际解析结果定位。 | Debug Android dependency resolution 说明依赖冲突排查方法。 | 构建冲突修复不等于加固后运行时冲突已消失。 |
| SDK 评审可参考版本采用、权限、可靠性、安全与数据处理指引。 | Google Play SDK Index 说明可用的 SDK 评审信息。 | SDK Index 不覆盖全部私有或开源 SDK,也不替代二进制清单。 |
| 开发者对集成 SDK 的代码与数据行为负责,部分 SDK 有额外签名和隐私要求。 | Apple third-party SDK requirements 说明 Apple 平台第三方 SDK 责任。 | Apple 列表不覆盖 Android 依赖,也不代表 SDK 通过安全评估。 |
| App 与第三方 SDK 的数据和 required reason API 应进入有效 privacy manifest。 | Apple privacy manifests 说明 App 与第三方 SDK 隐私清单。 | manifest 是声明和审核材料,不是运行时数据隔离。 |
| 安全发布应保留来源、构建、验证和变更证据并关注供应链风险。 | NIST SP 800-218 SSDF 给出组织级安全软件开发实践。 | SSDF 不定义御盾或任何具体 App 加固产品的功能。 |
| SDK 升级后应按解析图、初始化、权限、Native、反射、规则和隐私变化重算加固范围。 | 工程判断:这些输入会改变类变换、装载、数据责任和运行回归边界。 | 变化存在不自动证明候选失败,仍需生成新候选并执行对应门禁。 |
| 旧候选的回执不能直接作为依赖变化后新候选的通过证据。 | 工程判断:依赖与配置变化会改变被测试产物的字节身份和适用范围。 | 未受影响证据能否复用需逐项差异分析,不应一律全部作废或全部继承。 |
工程常见问题
第三方 SDK 只升级补丁版本,也必须重新评估吗?
需要先比较实际解析结果和产物摘要。补丁版本仍可能改变传递依赖、规则、权限、Native 库或初始化;若确认无相关变化,再缩小复测范围。
Gradle 构建成功是否说明原加固配置还能使用?
不能。构建成功不覆盖运行时反射、序列化、动态加载、JNI、权限拒绝和生命周期路径,需要按差异触发 release 候选回归。
供应方 changelog 没写加固兼容变化,是否可以跳过复测?
不可以据此跳过。changelog 只覆盖供应方声明,仍要核对实际依赖图、归档内容、合并 Manifest、消费者规则和真实运行路径。
新增一个权限是否一定代表 SDK 在收集新数据?
不一定。权限存在是静态信号,还要确认功能是否启用、运行时是否申请、数据是否采集传输,以及产品声明和允许范围。
SDK 新增 SO 后只测 arm64-v8a 是否足够?
若产品还发布其他 ABI 就不够。应按正式支持范围核对每个 ABI 的打包、装载、JNI、运行库和核心路径,单一架构结果不能外推。
申请 SDK 升级后的加固重评需要准备什么?
准备旧新发布变体解析快照、依赖锁、AAR 或 framework 摘要、合并 Manifest、消费者规则、Native 清单、隐私材料和新候选计划,再通过御盾中央平台提交申请。