先看结论与判断条件

  • 评估输入必须是发布变体实际解析出的直接与传递依赖快照,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、候选与设备回执,只提供影响计算方法,不声称御盾或任何产品已对某次升级完成兼容验证。

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 APIprivacy 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 白名单、保护目标和符号材料应同步清理并生成新候选。

Native 变化到专项门禁的路由
变化必须记录触发检查不能仅凭
新增 SOABI、路径、摘要装载、JNI、符号与运行库归档存在
SO 内容变化旧新 SHA-256ABI 契约和设备回归文件名相同
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 外部资料的使用边界
资料可支持不能支持本地补证
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 升级时,准备解析快照和变更材料,再通过御盾中央平台提交申请。

SDK 升级评估输出状态
状态含义允许动作禁止外推
snapshot-invalid输入不完整或身份错配修复快照SDK 存在故障
changes-detected存在需要评审的差异执行 affectedGates候选一定不兼容
needs-inventory关键元数据无法确认补齐产物清单字段未变化
retest-defined差异均有复测路由生成新候选并执行实际回归已通过
release-review-ready新候选证据闭合提交发布批准产品无未知风险
  • 旧新快照来自同一平台、同一发布 variant,并绑定各自候选 SHA-256
  • 直接与传递依赖均保存解析版本、内容摘要、路径和选择依据
  • 初始化、权限、Native、反射、消费者规则和隐私声明字段不可缺省
  • 每项变化输出 affectedGates、业务路径、owner 和所需证据
  • 配置更新后重新生成新候选和快照,不把旧候选回执复制为通过
  • 所有未覆盖平台、ABI、设备、私有 SDK 和运行路径明确列为限制
比较第三方 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 清单、隐私材料和新候选计划,再通过御盾中央平台提交申请。

想用自己的 App 验证?

提交候选包、目标系统和关键业务路径,申请御盾 PoC 与兼容性评估。

继续阅读: 御盾 VMP 接入前准备清单