先看结论与判断条件
- 第三方 SDK 清单要同时回答构建声明、解析结果和最终产物三个问题,任意单一视角都会遗漏依赖或产生误认。
- 交付物若来自 AAB,应针对真实设备配置检查派生 APK 集,不能只扫描未安装的 AAB 容器或一个随意生成的 universal APK。
- DEX 包名前缀、类字符串、资源、Manifest 组件和 Native 库都是二进制特征,只能与依赖图和可信资料交叉后提升置信度。
- 版本号必须来自解析锁定、组件元数据或可复核映射,不能根据文件名、类数量或搜索结果猜测精确版本。
- 最终清单应保留已确认、候选、未知和已排除四种状态,并记录每条结论的证据、冲突与适用变体。
- 发布门禁比较新增、删除、升级、权限组件变化和未知二进制条目,差异需要所有者与处置结论,不能自动修改基线。
先把 SDK 盘点拆成三个不同事实层
“项目声明了哪些依赖”“Gradle 为目标变体解析了哪些组件”“最终 APK 实际携带哪些代码与资源”是三张不同清单。开发脚本里的 implementation 只能看到直接声明,解析图还会包含传递依赖、平台约束和版本替换,最终 APK 又可能经过 shrink、资源裁剪、动态模块拆分或打包过滤。反查工作必须分别保存这三个事实层,再解释它们之间的差异,不能把其中一张表改名为完整 SDK 清单。
SDK 也不是一个可以仅靠包名前缀精确定义的对象。一个商业 SDK 可能由多个 Maven 组件、DEX 命名空间、资源前缀和 Native 库组成;多个无关项目也可能复用常见包名或开源基础组件。盘点记录因此要区分“组件坐标”“产品或供应商身份”“二进制特征”和“业务用途”。只有组件坐标与最终特征存在可复核关联时,才能写成已确认;其余情况应保留候选或未知状态。
本文只回答如何从最终 APK 反查第三方 SDK 并与构建依赖清单核对,不把主题扩成 AAB 发布身份验收,也不根据特征扫描宣称发现漏洞或数据行为。清单的价值是建立供应链可见性、解释发布差异并为权限和数据审查提供入口。没有源码、锁定依赖、供应商资料或运行证据时,二进制名称只能作为线索,不能证明某个厂商、版本或功能一定存在。
| 事实层 | 主要输入 | 能确认的内容 | 主要盲区 |
|---|---|---|---|
| 构建声明 | 版本目录和 Gradle 配置 | 直接依赖与配置意图 | 传递解析和最终裁剪 |
| 解析结果 | 目标 variant 依赖图和锁定文件 | 实际选中坐标与版本 | 是否进入最终二进制 |
| DEX 特征 | 类、包前缀和字符串索引 | 最终包包含的代码线索 | 混淆、合并与来源歧义 |
| Native 特征 | lib 目录、SONAME 和架构 | 随包交付的本地库线索 | 静态链接和通用文件名 |
| 清单与资源 | 权限、组件、provider 和资源名 | 集成产生的配置线索 | 共享声明与条件行为 |
| 公开资料 | SDK Index 与供应商文档 | 版本政策和评审参考 | 私有 SDK 与真实交付差异 |
检查对象必须对应真实设备交付物
直接发布单 APK 时,盘点对象应是准备交付且已固定摘要的最终候选。采用 Android App Bundle 时,AAB 由 base、feature、配置和资产模块组成,设备安装的是按配置派生的 APK 集。只扫描 AAB 压缩包可以了解模块输入,却不能代表某台设备最终得到的代码、语言、密度和 ABI 组合。验收要先定义目标设备规格和分发渠道,再保存对应 APK set 与每个 split 的摘要。
bundletool 可以从 AAB 生成、检查和部署 APK set,也可以针对设备规格选择 APK。使用它建立复核样本时,要显式保存工具版本、命令参数、device spec、签名输入和输出摘要。官方资料提醒,未显式提供签名信息时某些流程可能使用调试签名,因此临时生成包不能自动冒充商店发布候选。SDK 盘点可以使用结构样本,但身份结论必须注明样本来源和与真实交付物的关系。
模块化交付还会造成“某 SDK 在包里”这个句子缺少边界。代码可能位于 base,也可能只在按需 feature 或特定 ABI split 中出现;某个设备未取得该模块,不代表整个产品没有该 SDK。清单应以模块和设备配置为维度记录存在性,并明确汇总规则。若业务要回答全产品供应链,应取所有可交付模块并集;若要回答某设备暴露面,则只取该设备真实安装集合。
- 最终单 APK 或 APK set 均绑定不可变摘要
- AAB、派生 APK 和真实商店交付身份分别标注
- bundletool 版本、参数与 device spec 随回执保存
- base、feature、配置与 ABI split 分别盘点
- 产品并集和单设备安装集使用不同结论字段
- 临时或调试签名样本不会被登记为发布候选
从目标变体解析图取得版本与来源主线
Gradle 依赖解析会把直接依赖、传递依赖、版本约束、平台和替换规则收敛成目标 configuration 的具体组件图。SDK 反查应从实际发布 variant 的 runtime classpath 导出机器可读结果,至少包含 group、module、selected version、请求来源、选择原因和依赖路径。不能用默认 debug 的 dependency report 代表 release,也不能只截取顶层依赖,因为常见 SDK 会通过多个传递组件提供网络、存储或本地能力。
版本信息应优先来自这张解析图或对应锁定证据。DEX 中的 BuildConfig、资源字符串、META-INF 文件或库名可以辅助核对,但它们可能被裁剪、覆盖或保留旧值,不宜单独写成精确版本结论。若二进制特征与解析版本冲突,记录应标为冲突并回到构建缓存、依赖替换和实际候选来源排查。把看似合理的文件名解析成版本,会让清单产生难以发现的假精度。
Android 的依赖解析错误指南建议从目标变体的依赖树和实际选择结果定位重复类与版本问题。该方法能说明构建系统为何选择某个组件,却不能证明组件代码一定完整进入最终 APK;R8 可能裁剪未使用类,资源打包也有独立规则。反查流程需要把解析组件映射到预期二进制特征,然后在最终产物中记录已观察、未观察或无法区分,避免把“已解析”直接翻译为“已交付”。
| 解析状态 | 二进制状态 | 清单结论 | 后续动作 |
|---|---|---|---|
| 存在 | 观察到唯一特征 | 已确认组件 | 记录证据和版本 |
| 存在 | 只观察到共享特征 | 候选组件 | 补充供应商或映射证据 |
| 存在 | 未观察到特征 | 可能被裁剪或仅构建期使用 | 检查打包规则和用途 |
| 不存在 | 观察到明确特征 | 未知或预构建引入 | 追查 AAR、JAR 与本地文件 |
| 版本冲突 | 元数据指向另一版本 | 证据冲突 | 核对缓存、替换和候选摘要 |
| 无源码图 | 存在二进制特征 | 二进制候选 | 不得猜测精确坐标版本 |
DEX、Manifest 和资源只能提供可组合线索
DEX 盘点可以统计类描述符、包名前缀、特定接口实现、注解和稳定字符串,但每种信号都有歧义。混淆会改写类名,shade 或 relocate 会移动命名空间,多个组件会共享 Kotlin、protobuf 或网络基础库,字符串还可能来自未执行代码。扫描器应保存命中特征、所在 dex、样本数量和冲突候选,不应直接用一个前缀映射一个供应商。规则库还要版本化,方便解释同一候选为何在两次扫描中得到不同结果。
Manifest 权限、Activity、Service、Receiver、Provider、meta-data 和 intent filter 可以补充集成线索。例如某 SDK 文档要求特定组件和元数据,最终清单同时出现这些组合时,候选置信度可以提高。但权限通常被多个功能共享,组件也可能由宿主自建,不能用单个 INTERNET 权限或通用 FileProvider 推断厂商。每条规则应包含必须特征、可选特征、排除条件和资料来源,并把最终字段绑定到 APK 而非源码模板。
资源表、assets、META-INF 和服务注册文件可以揭示许可证、初始化配置或打包标识,但同样需要谨慎。许可证文本可能覆盖多个组件,资源名前缀可能被宿主复制,空配置文件也不能证明功能启用。清单应区分“二进制存在”“配置存在”“运行初始化”和“业务调用”四种状态。最终 APK 扫描最多直接支持前两项,后两项需要日志、调用边界或设备测试,不能从静态文件推演。
- DEX 规则记录命中特征、dex 位置和冲突候选
- 混淆、shade、relocate 和共享基础库进入限制说明
- Manifest 采用组件组合而非单个通用权限判断
- 资源、assets 与 META-INF 只作为辅助证据
- 二进制存在、配置、初始化和业务调用分别登记
- 特征规则库版本与扫描器版本绑定到报告
Native 条目要按 ABI、SONAME 和来源分别记录
APK 的 lib 目录能够列出各 ABI 随包携带的共享对象,文件名、ELF SONAME、构建标识、导出符号和依赖项可以形成 Native SDK 线索。盘点时要先按 split 和 ABI 建立矩阵,再对同名库计算摘要,防止把四个 ABI 副本误计为四个 SDK。一个 SDK 也可能携带多个 so,或者把第三方静态库链接进宿主 so,因此“一个文件等于一个供应商”的映射通常不成立。
通用文件名尤其容易误认。libcrypto、libc++_shared 或以业务缩写命名的库可能来自不同构建链,SONAME 也不一定携带版本。没有组件坐标、供应商发布摘要、许可证或可复核符号映射时,应标记为未知 Native 条目,而不是根据互联网搜索结果填入厂商和版本。若依赖图声明了带 prefab 或 jniLibs 的组件,可以把组件路径与最终文件摘要关联,提高来源置信度。
Native 差异要回答新增、删除、ABI 缺失、摘要变化和依赖变化。摘要变化不自动表示 SDK 升级,也可能来自编译器、链接顺序或重新签名以外的构建差异;文件未变化也不能证明上层 Java 包装和配置未变。报告应把 Native 文件事实与 SDK 产品判断分开,并将无法归因的新增 so 作为发布阻断项,直到所有者、用途、许可证和目标 ABI 得到解释。
| 证据组合 | 可写结论 | 禁止推断 | 补证方向 |
|---|---|---|---|
| 组件坐标加文件摘要 | 指定组件交付该文件 | 运行时一定加载 | 设备加载日志 |
| 供应商摘要加 SONAME | 与指定发布物一致 | 宿主配置正确 | 集成配置和功能测试 |
| 仅通用文件名 | 存在未知 Native 条目 | 厂商和精确版本 | 许可证、符号和构建来源 |
| 仅导出符号前缀 | 形成候选来源 | 唯一归属 | 组件映射与供应商资料 |
| 静态链接入宿主 so | 观察到代码特征线索 | 独立 SDK 文件存在 | 链接 map 与 SBOM |
| 某 ABI 缺失 | 该交付集未携带文件 | 目标设备必然失败 | 设备配置与兼容测试 |
用置信度和冲突字段代替武断识别
一份可审核的 SDK 清单不应只有名称和版本两列。建议为每条记录保存规范化组件坐标、产品名称、供应商、用途、目标 variant、模块、DEX 特征、Native 文件、Manifest 特征、公开资料、识别状态、置信度理由、证据冲突和所有者。置信度不是随意打分,而是由明确规则产生,例如解析坐标与唯一二进制特征同时命中可列为已确认,只有共享包前缀则保留候选。
Google Play SDK Index 可以提供部分商业 SDK 的版本采用、权限、可靠性、安全和数据处理指引,适合作为评审入口,却不覆盖所有私有、开源或内部 SDK。Index 中存在某产品不能证明当前 APK 携带它,Index 没有记录也不能证明二进制不是 SDK。正确做法是先从项目和产物建立清单,再用公开资料补充维护状态与审查问题,不能反向把公开目录当作扫描结果。
状态流转也要可追溯。候选升级为已确认时,应记录新增了哪条来源证据;已确认降级为冲突时,应保留此前判断和触发差异;未知条目被排除时,应说明它属于宿主代码、编译器运行库还是重复特征。不要删除不符合预期的扫描项,否则下一次发布无法判断它是消失、改名还是被规则忽略。规则调整需要与候选重新扫描分开登记。
- 每条 SDK 记录包含 variant、模块、状态和所有者
- 置信度由证据组合规则产生而非主观分数
- 公开 SDK 目录只用于补充评审资料
- 未知和冲突条目不会从报告中静默删除
- 状态流转记录触发证据与审核人
- 规则库变更和 APK 内容变更分别比较
用差异脚本合并解析图、DEX 和 Native 证据
下面的 Python 示例读取三份脱敏 JSON:目标 variant 的 Gradle 解析组件、从最终 APK 提取的 DEX 包前缀与 Native 文件、以及受控的组件特征映射。脚本只在坐标存在且至少一个预期二进制特征命中时登记已观察组件,同时输出未映射 DEX 前缀和 Native 文件。它不会联网搜索厂商,不猜测版本,也不会把未知条目自动追加到允许基线。
映射文件必须由依赖所有者根据组件发布物、许可证和可复核样本维护,不能由模型或搜索摘要自动生成。输入提取步骤也要绑定最终 APK 摘要和 variant,避免依赖图来自 release 而特征来自另一个 debug 包。示例设置了直接输入校验、空坐标校验、variant 不一致和无二进制特征等可达失败路径;失败用于提示证据不完整,不代表 SDK 本身存在安全缺陷。
真实流水线可继续加入 Manifest 组件、资源、模块和 ABI 矩阵,但输出仍应保留证据来源。差异比较以稳定坐标、文件摘要和规则版本为键,分别列出新增、删除、版本变化、特征变化和未知条目。自动化负责把变化呈现给责任人,不负责替责任人确认厂商、许可证、数据用途或升级兼容性。
from pathlib import Path
import json
import sys
if len(sys.argv) != 4:
raise SystemExit("usage: sdk_inventory.py resolved.json binary.json mapping.json")
def load(path_text):
path = Path(path_text).resolve()
if not path.is_file():
raise SystemExit(f"input missing: {path.name}")
return json.loads(path.read_text(encoding="utf-8"))
def coordinate(item):
group = str(item.get("group") or "").strip()
module = str(item.get("module") or "").strip()
version = str(item.get("version") or "").strip()
if not group or not module or not version:
raise SystemExit("resolved component coordinate is incomplete")
return f"{group}:{module}:{version}"
resolved_doc = load(sys.argv[1])
binary_doc = load(sys.argv[2])
mapping_doc = load(sys.argv[3])
if resolved_doc.get("variant") != binary_doc.get("variant"):
raise SystemExit("resolved graph and binary variant differ")
resolved = {}
for item in resolved_doc.get("components", []):
key = coordinate(item)
if key in resolved:
raise SystemExit(f"duplicate resolved component: {key}")
resolved[key] = item
dex_prefixes = {str(value).strip() for value in binary_doc.get("dex_prefixes", [])}
native_files = {str(value).strip() for value in binary_doc.get("native_files", [])}
if not dex_prefixes and not native_files:
raise SystemExit("binary feature inventory is empty")
confirmed = []
used_dex = set()
used_native = set()
for key, component in sorted(resolved.items()):
rule = mapping_doc.get("components", {}).get(key, {})
expected_dex = set(rule.get("dex_prefixes", []))
expected_native = set(rule.get("native_files", []))
hit_dex = sorted(expected_dex & dex_prefixes)
hit_native = sorted(expected_native & native_files)
if not hit_dex and not hit_native:
continue
used_dex.update(hit_dex)
used_native.update(hit_native)
confirmed.append({
"coordinate": key,
"selected_by": component.get("selection_reason"),
"dex_evidence": hit_dex,
"native_evidence": hit_native,
"status": "observed",
})
report = {
"variant": resolved_doc.get("variant"),
"apk_sha256": binary_doc.get("apk_sha256"),
"observed_components": confirmed,
"unmapped_dex_prefixes": sorted(dex_prefixes - used_dex),
"unmapped_native_files": sorted(native_files - used_native),
}
print(json.dumps(report, ensure_ascii=False, indent=2))把清单差异变成可复核的发布决定
安全软件开发框架强调将来源、构建、验证、变更和供应链风险纳入持续开发流程。落实到 APK SDK 清单,发布记录应绑定候选摘要、variant、依赖解析图、锁定文件、模块和 split 清单、DEX 与 Native 特征、规则库版本、公开资料以及人工结论。组织级框架不会替项目决定哪个 SDK 可用,但它要求决定有来源、有责任人并能在变更后重新验证。
发布差异至少分为新增组件、删除组件、版本变化、解析存在但二进制未观察、二进制存在但解析无来源、权限组件变化和 Native ABI 变化。新增不等于危险,删除也不等于安全;每项都需要业务用途、维护状态、许可证、数据处理、兼容性和回滚影响的项目判断。未知二进制条目应阻断到来源明确,不能用“扫描器不认识”或“构建能通过”代替处置。
门禁通过只能写成所列候选在指定规则和样本范围内完成清单复核,不能宣称识别了所有 SDK、验证了全部数据行为或消除了供应链风险。后续设备测试还要确认初始化、网络与功能路径,隐私审查还要核对实际数据流。需要评估具体 APK 时,可通过御盾中央平台提交脱敏候选摘要、目标 variant、依赖锁定和当前 SDK 台账,先固定证据范围,再处理未知与冲突项。
- 候选摘要、variant、模块与扫描规则版本完整绑定
- 新增、删除、升级、未知和冲突状态分别处置
- 许可证、维护状态、数据用途和兼容性有责任人
- 解析图与二进制差异不会被构建成功掩盖
- 静态清单不替代设备初始化和真实数据流验证
- 相邻的 AAB 交付身份问题参考本站对应技术说明
事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| 直接依赖和传递依赖会形成目标配置的具体版本解析图。 | Android Gradle dependency resolution 说明依赖声明、版本选择与运行时 classpath 的解析关系。 | 解析图只能缩小 SDK 来源范围,不能证明组件完整进入最终 APK。 |
| 重复类和版本冲突应从目标变体的依赖树与实际选择结果定位。 | Debug Android dependency resolution 说明依赖解析错误的诊断方法。 | 构建冲突修复不等于最终二进制清单和运行时兼容性已经验证。 |
| SDK 评审可以参考版本采用、权限、可靠性、安全和数据处理指引。 | Google Play SDK Index 提供部分商业 SDK 的公开评审信息。 | 该目录不覆盖所有私有或开源 SDK,也不能替代最终 APK 盘点。 |
| AAB 由 base、feature、配置与资产模块组成,设备安装的是派生 APK 集。 | Android App Bundle format 说明 App Bundle 的模块结构和交付关系。 | AAB 本身不是设备直接安装的最终 APK,单一扫描结果不能代表所有设备配置。 |
| bundletool 可以从 AAB 生成、检查和部署按设备配置选择的 APK set。 | bundletool 说明 APK set 生成、设备规格选择与部署流程。 | 未显式提供发布签名时生成样本不能作为真实发布候选身份。 |
| 安全发布应保留来源、构建、验证和变更证据,并把供应链风险纳入开发流程。 | NIST SP 800-218 SSDF 给出组织级安全软件开发与供应链实践。 | SSDF 不定义某个 APK 扫描器或软件加固产品的具体能力。 |
| DEX 包名、Manifest 组件、资源和 Native 文件应被视为可组合线索,而非单独身份结论。 | 工程判断:多源特征与解析坐标交叉可以降低混淆、共享命名和静态链接导致的误认。 | 静态特征不能证明 SDK 已初始化、执行某项功能或处理特定数据。 |
| SDK 清单应保存已确认、候选、未知和冲突状态及其证据变化。 | 工程判断:保留状态与来源能让发布差异得到复核,避免规则变化被误写成二进制变化。 | 状态模型不能替代许可证、隐私、维护和兼容性的项目责任判断。 |
工程常见问题
只看 Gradle dependencies 能得到最终 APK 的 SDK 清单吗?
不能。解析图包含实际选择的组件,但裁剪、模块拆分、预构建文件和本地 Native 库会让最终产物与解析图出现差异。
看到某个 DEX 包名前缀能确认 SDK 厂商和版本吗?
通常不能。混淆、命名空间迁移、共享基础库和宿主复制都会造成歧义,需要组件坐标、发布物或供应商证据交叉确认。
采用 AAB 时应该扫描 AAB 还是 APK?
两者回答不同问题。AAB 用于了解模块输入,真实设备暴露面应检查与设备配置和分发渠道对应的派生 APK 集。
Google Play SDK Index 没有记录的条目可以忽略吗?
不能。该目录不覆盖私有、内部和全部开源 SDK,清单必须先由项目解析图和最终二进制建立。
同名 so 文件能直接映射到一个 Native SDK 吗?
不能。通用文件名、静态链接和多组件组合都会产生歧义,应结合摘要、SONAME、组件坐标、许可证和供应商发布证据。
准备 SDK 二进制盘点需要哪些材料?
准备最终 APK 或 APK set、摘要、目标 variant、依赖解析与锁定文件、模块和设备规格、现有 SDK 台账,以及组件与二进制特征映射。