From a1919fe276de4993b3d69d7d258c0cfa2d6ec593 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Mon, 2 Nov 2026 08:04:45 -0800 Subject: [PATCH] fix(audit): keep a declared .map from reading as a linker map A script's sourceMappingURL=assets.map was dropped because the name already looked like a map. A malformed assets.map then settled as a linker map, so both source-map checks read not_observed and native.debug_companions reported an exposure the release does not ship. Keep a generic .map name as a declared target and read it as a JavaScript map, in either scan order, so it stays unresolved. --- src/artifacts/ReleaseExposureReader.test.ts | 26 +++++++++++++++++++++ src/artifacts/ReleaseExposureReader.ts | 13 ++++++++--- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/artifacts/ReleaseExposureReader.test.ts b/src/artifacts/ReleaseExposureReader.test.ts index cb9b69dfb6..a8b0a16183 100644 --- a/src/artifacts/ReleaseExposureReader.test.ts +++ b/src/artifacts/ReleaseExposureReader.test.ts @@ -2631,6 +2631,32 @@ describe("ReleaseExposureReader member identity", () => { }, ); + it.each([ + ["app.js", { embedded: 0, unparsed: 1, skipped: 0 }], + ["z.js", { embedded: 0, unparsed: 0, skipped: 1 }], + ])( + "keeps a malformed assets.map that %s declares unresolved", + async (script, expected) => { + const root = await createTestTempDirectory("rea-release-generic-map-"); + await writeFile( + join(root, script), + "const value = 1;\n//# sourceMappingURL=assets.map\n", + ); + await writeFile(join(root, "assets.map"), "not a map\n"); + const scanned = await inspect(root); + expect(scanned.source_maps).toMatchObject(expected); + expect(scanned.debug_companions.paths).toEqual([]); + }, + ); + + it("keeps an undeclared malformed assets.map a linker map", async () => { + const root = await createTestTempDirectory("rea-release-linker-map-"); + await writeFile(join(root, "assets.map"), "not a map\n"); + const scanned = await inspect(root); + expect(scanned.source_maps).toMatchObject({ embedded: 0, skipped: 0 }); + expect(scanned.debug_companions.paths).toHaveLength(1); + }); + it("counts a declared map target that never ships as skipped", async () => { const root = await createTestTempDirectory("rea-release-missing-map-"); await writeFile( diff --git a/src/artifacts/ReleaseExposureReader.ts b/src/artifacts/ReleaseExposureReader.ts index d8d3ce0d0c..33df02d5d1 100644 --- a/src/artifacts/ReleaseExposureReader.ts +++ b/src/artifacts/ReleaseExposureReader.ts @@ -987,8 +987,11 @@ const examineMapMember = async ( member: MemberView, ): Promise => { // A member a script names or an owner marks as a map is a JavaScript map - // whatever it is called. - const declared = member.logical.toLowerCase().endsWith(".map") + // whatever it is called. Only an undeclared `.map` may be a linker map. + const undeclared = + member.logical.toLowerCase().endsWith(".map") && + !state.mapTargets.has(memberLocationKey(member.location)); + const declared = undeclared ? member : { ...member, logical: `${member.logical}.js.map` }; const loaded = await examineMap(state, declared); @@ -1380,7 +1383,9 @@ const noteLocalMapReference = ( } if (/^[a-z][a-z0-9+.-]+:/iu.test(url)) return; const path = url.replace(/[?#].*$/su, ""); - if (path.length === 0 || isSourceMap(path, "file")) return; + // A generic `.map` name stays a target: only the declaration tells it from a + // linker map. + if (path.length === 0 || isJavaScriptSourceMapName(path)) return; let decoded: string; try { decoded = decodeURIComponent(path); @@ -1398,6 +1403,8 @@ const noteLocalMapReference = ( */ const noteMapTarget = (state: ScanState, location: string): void => { const target = memberLocationKey(location); + // A map read before its declaration may have settled as a linker map. + if (state.companionPaths.delete(location)) state.skippedMaps += 1; if (state.examinedMaps.has(target) || state.mapTargets.has(target)) return; if (state.examinedLocations.has(target)) { state.skippedMaps += 1;