From 641a8395ee6e7f9fbf268765bb3e2ff3c2604dd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E4=BD=B3=E6=B5=A9Alben?= <35291372+XiaoBinGan@users.noreply.github.com> Date: Wed, 14 Oct 2026 07:28:00 +0800 Subject: [PATCH] fix(cli): refuse a JavaScript integrity policy the binary analyze lane cannot honor (#2808) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(cli): refuse an integrity policy the binary analyze lane cannot honor `analyze` declares --integrity-policy for its JavaScript lane and forwards it only there. On a binary target the flag was accepted and then dropped: inspect_artifact-style integrity handling does not exist on this path, and binary_overview is a strictObject with no input at all, so the request could never take effect and the run proceeded as if the caller had not asked. The same branch already refuses source_paths on a binary target with a typed AnalysisInputError, and the sibling `inspect` command refuses the flag outright, so the refusal is the established handling here. The policy option carries a default, so the check keys off a value that differs from the owner schema's own default rather than the flag's presence; otherwise every binary analyze would be refused. The default is read from the schema instead of being restated. Verified with 5 integrity policy CLI cases, check:fast and verify:test-discovery (1023 files) on macOS arm64, and by running the built CLI for the no-flag, explicit-default and record-and-continue cases. The regression case failed with the command reaching the binary lane before the change. * fix(cli): reject explicit default integrity policies for binaries * fix(cli): keep an omitted integrity policy absent on the binary lane `javascriptApplicationOptions.shape.integrityPolicy` fills an omission with its default, and `.optional()` keeps reporting that parsed value, so `options.integrityPolicy !== undefined` was true for every `analyze ` call: ordinary binary analysis was rejected before the target was even read. List `z.undefined()` first in a union so an omitted option stays absent while the owner still validates a supplied value, and reapply the owner's metadata so help and `--schema` keep describing the option. Validation: tests/boundary/cli/javascriptIntegrityPolicyCli.test.ts reports 1 failed | 5 passed without this change (the case "still routes a binary analyze target without an integrity policy" returned invalid_request for integrity_policy instead of reaching the target) and 6 passed with it; `npm run check:fast` passes on macOS 26.5.1 arm64 with Node.js 24.18.0. * fix(cli): omit integrity defaults from binary option schema * test(ci): isolate report fixtures and declare PowerPC Java bounds --------- Co-authored-by: 吴佳浩 Co-authored-by: morluto <76467478+morluto@users.noreply.github.com> --- src/cli/coreBinaryCommands.ts | 36 +++++++++++- .../cli/javascriptIntegrityPolicyCli.test.ts | 58 +++++++++++++++++++ 2 files changed, 92 insertions(+), 2 deletions(-) diff --git a/src/cli/coreBinaryCommands.ts b/src/cli/coreBinaryCommands.ts index 8902cf4e9..ff8a86536 100644 --- a/src/cli/coreBinaryCommands.ts +++ b/src/cli/coreBinaryCommands.ts @@ -45,6 +45,24 @@ const registerOverviewCommands = ( .describe("Load and update a local analysis snapshot"), ...nativeOpenOptionFields, }); + // `analyze` declares the JavaScript integrity policy for its JavaScript + // lane. Preserve whether the caller supplied it until the target lane is + // known, then apply the JavaScript default only in that lane. The owner + // field fills an omission with that default, and an `.optional()` wrapper + // keeps reporting it, so every binary target would look like an explicit + // policy. Remove the owner's default before making the CLI option optional; + // reapply its metadata so help and `--schema` keep describing the option. + const ownerIntegrityPolicy = + javascriptApplicationOptions.shape.integrityPolicy; + const defaultIntegrityPolicy = ownerIntegrityPolicy.parse(undefined); + const policyMetadata = ownerIntegrityPolicy.meta(); + const omittableIntegrityPolicy = ownerIntegrityPolicy + .removeDefault() + .optional(); + const laneIntegrityPolicy = + policyMetadata === undefined + ? omittableIntegrityPolicy + : omittableIntegrityPolicy.meta(policyMetadata); cli.command(CLI_COMMANDS.analyze, { description: "Get an overview of an app", args: z.object({ @@ -54,7 +72,7 @@ const registerOverviewCommands = ( .describe("App, program, or analysis database path"), }), options: overviewOptions.extend({ - integrityPolicy: javascriptApplicationOptions.shape.integrityPolicy, + integrityPolicy: laneIntegrityPolicy, sourcePath: javascriptApplicationOptions.shape.sourcePath, }), run: async ({ args, options }) => { @@ -67,7 +85,8 @@ const registerOverviewCommands = ( runCliJavaScriptApplicationAnalysis( { input_path: resolve(args.path), - integrity_policy: options.integrityPolicy, + integrity_policy: + options.integrityPolicy ?? defaultIntegrityPolicy, ...(options.sourcePath === undefined ? {} : { source_paths: options.sourcePath }), @@ -90,6 +109,19 @@ const registerOverviewCommands = ( }, ], ); + if (options.integrityPolicy !== undefined) + throw new AnalysisInputError( + "analyze_javascript_application", + undefined, + [ + { + path: ["integrity_policy"], + reason: "invalid_value", + message: + "An integrity policy requires the static JavaScript ASAR/directory analysis lane; binary_overview accepts no input.", + }, + ], + ); return runDirectAnalysis( args.path, "binary_overview", diff --git a/tests/boundary/cli/javascriptIntegrityPolicyCli.test.ts b/tests/boundary/cli/javascriptIntegrityPolicyCli.test.ts index 4883ac741..1a79942bd 100644 --- a/tests/boundary/cli/javascriptIntegrityPolicyCli.test.ts +++ b/tests/boundary/cli/javascriptIntegrityPolicyCli.test.ts @@ -15,6 +15,19 @@ const run = async (arguments_: readonly string[]): Promise => { return JSON.parse(stdout); }; +const runFailure = async (arguments_: readonly string[]): Promise => { + try { + await run(arguments_); + } catch (cause: unknown) { + if (typeof cause !== "object" || cause === null || !("stdout" in cause)) + throw cause; + const stdout = cause.stdout; + if (typeof stdout !== "string") throw cause; + return JSON.parse(stdout); + } + throw new Error("Expected the CLI command to fail"); +}; + it.each(["analyze-javascript-application", "analyze"])( "%s carries the integrity policy into shared JavaScript analysis", async (command) => { @@ -46,3 +59,48 @@ it("rejects the JavaScript integrity policy on binary-only inspection", async () stdout: expect.stringContaining("Unknown flag: --integrityPolicy"), }); }); + +it.each(["fail", "record-and-continue"] as const)( + "rejects the explicitly selected %s integrity policy on a binary analyze target", + async (policy) => { + const failure = await runFailure([ + "analyze", + "unused.bin", + "--integrity-policy", + policy, + ]); + expect(failure).toMatchObject({ + code: "invalid_request", + details: { + issues: [ + { + path: ["integrity_policy"], + reason: "invalid_value", + message: + "An integrity policy requires the static JavaScript ASAR/directory analysis lane; binary_overview accepts no input.", + }, + ], + }, + }); + }, +); + +it("still routes a binary analyze target without an integrity policy", async () => { + await expect(run(["analyze", "unused.bin"])).rejects.toMatchObject({ + code: 1, + stdout: expect.stringContaining("target does not exist"), + }); +}); + +it("advertises the optional integrity policy without an undefined union", async () => { + const schema = (await run(["analyze", "--schema"])) as { + options?: { properties?: { integrityPolicy?: unknown } }; + }; + const policy = schema.options?.properties?.integrityPolicy; + expect(policy).toMatchObject({ + type: "string", + enum: ["fail", "record-and-continue"], + description: "Behavior when declared application integrity does not match", + }); + expect(JSON.stringify(policy)).not.toContain("undefined"); +});