
一、审核意见只有一句工程里却有四份权限事实PermissionLedger是我给提审流水线补的一道本地门禁。起因很普通语音拍摄页早已删除“读取照片地点”的功能module.json5里却还留着ohos.permission.MEDIA_LOCATION。测试机不会因此崩溃构建也能通过可提交审核后隐私说明、配置声明和实际功能对不上。人工搜索没有解决问题。权限至少散在四处各模块module.json5的requestPermissions、字符串资源里的 reason、usedScene的 Ability 与时机、ArkTS 中requestPermissionsFromUser()的实际请求。三方 HAR 还可能间接带入调用。只看声明会漏请求只看代码会漏冗余声明。本轮 Demo 把任务定为REVIEW-1346。初次扫描发现MEDIA_LOCATION是孤儿声明状态BLOCKED删除旧声明并同步隐私清单后敏感声明 2 项、运行时请求 2 项、缺失 0、孤儿 0、reason 2/2、usedScene 2/2最终PASSED。二、门禁比较的是集合不是配置文件格式官方权限模型要求应用在配置文件中逐项声明所需权限。user_grant权限需要 reason 与 usedScene运行时再由requestPermissionsFromUser()申请。工程门禁先把不同来源规整为三个集合declaredUserGrant、runtimeRequested、unresolvedCalls。missing runtimeRequested - declaredUserGrant表示代码请求了却没声明orphan declaredUserGrant - runtimeRequested - allowlist表示声明存在但工程没有可证明的请求点。system_grant 权限如 INTERNET 不进入运行时差分因为它们不需要同样的用户授权流程。保留名单也必须写理由和责任人不能用一个巨大 allowlist 把红灯变绿。三、先把 module.json5 变成可审计记录第一段 TypeScript 代码解决声明侧归一化。json5负责注释和尾逗号脚本遍历每个 HAP 模块不只读 entry。对 user_grant 权限reason 或 usedScene 缺失会单独报错不能和“权限未使用”混成一个问题。importJSON5fromjson5;importfsfromnode:fs;typeDeclaredPermission{name:string;module:string;reason?:string;abilities:string[];when?:string;};exportfunctionreadDeclared(path:string):DeclaredPermission[]{constdocJSON5.parse(fs.readFileSync(path,utf8));constmoduleName:stringdoc.module?.name??unknown;return(doc.module?.requestPermissions??[]).map((item:any)({name:item.name,module:moduleName,reason:item.reason,abilities:item.usedScene?.abilities??[],when:item.usedScene?.when}));}这里没有通过权限名称猜 user_grant 类型。项目内维护一份随 SDK 更新的权限元数据快照并记录 API 版本若快照里找不到权限状态应为 UNKNOWN 并阻断而不是当作普通权限放行。reason 只检查资源引用存在还不够后续还要解析对应语言资源避免$string:camera_reason指向空字符串。配置解析完成后也不能马上删“孤儿”。动态加载的 HAR、条件编译目录和生成代码可能不在默认扫描范围门禁需要把证据路径一起输出让开发者判断是补扫描范围、补保留说明还是删除声明。四、AST 只认可字面量动态数组进入人工队列第二段代码用ts-morph查找requestPermissionsFromUser()。正则容易把注释、示例字符串和同名业务方法算进去AST 至少可以确认调用表达式。脚本只自动提取数组字面量中的字符串变量、函数返回值和展开表达式都标成 unresolved不做“猜测式成功”。import{Project,Node,SyntaxKind}fromts-morph;exportfunctionscanRuntimeRequests(tsconfig:string){constprojectnewProject({tsConfigFilePath:tsconfig});constrequestednewSetstring();constunresolved:string[][];for(constsourceofproject.getSourceFiles()){for(constcallofsource.getDescendantsOfKind(SyntaxKind.CallExpression)){if(!call.getExpression().getText().endsWith(requestPermissionsFromUser))continue;constpermissionArgcall.getArguments()[1];if(!Node.isArrayLiteralExpression(permissionArg)){unresolved.push(${source.getFilePath()}:${call.getStartLineNumber()});continue;}for(constelementofpermissionArg.getElements()){if(Node.isStringLiteral(element))requested.add(element.getLiteralText());elseunresolved.push(${source.getFilePath()}:${element.getStartLineNumber()});}}}return{requested,unresolved};}这里的第二个参数位置来自常见调用签名但脚本还会校验导入符号来自 AbilityKit避免把业务对象的同名方法算进去。对于封装后的permissionService.requestCamera()项目通过一个显式注解文件把业务方法映射到权限集合映射也进入版本控制不能藏在 CI 环境变量里。当三方 HAR 不提供源码时门禁无法证明其内部调用。此时应结合依赖文档、产物清单与真机授权路径复核并把结论写入 allowlist。工具的边界是提供可重复证据不是宣称静态分析看见了全部运行时行为。五、差分结果必须带处置动作第三段代码把集合差分转成构建结论。任务REVIEW-1346的初始orphan包含ohos.permission.MEDIA_LOCATION所以即使 missing 为 0 也必须失败。修复后敏感权限只剩 CAMERA 与 MICROPHONE两者都有运行时调用、reason 和 usedScene。typeGateResult{missing:string[];orphan:string[];invalidReason:string[];invalidUsedScene:string[];unresolved:string[];state:PASSED|BLOCKED;};exportfunctionevaluate(declared:Setstring,runtime:Setstring,allow:Setstring,invalidReason:string[],invalidUsedScene:string[],unresolved:string[]):GateResult{constmissing[...runtime].filter(p!declared.has(p));constorphan[...declared].filter(p!runtime.has(p)!allow.has(p));constblockedmissing.lengthorphan.lengthinvalidReason.lengthinvalidUsedScene.lengthunresolved.length0;return{missing,orphan,invalidReason,invalidUsedScene,unresolved,state:blocked?BLOCKED:PASSED};}Hvigor 节点在打包前执行该函数失败时写出机器可读 JSON 和短日志再以非零状态结束。不能只在控制台打印警告因为流水线很容易被海量构建信息淹没。门禁也不会自动修改 module.json5删除权限会影响产品能力必须由责任开发者确认并提交代码评审。目录按职责拆成tools/permission-audit/ManifestReader.ts、RuntimeScanner.ts、GateEvaluator.ts页面pages/ReviewPreflightPage.ets只读取最新报告。最终日志为taskREVIEW-1346 declaredUserGrant2 runtimeRequested2 orphan0 missing0 statePASSED以及reason2/2 usedScene2/2 unresolved0 moduleentry durationMs184。六、从 BLOCKED 到 PASSED 不能丢掉失败证据报告保留本次修复前后的差异。初始记录是orphan1 permissionohos.permission.MEDIA_LOCATION stateBLOCKED修复提交删除声明后再次运行才生成 PASSED。这样审核负责人能回答“为什么删、谁确认、哪次构建通过”而不是只看到一张全绿截图。reason 检查还会确认资源键在默认语言和中文资源中都存在usedScene 检查 Ability 名称是否真正在当前模块声明when是否为支持值。若一个权限在多个 HAP 重复声明报告按模块保留来源不急着合并否则会掩盖某个模块独立缺字段的问题。七、手机页是提审清单不是权限申请页运行图展示任务REVIEW-1346、模块 entry、最终状态 PASSED、敏感声明 2、运行时请求 2、missing 0、orphan 0、reason 2/2、usedScene 2/2、unresolved 0、耗时 184 ms。时间 13:46按钮是“导出审核报告”和“查看修复记录”不会在这个页面再次拉起权限弹窗。这一区分很重要门禁页面只是读取构建产物不参与运行时授权。真正的 CAMERA、MICROPHONE 申请仍发生在用户触发拍摄或录音功能时并由业务页面处理拒绝与降级。为了让审计页变绿而提前申请权限反而违背场景化授权。八、静态门禁减少的是解释成本这套脚本不能替代隐私合规评审也不能发现所有反射、Native 或闭源依赖行为。它能稳定解决的是工程事实漂移声明新增了代码没跟功能删除了权限还在reason 资源丢了usedScene 指向已改名的 Ability。我最终把“通过”定义得很克制扫描范围内证据闭合、动态调用均有人工记录、配置字段完整。它不保证一定通过市场审核却能让团队在提交前消灭一批本可自动发现的矛盾。对频繁迭代的 HarmonyOS 工程来说这比临近提审再全文搜索可靠得多。参考资料华为开发者声明应用权限华为开发者abilityAccessCtrl API