fix(audit): keep an unresolved Electron host's fuses unknown

fuseCheck decided from whatever outcomes the images reported, so a host
with no fuse answer read not_observed as soon as any other image in the
directory reported safe fuses.

Pass the images named as the host into the report and leave a check
unknown when one of them has no answer for it. Other images stay free to
have no fuses, because helpers in a real Electron install have none.
This commit is contained in:
Matt Van Horn committed 2026-11-02 02:33:01 -08:00
1 parent f9eb5e0db3
commit fa91268045
3 files changed
+61 -1

No files matched your search

@@ -242,6 +242,7 @@ export class ReleaseExposureService {
electron_family: family.family,
electron_family_reason: family.reason,
electron_executable_included: prepared.electronExecutableIncluded,
electron_host_paths: prepared.electronHostPaths,
fuse_images: fuseImages,
go_images: goImages,
rust_images: rustImages,
@@ -671,6 +672,7 @@ const limitNativeSample = (
): {
readonly scan: ReleaseExposureScan;
readonly electronExecutableIncluded: boolean;
readonly electronHostPaths: readonly string[];
} => {
const ranked = electronExecutableRank(scan.native_images, basisPaths);
const preferred = scan.native_images
@@ -700,6 +702,11 @@ const limitNativeSample = (
preferred.length <= MAX_RELEASE_EXPOSURE_NATIVE_IMAGES);
return {
electronExecutableIncluded,
electronHostPaths: kept.flatMap((image) =>
(ranked.get(image.path) ?? ANY_SIBLING_RANK) < ANY_SIBLING_RANK
? [image.path]
: [],
),
scan: {
...scan,
native_images: kept,
+43
View File
@@ -710,3 +710,46 @@ describe("go.build_paths locations", () => {
]);
});
});
describe("electron host fuses beside other images", () => {
const safeImage = {
kind: "report" as const,
role: "native-image" as const,
path: "/work/app/helper",
implications: [
{ subject: "electron_run_as_node", outcome: "not_honored" },
{ subject: "node_cli_inspect_arguments", outcome: "not_honored" },
{ subject: "embedded_asar_integrity_validation", outcome: "enforced" },
{ subject: "only_load_app_from_asar", outcome: "restricted_to_app_asar" },
],
};
const hostWithoutFuses = {
kind: "report" as const,
role: "native-image" as const,
path: "/work/app/app",
implications: [],
};
const report = (electron_host_paths?: readonly string[]) =>
reportFor(
scan(),
owners({
electron_family: "electron",
electron_executable_included: true,
...(electron_host_paths === undefined ? {} : { electron_host_paths }),
fuse_images: [hostWithoutFuses, safeImage],
}),
);
it("leaves an unresolved host unknown whatever another image reports", () => {
expect(statusOf(report(["/work/app/app"]), "electron.run_as_node")).toBe(
"unknown",
);
});
it("does not let a fuseless helper unsettle the host's safe answer", () => {
expect(statusOf(report(["/work/app/helper"]), "electron.run_as_node")).toBe(
"not_observed",
);
expect(statusOf(report(), "electron.run_as_node")).toBe("not_observed");
});
});
+11 -1
View File
@@ -421,6 +421,8 @@ export interface ReleaseExposureOwnerFacts {
| "not_a_directory";
readonly electron_family_reason?: string | null;
readonly electron_executable_included?: boolean;
/** Images named as the Electron host. Others may be helpers with no fuses. */
readonly electron_host_paths?: readonly string[];
readonly fuse_images: readonly FuseImageFact[];
readonly go_images: readonly MetadataImageFact[];
readonly rust_images: readonly MetadataImageFact[];
@@ -578,13 +580,21 @@ const fuseCheck = (
: [],
);
const failed = images.some((image) => image.kind === "failed");
const hostUnresolved = images.some(
(image) =>
image.kind === "report" &&
image.path !== undefined &&
owners.electron_host_paths?.includes(image.path) === true &&
!(image.implications ?? []).some((item) => item.subject === spec.subject),
);
const partialFuseSample =
nativeImagesMayBeUnread(scan) &&
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";
else if (outcomes.includes("unknown") || failed) status = "unknown";
else if (outcomes.includes("unknown") || failed || hostUnresolved)
status = "unknown";
else if (outcomes.length > 0 && outcomes.every((item) => item === spec.safe))
status = partialFuseSample ? "unknown" : "not_observed";
else if (owners.electron_family === "not_a_directory")