From cc7b5af0777fc68ebba10ec061244729ebf2357d Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sat, 10 Oct 2026 00:13:24 +0800 Subject: [PATCH] fix(managed): bind graph facts to their source Evidence --- src/domain/managed/managedApplicationGraph.ts | 23 +- .../managedApplicationGraphNodeCoverage.ts | 11 - .../managed/managedApplicationGraphNodes.ts | 215 +++++++++++------- .../managedApplicationGraph.test.ts | 184 ++++++++++++--- 4 files changed, 309 insertions(+), 124 deletions(-) delete mode 100644 src/domain/managed/managedApplicationGraphNodeCoverage.ts diff --git a/src/domain/managed/managedApplicationGraph.ts b/src/domain/managed/managedApplicationGraph.ts index 57e845bf0..f99ebb47c 100644 --- a/src/domain/managed/managedApplicationGraph.ts +++ b/src/domain/managed/managedApplicationGraph.ts @@ -151,8 +151,8 @@ export const projectManagedApplicationGraph = ( }; const artifactNode = addArtifactNode(state); addArtifactIdentityNodes(state, artifactNode, parsed); - const memberCounts = addMemberNodes(state, artifactNode, parsed); - const boundaryCounts = addBoundaryNodes(state, artifactNode, parsed); + addMemberNodes(state, artifactNode, parsed); + addBoundaryNodes(state, artifactNode, parsed); const omissions = omittedCounts(parsed); const limitations = projectionLimitations(parsed, omissions); const graph = createJavaScriptApplicationGraph({ @@ -175,10 +175,16 @@ export const projectManagedApplicationGraph = ( summary: { graph_nodes: graph.nodes.length, graph_edges: graph.edges.length, - assemblies: parsed.artifact?.result.assembly === null ? 0 : 1, - modules: parsed.artifact?.result.module === null ? 0 : 1, - ...memberCounts, - ...boundaryCounts, + assemblies: countNodes(graph.nodes, "managed-assembly"), + modules: countNodes(graph.nodes, "managed-module"), + types: countNodes(graph.nodes, "managed-type"), + methods: countNodes(graph.nodes, "managed-method"), + fields: countNodes(graph.nodes, "managed-field"), + pinvoke_imports: countNodes(graph.nodes, "managed-pinvoke-import"), + native_implementations: countNodes( + graph.nodes, + "managed-native-implementation", + ), }, graph, coverage: managedGraphResultCoverage(omissions), @@ -191,6 +197,11 @@ export const projectManagedApplicationGraph = ( }); }; +const countNodes = ( + nodes: readonly ApplicationNode[], + kind: ApplicationNode["kind"], +): number => nodes.filter((node) => node.kind === kind).length; + const parseManagedInputs = ( input: ProjectManagedApplicationGraphInput, ): ParsedManagedGraphInput => ({ diff --git a/src/domain/managed/managedApplicationGraphNodeCoverage.ts b/src/domain/managed/managedApplicationGraphNodeCoverage.ts deleted file mode 100644 index 437f1d7f3..000000000 --- a/src/domain/managed/managedApplicationGraphNodeCoverage.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { ParsedManagedGraphInput } from "./managedApplicationGraph.js"; - -/** Derive module coverage from the best available managed evidence source. */ -export const moduleCoverageState = ( - parsed: ParsedManagedGraphInput, -): "complete" | "partial" | "unavailable" => - parsed.artifact !== null && parsed.artifact.result.module !== null - ? parsed.artifact.result.coverage.state - : parsed.members !== null && parsed.members.result.module !== null - ? parsed.members.result.coverage.state - : (parsed.boundaries?.result.coverage.state ?? "complete"); diff --git a/src/domain/managed/managedApplicationGraphNodes.ts b/src/domain/managed/managedApplicationGraphNodes.ts index bd5a72dba..cd631d018 100644 --- a/src/domain/managed/managedApplicationGraphNodes.ts +++ b/src/domain/managed/managedApplicationGraphNodes.ts @@ -6,7 +6,9 @@ import { import type { ApplicationGraphEvidence } from "../javascript/javascriptApplicationEvidenceSchemas.js"; import type { ApplicationNode } from "../javascript/javascriptApplicationGraphSchemas.js"; import { managedSourceCoverage } from "./managedApplicationGraphCoverage.js"; +import type { Evidence } from "../evidence.js"; import { + type ManagedArtifactInspection, type ManagedMemberInspection, type ManagedNativeBoundaryInspection, } from "./managedArtifact.js"; @@ -14,7 +16,6 @@ import type { GraphBuildState, ParsedManagedGraphInput, } from "./managedApplicationGraph.js"; -import { moduleCoverageState } from "./managedApplicationGraphNodeCoverage.js"; type ManagedMethod = ManagedMemberInspection["methods"][number]; type ManagedField = ManagedMemberInspection["fields"][number]; type ManagedType = ManagedMemberInspection["types"][number]; @@ -44,7 +45,7 @@ export const addArtifactNode = (state: GraphBuildState): ApplicationNode => { format: "pe", source: "managed-application-graph", }, - evidence: managedEvidence(state, "project-managed-artifact"), + evidence: projectedManagedEvidence(state, "project-managed-artifact"), }, ], }); @@ -58,47 +59,48 @@ export const addArtifactIdentityNodes = ( artifactNode: ApplicationNode, parsed: ParsedManagedGraphInput, ): void => { - const artifact = parsed.artifact?.result; - const artifactCoverage = managedSourceCoverage( - parsed.artifact?.result.coverage.state ?? "complete", - ); - if (artifact?.assembly !== undefined && artifact.assembly !== null) { + const artifactInput = parsed.artifact; + if (artifactInput !== null && artifactInput.result.assembly !== null) { + const assemblyFacts = artifactInput.result.assembly; + const artifactEvidence = sourceManagedEvidence( + state, + artifactInput.evidence, + artifactInput.result.coverage.state, + ); const assembly = createJavaScriptApplicationNode({ kind: "managed-assembly", identity: artifactLocalIdentity( state.artifactSha256, "managed-assembly", - artifact.assembly.token, + assemblyFacts.token, ), observations: [ { - label: artifact.assembly.name, + label: assemblyFacts.name, properties: { - name: artifact.assembly.name, - version: artifact.assembly.version, - culture: artifact.assembly.culture, - public_key_kind: artifact.assembly.public_key.kind, + name: assemblyFacts.name, + version: assemblyFacts.version, + culture: assemblyFacts.culture, + public_key_kind: assemblyFacts.public_key.kind, }, - evidence: managedEvidence( - state, - "inspect_managed_artifact", - artifactCoverage, - ), + evidence: artifactEvidence, }, ], }); state.nodes.push(assembly); addContainsEdge(state, artifactNode, assembly, { kind: "declares-managed-assembly", - coverage: artifactCoverage, + coverage: artifactEvidence.coverage, }); } - const module = - artifact?.module ?? - parsed.members?.result.module ?? - parsed.boundaries?.result.module; - if (module !== undefined && module !== null) { - const moduleCoverage = managedSourceCoverage(moduleCoverageState(parsed)); + const moduleSource = selectModuleSource(parsed); + if (moduleSource !== null) { + const { module } = moduleSource; + const moduleEvidence = sourceManagedEvidence( + state, + moduleSource.evidence, + moduleSource.coverageState, + ); const moduleNode = createJavaScriptApplicationNode({ kind: "managed-module", identity: artifactLocalIdentity( @@ -115,36 +117,68 @@ export const addArtifactIdentityNodes = ( generation: module.generation, token: module.token, }, - evidence: managedEvidence( - state, - "inspect_managed_artifact", - moduleCoverage, - ), + evidence: moduleEvidence, }, ], }); state.nodes.push(moduleNode); addContainsEdge(state, artifactNode, moduleNode, { kind: "declares-managed-module", - coverage: moduleCoverage, + coverage: moduleEvidence.coverage, }); } }; +interface ManagedModuleSource { + readonly module: NonNullable; + readonly evidence: Evidence; + readonly coverageState: ManagedArtifactInspection["coverage"]["state"]; +} + +const selectModuleSource = ( + parsed: ParsedManagedGraphInput, +): ManagedModuleSource | null => { + if (parsed.artifact?.result.module != null) + return { + module: parsed.artifact.result.module, + evidence: parsed.artifact.evidence, + coverageState: parsed.artifact.result.coverage.state, + }; + if (parsed.members?.result.module != null) + return { + module: parsed.members.result.module, + evidence: parsed.members.evidence, + coverageState: parsed.members.result.coverage.state, + }; + if (parsed.boundaries?.result.module != null) + return { + module: parsed.boundaries.result.module, + evidence: parsed.boundaries.evidence, + coverageState: parsed.boundaries.result.coverage.state, + }; + return null; +}; + /** Add managed type, method, and field nodes from member evidence. */ export const addMemberNodes = ( state: GraphBuildState, artifactNode: ApplicationNode, parsed: ParsedManagedGraphInput, -) => { - const members = parsed.members?.result; - if (members === undefined) return { types: 0, methods: 0, fields: 0 }; +): void => { + const memberInput = parsed.members; + if (memberInput === null) return; + const members = memberInput.result; const { types, methods, fields } = members; const typeCoverage = managedSourceCoverage(members.coverage.state); const methodCoverage = managedSourceCoverage(members.coverage.state); const fieldCoverage = managedSourceCoverage(members.coverage.state); + const sourceEvidence = sourceManagedEvidence( + state, + memberInput.evidence, + members.coverage.state, + ); for (const type of types) { - const node = typeNode(state, type, typeCoverage); + const node = typeNode(state, type, sourceEvidence); state.nodes.push(node); state.typeNodes.set(type.token, node); addContainsEdge(state, artifactNode, node, { @@ -153,7 +187,7 @@ export const addMemberNodes = ( }); } for (const method of methods) { - const node = methodNode(state, method, methodCoverage); + const node = methodNode(state, method, sourceEvidence); state.nodes.push(node); state.methodNodes.set(method.token, node); const owner = @@ -166,7 +200,7 @@ export const addMemberNodes = ( }); } for (const field of fields) { - const node = fieldNode(state, field, fieldCoverage); + const node = fieldNode(state, field, sourceEvidence); state.nodes.push(node); state.fieldNodes.set(field.token, node); const owner = @@ -178,18 +212,13 @@ export const addMemberNodes = ( coverage: fieldCoverage, }); } - return { - types: types.length, - methods: methods.length, - fields: fields.length, - }; }; /** Build a managed type application node. */ const typeNode = ( state: GraphBuildState, type: ManagedType, - coverage: ApplicationGraphEvidence["coverage"], + evidence: ApplicationGraphEvidence, ): ApplicationNode => createJavaScriptApplicationNode({ kind: "managed-type", @@ -209,7 +238,7 @@ const typeNode = ( flags: type.flags, extends_token: type.extends_token, }, - evidence: managedEvidence(state, "inspect_managed_members", coverage), + evidence, }, ], }); @@ -218,7 +247,7 @@ const typeNode = ( const methodNode = ( state: GraphBuildState, method: ManagedMethod, - coverage: ApplicationGraphEvidence["coverage"], + evidence: ApplicationGraphEvidence, ): ApplicationNode => createJavaScriptApplicationNode({ kind: "managed-method", @@ -245,7 +274,7 @@ const methodNode = ( normalized_il_sha256: method.body.normalized_il_sha256, il_size: method.body.il_size, }, - evidence: managedEvidence(state, "inspect_managed_members", coverage), + evidence, }, ], }); @@ -254,7 +283,7 @@ const methodNode = ( const fieldNode = ( state: GraphBuildState, field: ManagedField, - coverage: ApplicationGraphEvidence["coverage"], + evidence: ApplicationGraphEvidence, ): ApplicationNode => createJavaScriptApplicationNode({ kind: "managed-field", @@ -278,7 +307,7 @@ const fieldNode = ( signature_sha256: field.signature.raw_sha256, signature_status: field.signature.parse_status, }, - evidence: managedEvidence(state, "inspect_managed_members", coverage), + evidence, }, ], }); @@ -288,23 +317,33 @@ export const addBoundaryNodes = ( state: GraphBuildState, artifactNode: ApplicationNode, parsed: ParsedManagedGraphInput, -) => { - const boundaries = parsed.boundaries?.result; - if (boundaries === undefined) - return { pinvoke_imports: 0, native_implementations: 0 }; +): void => { + const boundaryInput = parsed.boundaries; + if (boundaryInput === null) return; + const boundaries = boundaryInput.result; const { pinvoke_imports: pinvokes, native_implementations: implementations } = boundaries; const pinvokeCoverage = managedSourceCoverage(boundaries.coverage.state); const implementationCoverage = managedSourceCoverage( boundaries.coverage.state, ); + const sourceEvidence = sourceManagedEvidence( + state, + boundaryInput.evidence, + boundaries.coverage.state, + ); for (const pinvoke of pinvokes) { - const node = pinvokeNode(state, pinvoke, pinvokeCoverage); + const node = pinvokeNode(state, pinvoke, sourceEvidence); state.nodes.push(node); - const owner = + const method = pinvoke.member_token === null - ? artifactNode - : (state.methodNodes.get(pinvoke.member_token) ?? artifactNode); + ? undefined + : state.methodNodes.get(pinvoke.member_token); + const memberEvidence = + method === undefined || parsed.members === null + ? undefined + : parsed.members.evidence; + const owner = method ?? artifactNode; state.edges.push( createJavaScriptApplicationEdge({ source_node_id: owner.node_id, @@ -315,10 +354,11 @@ export const addBoundaryNodes = ( import_name: pinvoke.import_name, import_scope_name: pinvoke.import_scope_name, }, - evidence: managedEvidence( + evidence: pinvokeAssociationEvidence( state, - "inspect_managed_native_boundaries", + boundaryInput.evidence, pinvokeCoverage, + memberEvidence, ), }), ); @@ -327,7 +367,7 @@ export const addBoundaryNodes = ( const node = nativeImplementationNode( state, implementation, - implementationCoverage, + sourceEvidence, ); state.nodes.push(node); const owner = state.methodNodes.get(implementation.token) ?? artifactNode; @@ -336,17 +376,13 @@ export const addBoundaryNodes = ( coverage: implementationCoverage, }); } - return { - pinvoke_imports: pinvokes.length, - native_implementations: implementations.length, - }; }; /** Build a managed P/Invoke import application node. */ const pinvokeNode = ( state: GraphBuildState, pinvoke: PinvokeImport, - coverage: ApplicationGraphEvidence["coverage"], + evidence: ApplicationGraphEvidence, ): ApplicationNode => createJavaScriptApplicationNode({ kind: "managed-pinvoke-import", @@ -368,11 +404,7 @@ const pinvokeNode = ( call_convention: pinvoke.call_convention, verification: pinvoke.verification, }, - evidence: managedEvidence( - state, - "inspect_managed_native_boundaries", - coverage, - ), + evidence, }, ], }); @@ -381,7 +413,7 @@ const pinvokeNode = ( const nativeImplementationNode = ( state: GraphBuildState, implementation: NativeImplementation, - coverage: ApplicationGraphEvidence["coverage"], + evidence: ApplicationGraphEvidence, ): ApplicationNode => createJavaScriptApplicationNode({ kind: "managed-native-implementation", @@ -403,11 +435,7 @@ const nativeImplementationNode = ( boundary_kind: implementation.boundary_kind, body_interpretation: implementation.body_interpretation, }, - evidence: managedEvidence( - state, - "inspect_managed_native_boundaries", - coverage, - ), + evidence, }, ], }); @@ -425,7 +453,7 @@ const addContainsEdge = ( target_node_id: target.node_id, relation: "contains", properties: { kind: options.kind }, - evidence: managedEvidence( + evidence: projectedManagedEvidence( state, "project-managed-contains", options.coverage, @@ -448,12 +476,43 @@ const artifactLocalIdentity = ( }); /** Build managed static-analysis evidence for a graph observation. */ -const managedEvidence = ( +const sourceManagedEvidence = ( + state: GraphBuildState, + source: Evidence, + coverageState: ManagedArtifactInspection["coverage"]["state"], +): ApplicationGraphEvidence => + createManagedEvidence( + state, + source.operation, + managedSourceCoverage(coverageState), + [source.evidence_id], + ); + +const projectedManagedEvidence = ( state: GraphBuildState, operation: string, coverage: ApplicationGraphEvidence["coverage"] = managedSourceCoverage( "complete", ), +): ApplicationGraphEvidence => + createManagedEvidence(state, operation, coverage, state.evidenceLinks); + +const pinvokeAssociationEvidence = ( + state: GraphBuildState, + boundaryEvidence: Evidence, + coverage: ApplicationGraphEvidence["coverage"], + memberEvidence: Evidence | undefined, +): ApplicationGraphEvidence => + createManagedEvidence(state, boundaryEvidence.operation, coverage, [ + boundaryEvidence.evidence_id, + ...(memberEvidence === undefined ? [] : [memberEvidence.evidence_id]), + ]); + +const createManagedEvidence = ( + state: GraphBuildState, + operation: string, + coverage: ApplicationGraphEvidence["coverage"], + evidenceIds: readonly string[], ): ApplicationGraphEvidence => ({ authority: "managed-static-analysis", state: "observed", @@ -483,7 +542,7 @@ const managedEvidence = ( : [ "The source managed Evidence slice is incomplete; unreturned or unavailable items remain unobserved.", ], - evidence_ids: state.evidenceLinks, + evidence_ids: [...evidenceIds], }); /** Sanitize an artifact path into a managed graph location. */ diff --git a/tests/composition/analysis-sessions/managedApplicationGraph.test.ts b/tests/composition/analysis-sessions/managedApplicationGraph.test.ts index 39d31b07b..22eed0b21 100644 --- a/tests/composition/analysis-sessions/managedApplicationGraph.test.ts +++ b/tests/composition/analysis-sessions/managedApplicationGraph.test.ts @@ -16,43 +16,51 @@ import { managedPeFixtureTarget, } from "../../../src/dotnet/ManagedPe.fixture.js"; -describe("managed application graph projection", () => { - it("projects managed metadata and native declarations into authenticated graph Evidence", () => { - const bytes = buildManagedPeFixture({ - pinvoke: { - moduleName: "user32.dll", - importName: "MessageBoxW", - mappingFlags: 0x0345, - }, - }); - const binary = managedPeFixtureTarget(bytes, "/fixture/ManagedInterop.exe"); - const managedArtifact = inspectManagedArtifactBytes(bytes, binary); - const members = inspectManagedMembersBytes(bytes, binary); - const boundaries = inspectManagedNativeBoundariesBytes(bytes, binary); - const artifactEvidence = createEvidence(binary, MANAGED_STATIC_PROVIDER, { +const createManagedInteropEvidence = () => { + const bytes = buildManagedPeFixture({ + pinvoke: { + moduleName: "user32.dll", + importName: "MessageBoxW", + mappingFlags: 0x0345, + }, + }); + const binary = managedPeFixtureTarget(bytes, "/fixture/ManagedInterop.exe"); + const managedArtifact = inspectManagedArtifactBytes(bytes, binary); + const members = inspectManagedMembersBytes(bytes, binary); + const boundaries = inspectManagedNativeBoundariesBytes(bytes, binary); + + return { + artifactEvidence: createEvidence(binary, MANAGED_STATIC_PROVIDER, { operation: "inspect_managed_artifact", parameters: {}, result: managedArtifact, rawResult: null, limitations: managedArtifact.limitations, locations: [{ kind: "artifact-path", path: binary.path }], - }); - const memberEvidence = createEvidence(binary, MANAGED_STATIC_PROVIDER, { + }), + memberEvidence: createEvidence(binary, MANAGED_STATIC_PROVIDER, { operation: "inspect_managed_members", parameters: {}, result: members, rawResult: null, limitations: members.limitations, locations: [{ kind: "artifact-path", path: binary.path }], - }); - const boundaryEvidence = createEvidence(binary, MANAGED_STATIC_PROVIDER, { + }), + boundaryEvidence: createEvidence(binary, MANAGED_STATIC_PROVIDER, { operation: "inspect_managed_native_boundaries", parameters: {}, result: boundaries, rawResult: null, limitations: boundaries.limitations, locations: [{ kind: "artifact-path", path: binary.path }], - }); + }), + }; +}; + +describe("managed application graph projection", () => { + it("projects managed metadata and native declarations into authenticated graph Evidence", () => { + const { artifactEvidence, memberEvidence, boundaryEvidence } = + createManagedInteropEvidence(); const evidence = projectManagedApplicationGraphEvidence({ managed_artifact: artifactEvidence, @@ -103,21 +111,51 @@ describe("managed application graph projection", () => { "managed-pinvoke-import", ]), ); + const allEvidenceIds = [ + artifactEvidence.evidence_id, + memberEvidence.evidence_id, + boundaryEvidence.evidence_id, + ].sort(); + const observedEvidenceIds = (kind: string): readonly string[] => { + const observation = graph.nodes.find((node) => node.kind === kind) + ?.observations[0]; + if (observation === undefined) + throw new Error(`Expected ${kind} observation`); + return observation.evidence.evidence_ids; + }; + expect(observedEvidenceIds("artifact")).toEqual(allEvidenceIds); + expect(observedEvidenceIds("managed-assembly")).toEqual([ + artifactEvidence.evidence_id, + ]); + expect(observedEvidenceIds("managed-module")).toEqual([ + artifactEvidence.evidence_id, + ]); + for (const kind of ["managed-type", "managed-method", "managed-field"]) + expect(observedEvidenceIds(kind)).toEqual([memberEvidence.evidence_id]); + expect(observedEvidenceIds("managed-pinvoke-import")).toEqual([ + boundaryEvidence.evidence_id, + ]); + for (const edge of graph.edges.filter( + ({ relation }) => relation === "contains", + )) + expect(edge.evidence.evidence_ids).toEqual(allEvidenceIds); const method = graph.nodes.find( ({ kind, observations }) => kind === "managed-method" && observations[0]?.label === "Fixture.Program.Main", ); expect(method).toBeDefined(); - expect( - graph.edges.some( - ({ source_node_id, relation, target_node_id }) => - source_node_id === method?.node_id && - relation === "imports" && - graph.nodes.find(({ node_id: id }) => id === target_node_id)?.kind === - "managed-pinvoke-import", - ), - ).toBe(true); + const importEdge = graph.edges.find( + ({ source_node_id, relation, target_node_id }) => + source_node_id === method?.node_id && + relation === "imports" && + graph.nodes.find(({ node_id: id }) => id === target_node_id)?.kind === + "managed-pinvoke-import", + ); + expect(importEdge).toBeDefined(); + expect(importEdge?.evidence.evidence_ids).toEqual( + [memberEvidence.evidence_id, boundaryEvidence.evidence_id].sort(), + ); expect( graph.nodes.flatMap(({ observations }) => observations.map(({ evidence }) => evidence.authority), @@ -167,7 +205,7 @@ describe("managed application graph request", () => { }); describe("managed application graph coverage", () => { - it("rejects managed Evidence digest mismatch and discloses missing artifact observations", () => { + it("rejects managed Evidence whose artifact digest differs from its subject", () => { const bytes = buildManagedPeFixture(); const binary = managedPeFixtureTarget(bytes, "/fixture/Mismatch.dll"); const members = inspectManagedMembersBytes(bytes, binary); @@ -191,7 +229,14 @@ describe("managed application graph coverage", () => { expect(JSON.stringify(rejected.error)).toContain( `Managed Evidence inspect_managed_members (${mismatched.evidence_id}) subject SHA-256 ${binary.sha256} does not match normalized artifact SHA-256 ${"b".repeat(64)}`, ); + }); +}); +describe("managed application graph identity facts", () => { + it("uses member Evidence for its module and reports only emitted identity nodes", () => { + const bytes = buildManagedPeFixture(); + const binary = managedPeFixtureTarget(bytes, "/fixture/MembersOnly.dll"); + const members = inspectManagedMembersBytes(bytes, binary); const aligned = createEvidence(binary, MANAGED_STATIC_PROVIDER, { operation: "inspect_managed_members", parameters: {}, @@ -204,11 +249,92 @@ describe("managed application graph coverage", () => { const result = managedApplicationGraphResultSchema.parse( parseEvidence(projected.value).normalized_result, ); + const graph = parseJavaScriptApplicationGraph(result.graph); + expect(result.summary).toMatchObject({ assemblies: 0, modules: 1 }); + expect( + graph.nodes.find(({ kind }) => kind === "managed-module") + ?.observations[0], + ).toMatchObject({ + evidence: { + extractor: { operation: "inspect_managed_members" }, + evidence_ids: [aligned.evidence_id], + }, + }); + expect(result.evidence_links).toEqual([aligned.evidence_id]); expect(result.limitations).toContain( "Managed artifact Evidence was not supplied; assembly identity observations are absent.", ); }); + it("binds a boundaries-only module node to the boundary Evidence that supplied it", () => { + const bytes = buildManagedPeFixture(); + const binary = managedPeFixtureTarget(bytes, "/fixture/BoundariesOnly.dll"); + const boundaries = inspectManagedNativeBoundariesBytes(bytes, binary); + const boundaryEvidence = createEvidence(binary, MANAGED_STATIC_PROVIDER, { + operation: "inspect_managed_native_boundaries", + parameters: {}, + result: boundaries, + rawResult: null, + limitations: boundaries.limitations, + locations: [{ kind: "artifact-path", path: binary.path }], + }); + + const projected = projectManagedApplicationGraphEvidence({ + managed_native_boundaries: boundaryEvidence, + }); + if (!projected.ok) throw projected.error; + const result = managedApplicationGraphResultSchema.parse( + parseEvidence(projected.value).normalized_result, + ); + const graph = parseJavaScriptApplicationGraph(result.graph); + expect(result.summary).toMatchObject({ assemblies: 0, modules: 1 }); + expect( + graph.nodes.find(({ kind }) => kind === "managed-module") + ?.observations[0], + ).toMatchObject({ + evidence: { + extractor: { operation: "inspect_managed_native_boundaries" }, + evidence_ids: [boundaryEvidence.evidence_id], + }, + }); + expect(result.evidence_links).toEqual([boundaryEvidence.evidence_id]); + }); + + it("reports no assembly or module when the inspector observes no identity rows", () => { + const bytes = buildManagedPeFixture({ + moduleRowCount: 0, + assemblyRowCount: 0, + }); + const binary = managedPeFixtureTarget(bytes, "/fixture/NoIdentityRows.exe"); + const inspection = inspectManagedArtifactBytes(bytes, binary); + expect(inspection).toMatchObject({ module: null, assembly: null }); + const artifactEvidence = createEvidence(binary, MANAGED_STATIC_PROVIDER, { + operation: "inspect_managed_artifact", + parameters: {}, + result: inspection, + rawResult: null, + limitations: inspection.limitations, + locations: [{ kind: "artifact-path", path: binary.path }], + }); + + const projected = projectManagedApplicationGraphEvidence({ + managed_artifact: artifactEvidence, + }); + if (!projected.ok) throw projected.error; + const result = managedApplicationGraphResultSchema.parse( + parseEvidence(projected.value).normalized_result, + ); + const graph = parseJavaScriptApplicationGraph(result.graph); + expect(result.summary).toMatchObject({ assemblies: 0, modules: 0 }); + expect( + graph.nodes.filter( + ({ kind }) => kind === "managed-assembly" || kind === "managed-module", + ), + ).toEqual([]); + }); +}); + +describe("managed application graph coverage", () => { it("preserves partial parser coverage in graph and per-fact coverage", () => { const bytes = buildManagedPeFixture(); const binary = managedPeFixtureTarget(bytes, "/fixture/ManagedInterop.exe");