From a05092726f1283d32b2afe761f430abd2de18386 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Fri, 16 Oct 2026 07:25:22 +0800 Subject: [PATCH] fix(go): distinguish dependency coverage from parse completeness Explain unknown dependency coverage when embedded metadata has no main-module record. Preserve decoded dependencies and distinguish GOPATH-style omissions from observed module-mode empty lists. Refs #4191, #4267 Co-authored-by: Y0UW1SH <29498139+Y0UW1SH@users.noreply.github.com> --- docs/go-binaries.md | 11 ++++- src/go/GoBinaryProvider.ts | 53 +++++++++++++--------- tests/acceptance/goBinaryCli.test.ts | 30 ++++++++++++ tests/boundary/go/goBinaryProvider.test.ts | 44 ++++++++++++++++++ 4 files changed, 115 insertions(+), 23 deletions(-) diff --git a/docs/go-binaries.md b/docs/go-binaries.md index 134a8c74d4..6088141c54 100644 --- a/docs/go-binaries.md +++ b/docs/go-binaries.md @@ -39,8 +39,8 @@ container format, architecture, word size and byte order. A recognized - `module.dependencies`, with the same fields and their original order; - `module.settings`, retaining ordered key/value entries and duplicate keys; - `module.unparsed_lines`, `module.unparsed_line_bytes_base64` and - `module.complete`, so unfamiliar, malformed, conflicting or undecodable records - do not disappear behind a complete result. + `module.complete`, which says whether every present module-text line was + parsed. It does not establish that the dependency inventory is complete. Missing checksums are `null`; empty fields and development/local replacement versions remain unchanged. Build settings such as `GOOS`, `GOARCH` or `vcs.revision` @@ -50,6 +50,13 @@ base64 while other decodable module records are still reported; the reader does not insert replacement characters or reject the entire artifact for those strings. +When no main-module (`mod`) record was decoded, `module.main` is `null` and +dependency coverage is unknown. Successfully decoded dependency records remain +visible. GOPATH-mode builds may contain only a package `path` and build settings; +their empty dependency array does not establish the absence of dependencies. +The result's limitations state this explicitly. A module-mode build with a main +module record and no dependency records has an observed empty dependency list. + ## Supported boundary The built-in TypeScript reader supports ELF, PE and thin Mach-O images with Go's diff --git a/src/go/GoBinaryProvider.ts b/src/go/GoBinaryProvider.ts index 9e2ac1f301..a399bfcb7c 100644 --- a/src/go/GoBinaryProvider.ts +++ b/src/go/GoBinaryProvider.ts @@ -17,6 +17,7 @@ import { import { goBinarySchema, type GoBinary, + type GoModuleMetadata, type InspectGoBinaryInput, } from "../domain/go/goBinary.js"; import { ProviderAdapterError } from "../domain/providerAdapterError.js"; @@ -138,6 +139,36 @@ const outputLimit = (path: string) => { maximum_output_bytes: OUTPUT_BYTES }, ); +/** Fixed reader boundaries plus unknowns left by the observed metadata. */ +const limitationsFor = ( + info: { go_version: string | null; module_text: string | null } | null, + module: GoModuleMetadata | null, +): string[] => [ + "Embedded build metadata is observed file content, not proof of compiler identity, dependency authenticity or runtime behavior.", + "Only Go build information is inspected; function names, source mappings, type metadata and decompiled code are not recovered.", + "Resource guards bound stable artifact reads and cumulative header searches to 256 MiB each, aggregate embedded strings to 1 MiB, structural table decoding to 16 MiB and complete metadata payloads to 8 MiB; these are not operating-system memory or CPU limits.", + ...(info !== null && (info.go_version === null || info.module_text === null) + ? [ + "Some embedded Go strings contain non-UTF8 bytes; undecodable text is null and exact source bytes are preserved as base64.", + ] + : []), + ...(info === null + ? [ + "No recognized Go build-info record was found in the inspected image; absent metadata does not establish that the binary is not Go.", + ] + : []), + ...(module !== null && module.main === null + ? [ + "Dependency coverage is unknown because no main-module record was decoded from the embedded module information, as in GOPATH-mode builds. Decoded dependency records remain visible; an empty list does not establish the absence of dependencies.", + ] + : []), + ...(module?.complete === false + ? [ + "Some embedded module-text records could not be parsed completely; their original lines are preserved and additional metadata remains unknown.", + ] + : []), +]; + /** Stable in-process metadata inspection without subprocesses, temporary files or Go installation. */ export class GoBinaryProvider implements GoBinaryPort { readonly identity = GO_BINARY_PROVIDER_IDENTITY; @@ -191,27 +222,7 @@ export class GoBinaryProvider implements GoBinaryPort { bytes: snapshot.bytes.length, }, build_info: info === null ? null : { ...info, module }, - limitations: [ - "Embedded build metadata is observed file content, not proof of compiler identity, dependency authenticity or runtime behavior.", - "Only Go build information is inspected; function names, source mappings, type metadata and decompiled code are not recovered.", - "Resource guards bound stable artifact reads and cumulative header searches to 256 MiB each, aggregate embedded strings to 1 MiB, structural table decoding to 16 MiB and complete metadata payloads to 8 MiB; these are not operating-system memory or CPU limits.", - ...(info !== null && - (info.go_version === null || info.module_text === null) - ? [ - "Some embedded Go strings contain non-UTF8 bytes; undecodable text is null and exact source bytes are preserved as base64.", - ] - : []), - ...(info === null - ? [ - "No recognized Go build-info record was found in the inspected image; absent metadata does not establish that the binary is not Go.", - ] - : []), - ...(module?.complete === false - ? [ - "Some embedded module-text records could not be parsed completely; their original lines are preserved and additional metadata remains unknown.", - ] - : []), - ], + limitations: limitationsFor(info, module), }; if (Buffer.byteLength(JSON.stringify(report), "utf8") > OUTPUT_BYTES) throw outputLimit(input.path); diff --git a/tests/acceptance/goBinaryCli.test.ts b/tests/acceptance/goBinaryCli.test.ts index 22cb9fed0c..0eb5889013 100644 --- a/tests/acceptance/goBinaryCli.test.ts +++ b/tests/acceptance/goBinaryCli.test.ts @@ -284,3 +284,33 @@ cliTest( ); }, ); + +cliTest( + "returns unknown dependency coverage for parse-complete GOPATH metadata in CLI Evidence", + async ({ cli }) => { + const moduleText = + "path\texample.com/tool/cmd/tool\n" + + "build\t-compiler=gc\n" + + "build\tGOOS=linux\n"; + const root = await createTestTempDirectory("rea-go-gopath-cli-"); + const path = join(root, "application.elf"); + await writeFile(path, createGoBinaryFixture({ moduleText }).bytes); + const result = await cli.run({ + arguments: ["inspect-go-binary", path, "--json"], + }); + expect(result.exitCode, result.stderr).toBe(0); + const evidence = toolContract("inspect_go_binary").outputSchema.parse( + result.json, + ); + expect(evidence.normalized_result.build_info?.module).toMatchObject({ + path: "example.com/tool/cmd/tool", + main: null, + dependencies: [], + complete: true, + unparsed_lines: [], + }); + expect(evidence.limitations.join(" ")).toContain( + "Dependency coverage is unknown", + ); + }, +); diff --git a/tests/boundary/go/goBinaryProvider.test.ts b/tests/boundary/go/goBinaryProvider.test.ts index 5a43cd8d28..7c9c9a5475 100644 --- a/tests/boundary/go/goBinaryProvider.test.ts +++ b/tests/boundary/go/goBinaryProvider.test.ts @@ -82,3 +82,47 @@ it.skipIf(process.platform === "win32")( if (!result.ok) expect(result.error).toBeInstanceOf(AnalysisInputError); }, ); + +it("reports a parse-complete GOPATH module text as unknown dependency coverage", async () => { + const moduleText = + "path\texample.com/tool/cmd/tool\n" + + "build\t-compiler=gc\n" + + "build\tGOOS=linux\n"; + const root = await createTestTempDirectory("rea-go-gopath-metadata-"); + const path = join(root, "application.elf"); + await writeFile(path, createGoBinaryFixture({ moduleText }).bytes); + const result = await new GoBinaryProvider().inspect({ path }); + if (!result.ok) throw result.error; + expect(result.value.build_info?.module_text).toBe(moduleText); + expect(result.value.build_info?.module).toMatchObject({ + path: "example.com/tool/cmd/tool", + main: null, + dependencies: [], + complete: true, + unparsed_lines: [], + }); + expect(result.value.limitations.join(" ")).toContain( + "Dependency coverage is unknown", + ); +}); + +it("keeps a module-mode build with no dependency records as an observed empty list", async () => { + const moduleText = + "path\texample.com/tool\n" + + "mod\texample.com/tool\t(devel)\t\n" + + "build\tGOOS=linux\n"; + const root = await createTestTempDirectory("rea-go-empty-module-deps-"); + const path = join(root, "application.elf"); + await writeFile(path, createGoBinaryFixture({ moduleText }).bytes); + const result = await new GoBinaryProvider().inspect({ path }); + if (!result.ok) throw result.error; + expect(result.value.build_info?.module_text).toBe(moduleText); + expect(result.value.build_info?.module).toMatchObject({ + main: { path: "example.com/tool", version: "(devel)" }, + dependencies: [], + complete: true, + }); + expect(result.value.limitations.join(" ")).not.toContain( + "Dependency coverage is unknown", + ); +});