mcpField
A
audit-ppmplugin

audit-ppmplugin

github.com/microsoft测试与质量1.1k71 次安装今天更新
代码仓库

关于 audit-ppmplugin

audit-ppmplugin 是面向 .ppmplugin 开发者的只读验证技能,用于上传前检查已完成的包。它在本地检查包格式、清单、二进制文件及运行时加载相关问题。

主要功能

  1. 01只读检查软件包
  2. 02验证 ZIP 布局与清单
  3. 03检查清单和二进制文件的一致性
  4. 04扫描 Android DEX 完整性及遗留内容
  5. 05检查 iOS 框架结构

适用场景

  1. 01上传前验证软件包
  2. 02定位清单与平台二进制文件不匹配
  3. 03发现 Android DEX 或 iOS 框架加载风险

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/microsoft/power-platform-skills --skill audit-ppmplugin

安装后在 Claude Code 中输入/audit-ppmplugin确认已加载。

详细介绍

/audit-ppmplugin

验证关卡。/assemble-ppmplugin 用于证明包的格式正确;此技能则用于证明它能在 wrap 运行时实际加载并分发调用。大多数 .ppmplugin 故障都不会显现:包可以顺利上传,但随后某个方法返回 native module 'X' not loaded,运行时无法实例化包类,或者上传因 0x80040265(规范前缀违规)而被拒绝。要发现这些问题,通常需要完整跑一轮 wrap 构建。此技能可在几秒内于本地发现它们。

此技能对包只读——它会解压到临时目录进行检查,绝不会修改 .ppmplugin。修复应在上游进行(清单问题使用 /generate-ppmplugin-manifest,二进制问题使用构建技能),然后重新组装并再次审计。

请阅读 shared/ppmplugin-format.md §1(布局)、§3(规范前缀)、§4(验证器规则)、§5(Android DEX 要求)——此技能会针对已构建的制品检查以上四项。

此技能不会做什么

  • 不会构建、压缩或编写任何内容——它只检查已完成的 .ppmplugin。应在上游修复问题,然后重新组装。
  • 不会就地修补 zip——包是不可变的交付物;修改它会导致它与暂存源文件不同步。此技能会指向相应的上游技能。
  • 不会上传到 Dataverse,也不会接入 canvas 应用(第 3 阶段——暂缓)。「READY TO UPLOAD」 表示通过本地验证,而非已上传。

步骤 1 — 阅读共享文档 + 确定制品 + 检查先决条件

  1. 阅读 shared/shared-instructions.md 和 shared/ppmplugin-format.md。
  2. 确定要审计的包:
    • 如果用户提供了路径,则使用该路径。如果是目录,则查找 <dir>/*.ppmplugin(如果有多个,请提示用户选择)。
    • 否则,默认使用组装输出:仓库根目录下唯一的 ppmplugin/<name>.ppmplugin(如果有多个,则使用最近构建的那个)。
    • 如果没有找到,立即停止并提示:NEEDS_CONTEXT: no .ppmplugin to audit — run /assemble-ppmplugin first, or pass a path。
  3. 先决条件检查(shared-instructions §9.2)。策略:先定位,不要直接放弃(§1.5)——失败前先按路径查找工具:
检查验证缺失时
jar(JDK)jar --version — 用于列出并提取包内容停止并提示 BLOCKED: jar not found — install a JDK (JDK 17)
DEX 扫描器dexdump(首选——在 Android SDK build-tools/*/dexdump 中定位,方式类似 /build-android-binary 定位 $D8);否则使用 strings(mac/linux 自带)如果两者都没有:DEX 字符串检查降级为 WARNING(「跳过 DEX 扫描——未找到 dexdump/strings」);仍会执行结构检查(魔数、大小)。在 Windows 上,如果两者都没有,请注明此限制。
  1. 解压包到新建的临时目录以供检查(原文件只读):
    work=$(mktemp -d); ( cd "$work" && jar xf "<abs path to .ppmplugin>" )
    
    $work = New-Item -ItemType Directory -Force (Join-Path $env:TEMP "ppm-audit"); Push-Location $work; jar xf "<abs path>"; Pop-Location
    

后续每个步骤都将检查结果追加到列表,格式为 [SEVERITY] <check-id>: <result>。不要在遇到第一个 CRITICAL 时就停止——请收集所有问题,以便用户一次性修复。严重级别:CRITICAL(上传或设备运行时会失败——阻断)、WARNING(很可能有误,但仍可能正常工作)、INFO(风格建议或提示)。


步骤 2 — 类别 A:Zip 结构

清单来自 jar tf "<.ppmplugin>"。此包仅包含原生内容(格式 §1):根目录下有 manifest.json,以及仅包含已声明的 android/ / ios/ 分片。

检查项断言严重性
zip-manifest-at-rootmanifest.json 位于归档根目录CRITICAL
zip-only-native-slices顶层条目 ⊆ { manifest.json、android/、ios/ }CRITICAL
zip-no-ts-js-layer任何位置都没有 src/、*.ts、*.tsx、*.js、extension.js、extension.hbc — 此包不包含 TS/JS 层CRITICAL
zip-no-build-dirs没有混入 android-build/、ios-build/、node_modules/、dist/、build/CRITICAL
zip-no-stray-files没有 META-INF/、.DS_Store、隐藏文件、嵌套的 *.zip/*.ppmpluginWARNING
zip-reasonable-size包小于 50 MB(较大的包通常意味着 React 或 node_modules 被一并打包)WARNING

src/ 目录树、extension.js/.hbc 或任何 *.ts 都属于 SDK 时代 / JS 层泄漏 — 标记为 CRITICAL,并指向格式 §6(此包只能包含原生二进制文件)。


步骤 3 — 类别 B:Manifest 架构、验证器规则 + 字段泄漏

读取解压后的 manifest.json。针对构建产物重新运行 ppmplugin-format §4 中的所有规则(深度防御 — /generate-ppmplugin-manifest 之后可能有人手动编辑了 manifest):

检查项断言严重性
mf-name-shapename 匹配 ^[a-z0-9][a-z0-9-]{0,63}$CRITICAL
mf-canonical-prefix每个 receivers[].nativeModule 都以 name 的规范前缀开头(按 -/_ 拆分,每段转为 PascalCase,再拼接 — §3)(按序数、区分大小写)CRITICAL
mf-reserved-prefix没有 nativeModule 以保留前缀开头(§4 中不区分大小写的列表:Microsoft、MS、Intune、Wrap、Pcf、PowerApps、…)CRITICAL
mf-reserved-exact-known没有 nativeModule 与本地检查的不兼容名称子集(§4:DeviceInfo、AuthenticationHelper、NetworkClient、DataverseOfflineProvider、IntuneMAM)完全匹配。并非完整列表;修复方法 = 将 getName() 重命名为非保留形式(添加 Module 后缀或供应商前缀),然后重新派生CRITICAL
mf-nativeModule-generic-nounnativeModule 不匹配通用平台名词启发式规则 ^(Device|Network|File|Audio|Camera|Sensor|Location|Storage|Notification|Bluetooth|Wifi|Media|Photo|Contact|Calendar|Battery) — 这些裸名称既容易命中拒绝名单,也容易在共享的 NativeModules 命名空间中发生冲突。(这是启发式规则,不是真实名单 — DeviceInfo 在本地被判定为 READY,但被服务端拒绝;此规则本可以发出警告。)WARNING
mf-methods每个 receivers[].methods 都存在、非空、不超过 32 项,且每项匹配 ^[a-zA-Z_$][a-zA-Z0-9_$]{0,127}$CRITICAL
mf-receiver-name每个 receivers[].name 都匹配 JS 标识符正则表达式CRITICAL
mf-version-presentversion 是非空字符串WARNING
mf-no-sdk-fieldsmanifest 中没有 entrypoints.js、entrypoints.ts、extension.js、extension.hbc、extensionClassName、jsLayer 字段。这是我们的纯原生策略,不是服务端准入条件 — wrap 注入器将 entrypoints.js 视为可选并接受它;我们标记此字段,是为了让纯原生包保持明确。此项为 WARNING,而非阻止项。WARNING

请在报告中注明,本地只检查了不兼容精确名称的已知子集 (mf-reserved-exact-known),因此本地检查通过并不保证上传服务会接受该名称。


Step 4 — 类别 C:清单 ↔ bundle 一致性

声明的 entrypoints 必须与 zip 中实际包含的二进制文件完全匹配。(/assemble-ppmplugin 会在构建时协调这两者;审计已完成的 bundle 可以发现手动编辑的清单或手动打包的 bundle。)

检查项断言严重程度
consistency-has-native至少声明 entrypoints.android / entrypoints.ios 其中之一(仅包含清单的 bundle 没有可加载的内容)CRITICAL
consistency-android-dex如果声明了 entrypoints.android → zip 中存在 android/<entrypoints.android.dex>CRITICAL
consistency-ios-framework如果声明了 entrypoints.ios → zip 中存在 ios/<entrypoints.ios.framework>.framework/(扁平 framework——参见类别 E 中的 ios-flat-framework-not-xcframework)CRITICAL
consistency-no-orphanzip 中的每个 android/ 或 ios/ 切片都有对应的已声明 entrypoint(不存在运行时无法路由到的未声明二进制文件)WARNING
consistency-packageClass-shapeentrypoints.android.packageClass 是完全限定的 Java 类名(^([a-z][a-z0-9_]*\.)+[A-Z][A-Za-z0-9_]*$)CRITICAL
dispatch-composite-key复合路由键 <name>/<receivers[].name> 格式正确(两个片段均存在,后缀符合 JS 标识符规则)——dispatcher PCF 会将其绑定为 ReceiverKey,以访问 NativeModules.<nativeModule>.<method>(格式 §2 — 运行时分发契约)WARNING

Step 5 — 类别 D:DEX 完整性 + SDK 泄漏字节扫描(Android)

仅当声明了 entrypoints.android 时适用。DEX 通过 DexClassLoader 加载(格式 §5);以下检查用于确认它可加载且不包含 SDK 时代的残留。若有 dexdump -l plain <dex>,则使用该命令;否则使用 strings <dex>——两者都会公开检查所扫描的字符串/类型池。

检查项断言严重程度
dex-magic首字节为 dex\n03[5-9](有效 DEX)CRITICAL
dex-has-packageClassDEX 中包含 entrypoints.android.packageClass 类型描述符(Lcom/.../<Pascal>Package;)——否则运行时类加载会失败CRITICAL
dex-has-module-classDEX 中包含模块类描述符CRITICAL
dex-no-INativeExtension不包含 INativeExtension、INativeOperation、INativeExtensionContext 字符串——这些是仅含原生内容的 bundle 中 SDK 时代的符号。清洁度策略,不是服务端门槛:播放器会忽略这些符号(旧模式插件只要其 receivers[] 映射到实际的原生模块,仍可分发),因此应显示出来,但不要阻止构建。WARNING
dex-no-jslayer-loader不包含 HermesBytecodeLoader、WrapPluginJsLayerLoader 字符串——仅含原生内容的 DEX 中不应出现 JS 层加载器(同样:标记问题,但不阻止构建)。WARNING
dex-no-sendAsync不包含 sendAsync 符号——可能存在 SDK 传输层泄漏WARNING

这些 SDK 泄漏检查是仅含原生内容契约的核心:即使编译干净,也仍可能通过传递依赖引入这些符号。扫描构建后的 DEX 是唯一能捕获它们的环节。


第 6 步 — 类别 E:iOS 框架结构(wrap-CI 门禁)

仅当声明了 entrypoints.ios 时检查。CRITICAL 检查项与运行时的 dlopen 加载和 wrap 签名流程所需内容完全一致。伞形头文件和模块映射仅用于构建规范(加载器会对二进制执行 dlopen,而不会 import 模块),因此它们属于 INFO — 缺少这些文件时,不要阻止有效 bundle 通过。

检查项断言严重性
ios-flat-framework-not-xcframeworkbundle 包含 ios/<framework>.framework/ — 而非 ios/<framework>.xcframework/。如果存在 .xcframework,则按原样返回 wrap-CI 错误("Framework '.framework' not found in plugin"),并提供修复方法:使用 /build-ios-binary 重新构建(设备切片、扁平框架 — 移除 -create-xcframework)。CRITICAL
ios-binary-presentios/<framework>.framework/<framework>(Mach-O 二进制,名称必须严格为 <framework>)存在 — 加载器会对 Frameworks/<framework>.framework/<framework> 执行 dlopen,因此缺失或名称不同的二进制无法加载CRITICAL
ios-framework-has-infoplistios/<framework>.framework/Info.plist 存在,且包含 CFBundlePackageType=FMWK、CFBundleExecutable=<framework>、CFBundleIdentifier、CFBundleShortVersionString、MinimumOSVersion — 没有 Info.plist 的框架 bundle 无效,wrap codesign 步骤会拒绝它CRITICAL
ios-framework-has-umbrella存在伞形头文件 ios/<framework>.framework/Headers/<framework>.h — 仅用于构建规范;dlopen 加载器不会 import 模块,因此缺少该文件不会影响加载INFO
ios-framework-has-modulemap存在 ios/<framework>.framework/Modules/module.modulemap — 仅用于构建规范(原因相同);运行时不需要INFO
ios-moduleClass-matchentrypoints.ios.moduleClass 是文档规定的 Obj-C 类(RCT<Pascal>Module),并且在源代码可访问时,.h/.m 中的类名与之匹配WARNING
ios-moduleName-matches在源代码可访问时,.m 声明了 + (NSString *)moduleName,返回值为 receivers[].nativeModule,且不使用 RCT_EXPORT_MODULE(...)(wrap 插件由运行时注册,而非通过 +load 注册)CRITICAL

(深入检查 Mach-O — Dynamic 与 Static、死代码剥离、弱 React 链接 — 由 /build-ios-binary 在构建时负责;此处验证交付的框架符合加载器和 wrap CI 的要求。)


第 7 步 — 类别 F:源代码到接收器的契约(当仓库可访问时)

这些检查需要扩展源代码。如果审计在控制仓库中运行(其中包含 bundle 的源代码树),则执行这些检查;否则,将每项标记为 SKIPPED — source not reachable,并注明构建技能已在构建时验证这些项。

检查项断言严重性
src-getName-matchesKotlin 模块的 getName() 字面值 == receivers[].nativeModule — 不匹配会导致设备上出现 native module 'X' not loaded(可正常编译;最难发现)严重
src-methods-annotated每个 receivers[].methods[] 条目都有匹配的 @ReactMethod(Android)/ RCT_EXPORT_METHOD(iOS)严重
src-package-fqn-matchesReactPackage FQN == entrypoints.android.packageClass严重
src-package-no-arg-ctor<Pascal>Package(Android)有一个公共无参构造函数 — 运行时使用 getDeclaredConstructor().newInstance();主构造函数带参数列表(class <Pascal>Package(...))会导致插件静默加载失败(Loaded 0 plugin package(s))。iOS 对应情况:模块类通过 [cls new] 实例化(没有自定义的带参数初始化方法)。严重
src-ios-module-no-arg-init(iOS)模块类没有仅带参数的初始化方法 — player 会执行 NSClassFromString(moduleClass) → [cls new],因此只有带参数初始化方法的类无法实例化,并会在加载时跳过(§5b)。严重
src-ios-requires-main-queue(iOS)如果声明了 + (BOOL)requiresMainQueueSetup,则返回 NO(返回 YES 会强制在启动时于主线程设置;若 init 较重或会抛出异常,就会导致启动卡顿或崩溃 — §5b)。警告
src-method-single-map-param每个 @ReactMethod / RCT_EXPORT_METHOD 都接收一个 ReadableMap/NSDictionary 请求参数(随后是 Promise/resolver)— wrap 代理会按位置展开 PCF 的 args:[request],因此带有多个位置参数的方法无法收到其负载(仅接收 Promise 的无参操作可以正常工作)。警告
pcf-composite-key-matches-receiver如果同级 PCF(pcf/<…>/index.ts)声明了 COMPOSITE_KEY = "<name>/<receiver>"(或绑定了 ReceiverKey),它必须等于 <manifest.name>/<receivers[].name> — 不匹配意味着 PCF 会向 manifest 未注册的 receiver 分发请求,并在首次调用时失败(实际问题:PCF→Snapshot,manifest→DeviceInfoExtension)。警告
pcf-uses-sendasync如果存在同级 PCF(pcf/<…>/index.ts),它必须通过 window.PowerApps.NativeExtension.sendAsync(...) 分发请求,绝不能直接调用 cordova.exec(或任何 cordova.*)。 直接调用 cordova.exec 是严重缺陷:PCF 沙箱中未暴露原始 cordova 全局对象,因此该调用在设备上会静默无效(Android 上尤为严重)— 它能通过 zip/manifest/DEX/key 检查,却只在设备上失败。修复方法:通过宿主注入的 sendAsync 全局对象转发(format §2)。严重
pcf-envelope-is-raw-object如果存在同级 PCF,sendAsync 负载必须是原始 { method, args: [...] } 对象 — PCF 不能预先对其执行 JSON.stringify(sendAsync 会在内部进行字符串化)。sendAsync(key, JSON.stringify({...})) 会造成双重编码 → 代理 JSON.parse 得到字符串,而不是 {method,args} → BRIDGE_FAILED。修复方法:传入对象字面量,而不是字符串化后的对象。严重
pcf-args-is-array如果存在同级 PCF,sendAsync 负载中的内部 args 必须是 JSON 数组(args: [request]),而不是裸对象 — 解析后,wrap 代理会执行 Array.isArray(parsed.args) ? parsed.args : [] 并丢弃非数组值,因此原生方法收不到负载(format §2)。警告
pcf-has-ambient-dts如果存在同级 PCF,它会附带本地 PowerAppsNativeExtension.d.ts 环境声明(为 window.PowerApps.NativeExtension.sendAsync 提供类型),而不是导入宿主 SDK 包 — 以保持 PCF 与宿主无关。信息
src-no-ReactModule-annotation模块没有 @ReactModule 注解(与 DexClassLoader 不兼容 — format §5)严重
src-ctor-no-throwable-sideeffects将扫描范围限定为模块的构造闭包,而不是整个文件 — 主/次构造函数、Kotlin init{} 代码块、构造时运行的属性初始化器(private val x = …),以及它们调用的任何私有函数(跟进一层 foo() / this.foo())。在该闭包内,标记:(a) register*Callback(…, null),或不显式指定 Looper 的裸 Handler() / Handler(...);(b) 任何未置于 try { } catch 中的副作用调用(register*/add*Listener/observe/getSystemService+使用/文件或网络 I/O/runBlocking)。@ReactMethod 方法体不在范围内(它们在每次调用时运行,而不是在构造时运行)。模块会在桥接启动时于可能没有 Looper 的线程上急切构造 — 未捕获的异常会在启动时、任何 UI 出现之前使宿主崩溃(format §5)。修复方法:延迟到首次调用时进行惰性初始化、传入 Handler(Looper.getMainLooper()),并用 try/catch 包裹无法避免的初始化。iOS 对应情况: 对可能抛出异常或耗时的 -init 应用相同扫描(通过 [cls new] 急切实例化,§5b)。启发式检查 — 确定性捕获依赖 /test-native-extension 第 1 层的加载/初始化就绪断言(静态检查)和第 5 层的运行时启动崩溃扫描。警告
pcf-response-unwraps-message如果存在同级 PCF,其 sendAsync 成功路径会解包 wrap 响应容器({isUpdate, message:"<json>"})— 即调用类似 extractResponse 的辅助函数(解析 result.data 并检查 message),而不是只执行一次 JSON.parse。仅解析一次的处理程序会停留在容器层,尽管原生调用成功,每次调用仍会因 UNEXPECTED_PAYLOAD 失败(format §2)。警告
src-promise-always-settled每个接收 Promise / resolver+rejecter 的 @ReactMethod / RCT_EXPORT_METHOD 至少在一条可达路径上包含 promise.resolve / promise.reject(或 resolve(...) / reject(...))。接收 Promise 的方法若可能不处理 Promise 就返回,会让调用方陷入挂起状态,且没有代码或消息。警告
src-listener-released模块中的每个 register* / add*Listener / observe / 已获取的 manager,都在 invalidate() / onCatalystInstanceDestroy() / teardown 中有对应的释放操作(unregister* / remove*Listener / .close() / .release())。已注册的回调若未释放,会造成泄漏,并可能在模块失效后触发。警告

第 8 步 — 报告并路由修复

将判定结果作为可见的围栏代码块打印出来(参见 shared-instructions §9.1)。按照 shared-instructions.md §9.3 为每个严重性级别添加前缀 — 🔴 CRITICAL、🟡 WARNING、🔵 INFO、🟢 READY — 保留文字,确保在不支持表情符号的终端中也能传达含义:

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 audit-ppmplugin — pen-input.ppmplugin
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 Path: ppmplugin/pen-input.ppmplugin   (v0.1.4, Android-only, 3.4 KB)
 🔴 CRITICAL: 0   🟡 WARNING: 1   🔵 INFO: 0   SKIPPED: 4 (source not reachable)

 🟡 [WARNING] zip-no-stray-files: .DS_Store present in android/
   Fix: re-run /assemble-ppmplugin (it excludes dotfiles).

 ⓘ The upload service may apply additional name checks not represented locally.
 🟢 ✓ READY TO UPLOAD
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

(如果存在 CRITICAL,判定行应以 🔴 开头,最后一行应为 🔴 ✗ NOT READY — fix the CRITICAL findings above。)

每条非无问题项都要附带 Fix:,写明具体的后续操作,并路由到负责的技能(绝不能使用 zip 补丁):

  • Manifest 规则 / 泄漏字段 → “编辑并重新运行 /generate-ppmplugin-manifest,然后运行 /assemble-ppmplugin。”
  • DEX 泄漏 / 缺少类 → “Android 源代码引入了 SDK 符号 — 移除该导入;重新运行 /build-android-binary,然后运行 /assemble-ppmplugin。”
  • Zip 多余文件 / 结构问题 → “重新运行 /assemble-ppmplugin。”

然后通过 AskUserQuestion 展示后续步骤(参见 shared-instructions §9.1):

  • 无问题(0 个 CRITICAL): 提供 重新运行 /generate-ppmplugin-manifest(仅当某条 WARNING 指向该操作时)/ 保持现状 — 我会手动上传(Stage 3 尚未成为技能)。用户选择 Run /… 选项时,在同一轮中通过 Skill 工具调用该技能(§8 + §9.1 “执行,而非描述”)。
  • 有问题(≥1 个 CRITICAL): 提供跳转到首个 CRITICAL 对应技能的选项 — 运行 /generate-ppmplugin-manifest 或 运行 /build-android-binary — 然后重新组装并重新审计。用户选择后调用该技能。

更新 .extension-state.md 中的 ## ppmplugin (third-party controls) 块:Audit: <name>.ppmplugin <ISO timestamp> — <CRITICAL>C/<WARNING>W (READY | BLOCKED)。

返回代码:

  • DONE — 0 个 CRITICAL(无问题,或仅有警告 ⇒ 说明警告)。该包通过本地验证。
  • BLOCKED: audit found <N> CRITICAL — see findings — 拒绝给出 READY 判定;不得声称该包可上传。

评论

支持 Markdown