From ec3d2ed66c0c283b4e12e2e4ad0ba270f1415c60 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Sun, 1 Nov 2026 15:26:48 -0800 Subject: [PATCH] fix(release-exposure): keep fuses unknown on incomplete inventory A directory inventory that stops at an acquisition limit never reports the images it did not reach, so the native image sample has no omission and safe fuses on an included helper settled to not_observed even though scan coverage was unknown. Treat unknown coverage like a truncated sample until the Electron executable is accounted for. A selected native file is that executable, so it keeps its decided fuse result. --- docs/release-exposure.md | 2 +- .../ReleaseExposureService.test.ts | 99 ++++++++++++++----- .../releaseExposure/ReleaseExposureService.ts | 6 +- src/domain/releaseExposure.test.ts | 31 ++++++ src/domain/releaseExposure.ts | 7 +- 5 files changed, 114 insertions(+), 31 deletions(-) diff --git a/docs/release-exposure.md b/docs/release-exposure.md index 5cf7d6431c..1a3b7859d7 100644 --- a/docs/release-exposure.md +++ b/docs/release-exposure.md @@ -58,7 +58,7 @@ A `.map` too large to parse is a linker map when its header cannot begin a JSON ## Status -`exposed` means the check's condition was observed. `not_observed` means that condition was absent in a completed scan. `not_applicable` means the check does not apply to this artifact, for example Electron fuses on an app bundle with no Electron Framework, or on a directory whose native images were all read and hold no fuse sentinel. `unknown` means the check could not be decided, including a directory with no ASAR layout where those reads are missing or incomplete (an `asar: false` Electron build has no ASAR to find), a missing fuse sentinel on an Electron target, a universal binary with a slice that has no sentinel or an undecided fuse, a file that changed after inventory, Electron fuses on a selected ASAR (which does not include the executable that holds them), a PE inside a package that was too large to read, Go or Rust metadata on a native image stored inside a package, and Go or Rust metadata that was not present. Absence of that metadata is not proof the binary is something else. `not_assessed` means this audit did not run the check. +`exposed` means the check's condition was observed. `not_observed` means that condition was absent in a completed scan. `not_applicable` means the check does not apply to this artifact, for example Electron fuses on an app bundle with no Electron Framework, or on a directory whose native images were all read and hold no fuse sentinel. `unknown` means the check could not be decided, including a directory with no ASAR layout where those reads are missing or incomplete (an `asar: false` Electron build has no ASAR to find), safe fuses from an incomplete inventory that has not accounted for the Electron executable, a missing fuse sentinel on an Electron target, a universal binary with a slice that has no sentinel or an undecided fuse, a file that changed after inventory, Electron fuses on a selected ASAR (which does not include the executable that holds them), a PE inside a package that was too large to read, Go or Rust metadata on a native image stored inside a package, and Go or Rust metadata that was not present. Absence of that metadata is not proof the binary is something else. `not_assessed` means this audit did not run the check. Cancellation during a Go, Rust, fuse, or desktop read returns a typed cancellation error. Its partial observation holds the report with completed facts, and checks whose readers did not run are `unknown`. Cancellation during the member scan itself returns no partial report. diff --git a/src/application/releaseExposure/ReleaseExposureService.test.ts b/src/application/releaseExposure/ReleaseExposureService.test.ts index fa2ee63a33..a4c46b6fc2 100644 --- a/src/application/releaseExposure/ReleaseExposureService.test.ts +++ b/src/application/releaseExposure/ReleaseExposureService.test.ts @@ -745,24 +745,36 @@ describe("ReleaseExposureService fuse sampling", () => { }); }); +const directoryScan = (paths: readonly string[]) => { + const images = paths.map((path) => ({ path, format: "elf" as const })); + return scan({ + target: { + selected_path: "/work/app", + kind: "directory", + format: "directory", + sha256: null, + }, + subject: { path: "/work/app", sha256: SHA, format: "directory" }, + native_images: images, + fuse_candidates: images.map((image) => ({ + path: image.path, + role: "native-image" as const, + })), + }); +}; + +const runAsNode = async ( + service: ReleaseExposureService, + path = "/work/app", +) => { + const result = await service.inspect({ path }); + if (!result.ok) throw result.error; + return releaseExposureReportSchema + .parse(result.value.normalized_result) + .checks.find((check) => check.id === "electron.run_as_node"); +}; + describe("ReleaseExposureService Electron applicability", () => { - const directoryScan = (paths: readonly string[]) => { - const images = paths.map((path) => ({ path, format: "elf" as const })); - return scan({ - target: { - selected_path: "/work/app", - kind: "directory", - format: "directory", - sha256: null, - }, - subject: { path: "/work/app", sha256: SHA, format: "directory" }, - native_images: images, - fuse_candidates: images.map((image) => ({ - path: image.path, - role: "native-image" as const, - })), - }); - }; const appScan = scan({ target: { selected_path: "/work/App.app", @@ -783,16 +795,6 @@ describe("ReleaseExposureService Electron applicability", () => { }, ], }); - const runAsNode = async ( - service: ReleaseExposureService, - path = "/work/app", - ) => { - const result = await service.inspect({ path }); - if (!result.ok) throw result.error; - return releaseExposureReportSchema - .parse(result.value.normalized_result) - .checks.find((check) => check.id === "electron.run_as_node"); - }; const noSentinel = ok(evidence(fuseReport([slice(0, { sentinels: 0 })]))); const unreadable = err(new AnalysisInputError("inspect_electron_fuses")); @@ -806,6 +808,49 @@ describe("ReleaseExposureService Electron applicability", () => { expect((await runAsNode(service))?.status).toBe("not_observed"); }); + it("keeps safe fuses unknown when inventory may have missed the executable", async () => { + const { service, electronFuses } = owners({ + ...directoryScan(["/work/app/helper"]), + coverage: "unknown", + }); + electronFuses.inspect.mockResolvedValue( + ok(evidence(fuseReport([slice(0)]))), + ); + expect((await runAsNode(service))?.status).toBe("unknown"); + }); + + it("keeps safe fuses decided when inventory stopped after the hinted executable", async () => { + const { service, desktop, electronFuses } = owners({ + ...directoryScan(["/work/app/MyApp.exe"]), + coverage: "unknown", + }); + desktop.inspect.mockResolvedValue(ok(evidence(electronDesktopReport()))); + electronFuses.inspect.mockResolvedValue( + ok(evidence(fuseReport([slice(0)]))), + ); + expect((await runAsNode(service))?.status).toBe("not_observed"); + }); + + it("keeps safe fuses decided on a selected executable with unknown inventory", async () => { + const { service, electronFuses } = owners( + scan({ + target: { + selected_path: "/work/app", + kind: "executable", + format: "elf", + sha256: SHA, + }, + native_images: [{ path: "/work/app", format: "elf" }], + fuse_candidates: [{ path: "/work/app", role: "native-image" }], + coverage: "unknown", + }), + ); + electronFuses.inspect.mockResolvedValue( + ok(evidence(fuseReport([slice(0)]))), + ); + expect((await runAsNode(service))?.status).toBe("not_observed"); + }); + it("does not hide a failed fuse read behind a missing ASAR hint", async () => { const { service, electronFuses } = owners( directoryScan(["/work/app/electron"]), diff --git a/src/application/releaseExposure/ReleaseExposureService.ts b/src/application/releaseExposure/ReleaseExposureService.ts index d6ac8eb898..a498077ae3 100644 --- a/src/application/releaseExposure/ReleaseExposureService.ts +++ b/src/application/releaseExposure/ReleaseExposureService.ts @@ -662,7 +662,11 @@ const limitNativeSample = ( const electronExecutableIncluded = scan.target.kind === "app" || scan.fuse_candidates.some((candidate) => candidate.role === "app-bundle") || - kept.some((image) => preferredPaths.has(image.path)); + kept.some( + (image) => + preferredPaths.has(image.path) || + image.path === scan.target.selected_path, + ); return { electronExecutableIncluded, scan: { diff --git a/src/domain/releaseExposure.test.ts b/src/domain/releaseExposure.test.ts index 99e9f98181..1e81a240e0 100644 --- a/src/domain/releaseExposure.test.ts +++ b/src/domain/releaseExposure.test.ts @@ -370,6 +370,37 @@ describe("release exposure fuse sampling", () => { ); expect(statusOf(included, "electron.run_as_node")).toBe("not_observed"); }); + + it("does not treat incomplete inventory as an absent fuse", () => { + const fuseFacts = { + electron_family: "electron" as const, + fuse_images: [ + { + kind: "report" as const, + role: "native-image" as const, + implications: [ + { subject: "electron_run_as_node", outcome: "not_honored" }, + ], + }, + ], + }; + const incomplete = scan({ coverage: "unknown" }); + expect( + statusOf( + reportFor(incomplete, owners(fuseFacts)), + "electron.run_as_node", + ), + ).toBe("unknown"); + expect( + statusOf( + reportFor( + incomplete, + owners({ ...fuseFacts, electron_executable_included: true }), + ), + "electron.run_as_node", + ), + ).toBe("not_observed"); + }); }); describe("release exposure package checks", () => { diff --git a/src/domain/releaseExposure.ts b/src/domain/releaseExposure.ts index d86a3d07c7..41488daa51 100644 --- a/src/domain/releaseExposure.ts +++ b/src/domain/releaseExposure.ts @@ -562,10 +562,13 @@ const fuseCheck = ( : [], ); const failed = images.some((image) => image.kind === "failed"); - const partialFuseSample = + const unreadImagePossible = + scan.coverage === "unknown" || scan.omitted.some( (item) => item.subject === "native images" && item.count > 0, - ) && owners.electron_executable_included !== true; + ); + const partialFuseSample = + unreadImagePossible && owners.electron_executable_included !== true; let status: Status; if (outcomes.includes(spec.exposed)) status = "exposed"; else if (owners.electron_family === "not_electron") status = "not_applicable";