上架前检查第三方依赖时,最容易产生误判的不是“有没有依赖”,而是把依赖树等同于权限事实。ohpm list适合回答安装了哪些包、父子关系如何;私仓管理员导出的packagePermission_xxx.json则描述仓库当前记录的包权限数据。两份信息用途不同。前者能解释依赖从哪里来,后者更适合做仓库侧权限基线。若只保存一张终端截图,下一次版本变化时很难证明哪一项新增、谁确认、依据是什么。
本文围绕一个上架准备工具PermissionEvidenceGate展开。它不自动裁决“合规”或“可上架”,只把私仓权限导出文件转成可审核的差异证据。演示任务为PPG-1231-287,仓库为release,基线文件baseline_20261001.json,本次导出packagePermission_1791241882000.json。扫描 46 个包后得到 4 个变化项:新增权限叶子 2、移除 1、仍需归属人解释 1;25 项证据检查完成 17 项,即 68%,状态为WAITING_OWNER_REVIEW。
一、先承认工具不能替审核结论
华为当前上架资料强调,应用提交前需要完成漏洞、隐私、兼容性、稳定性和性能等测试;只要集成第三方 SDK,还应在隐私政策中逐一明示 SDK 收集个人信息的目的、方式和范围。这个要求不能由一个 JSON 差异脚本代替。包权限变化只是一条线索,最终还要结合实际调用、隐私文本、用户授权时机、测试账号与应用可用性做人工判断。
因此,PermissionEvidenceGate的输出名称是“证据包”,不是“审核通过报告”。每一个变化项只能进入三种应用层状态:KNOWN表示已有明确归属和说明;WAIVED表示经授权接受且记录理由;NEEDS_REVIEW表示无法自动解释。工具不会把“文件没有变化”翻译成“隐私合规”,也不会把“新增字符串”直接命名成敏感权限。
这里还有一个版本边界。华为文档说明从ohpm-repo 5.4.0起支持export_pkgPermission,命令会在当前工作目录生成packagePermission_xxx.json。--repos可指定一个或多个仓库,不传则导出全部仓库。演示固定执行ohpm-repo export_pkgPermission --repos release,避免把测试仓和生产候选混在同一证据里。
二、导出文件必须按“不透明输入”处理
公开文档给出了命令、选项和输出文件名,但没有承诺本文可以依赖的稳定 JSON 字段模式。最危险的写法,是看到一次样例就把字段名硬编码为业务事实。仓库升级后字段顺序、层级或元数据可能变化,脚本会在没有报错的情况下漏项。
更稳妥的策略是把 JSON 视为不透明树:解析语法,删除明确列入忽略清单的时间类元数据,然后把所有叶子值转换成稳定的 JSON Pointer 路径。差异结果描述“路径和值发生变化”,不擅自把路径解释成权限语义。只有人工映射表确认后,报告才附加业务标签。
第一段 TypeScript 代码解决规范化问题。对象键排序、数组保留顺序,叶子以类型和值共同编码。这样既不会受普通对象键顺序影响,也不会把字符串true与布尔值true混为一谈。示例在 Node.js 工具侧运行,不是 ArkUI 页面 API。
typeJson=null|boolean|number|string|Json[]|{[key:string]:Json};interfaceLeaf{pointer:string;encoded:string}functionescapePointer(token:string):string{returntoken.replace(/~/g,'~0').replace(/\//g,'~1');}functionflatten(value:Json,pointer:string=''):Leaf[]{if(Array.isArray(value)){returnvalue.flatMap((item,index)=>flatten(item,`${pointer}/${index}`));}if(value!==null&&typeofvalue==='object'){returnObject.keys(value).sort().flatMap(key=>flatten(value[key],`${pointer}/${escapePointer(key)}`));}return[{pointer:pointer||'/',encoded:`${typeofvalue}:${String(value)}`}];}functionstableLeaves(input:Json,ignored:Set<string>):Leaf[]{returnflatten(input).filter(item=>!ignored.has(item.pointer)).sort((a,b)=>a.pointer.localeCompare(b.pointer));}这段代码也故意不对数组排序。若数组次序在仓库数据中有语义,排序会制造假等价;若没有语义,次序变化会产生噪声,但噪声应由明确的路径级规则消除,而不是全局猜测。忽略清单必须进入版本控制,新增忽略规则也要被评审,否则“降噪”很容易变成“消除证据”。
三、差异门禁要能回答“新增、移除、改变”
第二段代码把规范化叶子转成差异。新增与移除分别记录;同一路径值改变时,报告包含前后编码。它不读取开发者电脑上的任意目录,只接收两个已确定的文件路径;输出也固定落在任务目录。演示生成证据 IDperm_evidence_287,SHA-256 前缀b72e91c4用于屏幕核对,完整哈希保存在 JSON 报告。
importfsfrom'node:fs/promises';importcryptofrom'node:crypto';interfaceDiffItem{kind:'ADDED'|'REMOVED'|'CHANGED';pointer:string;before?:string;after?:string;}asyncfunctioncompareFiles(beforePath:string,afterPath:string):Promise<DiffItem[]>{const[beforeRaw,afterRaw]=awaitPromise.all([fs.readFile(beforePath,'utf8'),fs.readFile(afterPath,'utf8')]);constignored=newSet<string>(['/generatedAt','/exportTime']);constbefore=newMap(stableLeaves(JSON.parse(beforeRaw)asJson,ignored).map(item=>[item.pointer,item.encoded]));constafter=newMap(stableLeaves(JSON.parse(afterRaw)asJson,ignored).map(item=>[item.pointer,item.encoded]));constpointers=[...newSet([...before.keys(),...after.keys()])].sort();returnpointers.flatMap(pointer=>{constoldValue=before.get(pointer);constnewValue=after.get(pointer);if(oldValue===undefined)return[{kind:'ADDED',pointer,after:newValue}];if(newValue===undefined)return[{kind:'REMOVED',pointer,before:oldValue}];if(oldValue!==newValue){return[{kind:'CHANGED',pointer,before:oldValue,after:newValue}];}return[];});}functionsha256(bytes:string):string{returncrypto.createHash('sha256').update(bytes).digest('hex');}JSON.parse()失败必须让门禁失败,不能回退为空对象。文件不存在、读权限不足、编码异常也一样。证据链最怕“工具报错,但流水线仍生成绿色徽标”。实际工程会把这些错误映射成INPUT_INVALID,与DIFF_FOUND、WAITING_OWNER_REVIEW分开。
另一个取舍是保留原始导出文件。差异 JSON 方便阅读,却不足以重新计算。证据目录同时保存原始基线、本次原始导出、规范化摘要、差异列表、人工说明和哈希清单。原始文件可以按企业策略加密保存,但不能只留一张截图。
配套开发图展示了tools/permission-gate工程:左侧有flatten.ts、diff.ts、evidence.ts,中间是 JSON Pointer 差异逻辑,右侧 HarmonyOS 模拟器展示审核摘要,底部日志包含PPG-1231-287、46 包、变化 4、检查 17/25 和状态WAITING_OWNER_REVIEW。这是一张白色主题演示图,不冒充真实 DevEco Studio 执行证据。
四、人工说明不是备注,而是门禁输入
第三段代码给每个差异绑定归属人和结论。KNOWN需要责任人、依据链接和说明;WAIVED还需要到期时间;NEEDS_REVIEW不能进入“可封存”状态。映射键使用差异路径加前后值摘要,避免同一路径未来再次变化时误用旧说明。
interfaceDecision{diffKey:string;status:'KNOWN'|'WAIVED'|'NEEDS_REVIEW';owner:string;rationale:string;evidenceUrl?:string;expiresAt?:string;}functioncanSeal(diffs:DiffItem[],decisions:Decision[]):boolean{constbyKey=newMap(decisions.map(item=>[item.diffKey,item]));returndiffs.every(diff=>{constkey=sha256(JSON.stringify(diff));constdecision=byKey.get(key);if(!decision||decision.status==='NEEDS_REVIEW')returnfalse;if(!decision.owner.trim()||!decision.rationale.trim())returnfalse;if(decision.status==='WAIVED'&&!decision.expiresAt)returnfalse;returntrue;});}工具不应把人员姓名强制写进公开报告。内部版本可以记录账号标识,外发证据只保留角色,例如“媒体能力负责人”。链接也要指向可长期访问的规范或评审单,不要使用即时聊天中的临时消息。若依据是第三方 SDK 隐私声明,还要记录访问日期,因为声明可能更新。
演示手机首页强调“尚未完成”,而不是用绿色大勾制造通过错觉。12:31 时,25 项证据检查完成 17 项,进度 68%;46 个包中发现 4 个变化,新增 2、移除 1、待解释 1。状态栏含 Wi‑Fi、5G、信号和 83% 电量,页面无手机边框。
五、把差异接到上架准备,而不是接到自动放行
一份合格的证据包至少回答五件事:导出命令是什么,针对哪个仓库,输入文件哈希是多少,差异算法版本是什么,未决项由谁处理。PermissionEvidenceGate将这些字段写入manifest.json,再生成只读报告。构建流水线可以规定未决项大于 0 时阻止“提交候选”阶段,但不能写成“审核不通过”,因为真正审核发生在平台侧。
依赖树仍然有价值。发现变化后,使用ohpm list查询相关包的父依赖,能判断它是直接依赖还是传递依赖;但这一步是归因,不是用依赖树替代权限导出。本批刻意不再讨论上一轮已经覆盖的“依赖拓扑快照与版本漂移门禁”,而只把必要的父链作为证据附件。
与隐私政策的衔接也必须人工完成。官方上架说明要求集成第三方 SDK 时明示其收集个人信息的目的、方式和范围。工具可以检查“每个已知 SDK 是否有隐私条目”,但不能仅凭包名推断其实际收集行为。静态依赖存在不代表功能一定调用;反过来,运行时动态模块、系统能力或远端配置也可能改变行为。最终检查需要结合代码、运行测试、SDK 声明和产品功能。
详情页展示的不是首页数字重复,而是四个变化项的处置状态:两项已有归属,一项确认移除,一项仍为NEEDS_REVIEW。下方列出baseline_20261001.json、packagePermission_1791241882000.json、证据 ID 与哈希前缀。红色细圈落在未决项,箭头指向“禁止封存”,使门禁原因一眼可见。
六、失败路径比成功截图更重要
导出文件为空时,不应与“零权限”混淆。工具先检查文件存在、字节数大于零、JSON 可解析,再做规范化。若命令执行失败,要保留退出码与标准错误,但日志中不打印仓库令牌、完整内部地址或用户名。
基线不存在时,工具只能生成BASELINE_REQUIRED,不能把第一次导出自动认定为安全基线。基线的建立本身需要一次评审:确认仓库范围、工具版本、忽略规则以及与目标应用版本的对应关系。之后任何基线更新都应包含旧基线 ID,形成连续链条。
文件哈希不一致时,报告应失效。最常见的情况是开发者打开 JSON 后手工格式化,肉眼看内容相同,但字节哈希已经变化。可以同时保存“原始文件哈希”和“规范化叶子哈希”:前者证明文件未改,后者用于解释语义比较。两种哈希用途不同,不能只留更方便的那个。
人工说明过期同样要失败。WAIVED不是永久放行,到了expiresAt必须回到NEEDS_REVIEW。责任人离开项目时,也要通过角色映射重新分配。证据系统若只会累积“已确认”,最终会把过去的例外变成无人负责的常态。
最后,私仓导出与应用上架之间不是一对一关系。一个仓库可能服务多个应用,一个应用也可能混用多个仓库或本地 HSP。演示中的release只是单仓范围。实际项目必须把应用构建清单与导出范围对齐,并记录哪些依赖来自私仓、哪些来自公共源、哪些被打进目标产物。否则 46 个包的统计数字没有审计意义。
七、这套工具的边界与可复核结论
本文能够确认的是:ohpm-repo 5.4.0起提供包权限数据导出命令,--repos可限定仓库;HarmonyOS 应用上架前需要进行多维测试,集成第三方 SDK 时需要在隐私政策中逐一明示相关信息。本文的 JSON Pointer 规范化、差异状态、证据 ID、进度与包数量均为应用侧演示设计,不是华为平台字段或审核结论。
对团队而言,工具真正减少的不是审核工作,而是记忆成本。每次候选版本都能回答:相对上次基线改了什么,原始输入是否保留,哪些变化已有解释,哪些仍然阻断。这样在依赖升级、SDK 声明变化或产品权限调整时,不必从聊天记录和截图中拼接历史。
PPG-1231-287在 68% 时停住是有意的。还有一个未决项,就不应该显示“完成”。工程工具最难得的品质不是自动化程度,而是在证据不足时愿意保持红色。等责任人补齐依据,系统重新计算哈希、把 25/25 写入新报告,再执行封存;旧的 17/25 仍保留,成为这次判断过程的一部分。
八、参考资料
- 华为开发者文档:
ohpm-repo export_pkgPermission,核对时间为 2026-10-06;用于确认版本起点、命令格式、输出文件与--repos行为。 - 华为开发者文档:HarmonyOS 应用上架申请与提交流程,核对时间为 2026-10-06;用于确认上架前测试维度与第三方 SDK 隐私明示要求。
- 华为开发者文档:
ohpm list,用于界定依赖父链查询与包权限导出的职责差异。
当权限差异、人工说明、原始导出和哈希被封存在同一批证据里,上架准备才从“我记得这次没问题”变成“任何人都能复算这次判断”。这不是把审核交给脚本,而是让脚本把需要人判断的地方准确地留下来。