diff --git a/docs/managed-code-analysis.md b/docs/managed-code-analysis.md index eb2eeee18..72c55e6da 100644 --- a/docs/managed-code-analysis.md +++ b/docs/managed-code-analysis.md @@ -38,6 +38,12 @@ Inspect separately obtained components explicitly and preserve their identities. A valid implementation marker establishes a candidate native boundary, not recovered native semantics or runtime behavior. +An assembly targeting [.NET Standard](https://learn.microsoft.com/en-us/dotnet/standard/net-standard) +declares API compatibility across multiple .NET implementations. REA preserves +that target in `target_frameworks` and leaves `runtime_family` as `unknown` +unless independent markers identify a runtime family, such as a ReadyToRun +signature. + ### Resource admission The static provider admits regular input files up to 128 MiB before retaining diff --git a/src/dotnet/ManagedArtifactInspector.ts b/src/dotnet/ManagedArtifactInspector.ts index ca4824563..cd0d627b0 100644 --- a/src/dotnet/ManagedArtifactInspector.ts +++ b/src/dotnet/ManagedArtifactInspector.ts @@ -204,10 +204,7 @@ const runtimeFamilyFor = ( file_offset: metadata.table(12)?.offset ?? null, }); } else if ( - frameworks.some( - (value) => - value.startsWith(".NETCoreApp,") || value.startsWith(".NETStandard,"), - ) || + frameworks.some((value) => value.startsWith(".NETCoreApp,")) || cli.readyToRunSignature ) { runtimeFamily = "modern-dotnet"; @@ -226,6 +223,13 @@ const runtimeFamilyFor = ( ) : (metadata.table(12)?.offset ?? null), }); + } else if (frameworks.some((value) => value.startsWith(".NETStandard,"))) { + evidence.push({ + code: "target-framework-observed", + detail: + "An assembly-level TargetFrameworkAttribute names .NET Standard, an API specification that does not identify a runtime family.", + file_offset: metadata.table(12)?.offset ?? null, + }); } return { runtime_family: runtimeFamily, evidence }; }; diff --git a/tests/acceptance/managedCliOutput.test.ts b/tests/acceptance/managedCliOutput.test.ts index be7506c85..a336a2a06 100644 --- a/tests/acceptance/managedCliOutput.test.ts +++ b/tests/acceptance/managedCliOutput.test.ts @@ -7,6 +7,56 @@ import { buildManagedPeFixture } from "../../src/dotnet/ManagedPe.fixture.js"; import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js"; import { cliTest } from "../support/cli/cliFixture.js"; +cliTest( + "managed CLI separates .NET Standard API targets from runtime evidence", + async ({ cli }) => { + const root = await createTestTempDirectory("rea-managed-runtime-family-"); + for (const [name, targetFramework, readyToRun, runtimeFamily] of [ + ["standard", ".NETStandard,Version=v2.0", false, "unknown"], + ["core", ".NETCoreApp,Version=v8.0", false, "modern-dotnet"], + ["standard-r2r", ".NETStandard,Version=v2.0", true, "modern-dotnet"], + ] as const) { + const path = join(root, `${name}.dll`); + await writeFile( + path, + buildManagedPeFixture({ targetFramework, readyToRun }), + ); + const result = await cli.run({ + arguments: ["inspect-managed-artifact", path, "--json"], + }); + expect(result.exitCode).toBe(0); + expect(result.json).toMatchObject({ + operation: "inspect_managed_artifact", + subject: { local_path: path }, + normalized_result: { + classification: { + status: "managed", + runtime_family: runtimeFamily, + implementation: readyToRun ? "cil-and-ready-to-run" : "cil", + evidence: expect.arrayContaining([ + expect.objectContaining({ + code: readyToRun + ? "ready-to-run-signature-observed" + : "target-framework-observed", + detail: expect.stringContaining( + readyToRun + ? "ReadyToRun signature" + : name === "standard" + ? "does not identify a runtime family" + : "names modern .NET", + ), + file_offset: expect.any(Number), + }), + ]), + }, + target_frameworks: [targetFramework], + coverage: { state: "complete", issues: [] }, + }, + }); + } + }, +); + cliTest( "managed CLI preserves evidence, filters, envelopes, and bounded token counts", async ({ cli }) => {