fix(managed): preserve literal library names during native matching
This commit is contained in:
1 parent
5eb0049a9f
commit
81b70bc358
2 files changed
+176
-145
No files matched your search
@@ -109,9 +109,17 @@ const sameSymbolName = (expected: string, native: NativeSymbol): boolean =>
|
||||
const normalizeModule = (value: string): string =>
|
||||
basename(value)
|
||||
.toLowerCase()
|
||||
.replace(/^lib/u, "")
|
||||
.replace(/\.(dll|dylib|so|node|exe)$/u, "");
|
||||
|
||||
const nativeModuleIdentities = (value: string): ReadonlySet<string> => {
|
||||
const filename = basename(value).toLowerCase();
|
||||
const stem = normalizeModule(filename);
|
||||
const identities = new Set([stem]);
|
||||
if (/\.(dylib|so)$/u.test(filename) && stem.startsWith("lib"))
|
||||
identities.add(stem.slice("lib".length));
|
||||
return identities;
|
||||
};
|
||||
|
||||
const moduleCompatible = (
|
||||
scope: string | null,
|
||||
symbol: NativeSymbol,
|
||||
@@ -120,8 +128,7 @@ const moduleCompatible = (
|
||||
const expected = normalizeModule(scope);
|
||||
return [symbol.module_name, symbol.module_path]
|
||||
.filter((value): value is string => value !== null)
|
||||
.map(normalizeModule)
|
||||
.some((value) => value === expected);
|
||||
.some((value) => nativeModuleIdentities(value).has(expected));
|
||||
};
|
||||
|
||||
const candidateNames = (
|
||||
|
||||
@@ -16,7 +16,7 @@ const exampleInput = () =>
|
||||
MANAGED_NATIVE_VERIFICATION_EXAMPLE,
|
||||
);
|
||||
|
||||
const nativeEvidenceWithExports = (names: readonly string[]) => {
|
||||
const nativeEvidenceWithExports = (names: readonly string[], path?: string) => {
|
||||
const input = exampleInput();
|
||||
const native = input.native_observations[0];
|
||||
if (native === undefined || native.subject === null)
|
||||
@@ -24,7 +24,7 @@ const nativeEvidenceWithExports = (names: readonly string[]) => {
|
||||
const macho = inspectMachoSchema.parse(native.normalized_result);
|
||||
return createEvidence(
|
||||
{
|
||||
path: native.subject.local_path,
|
||||
path: path ?? native.subject.local_path,
|
||||
sha256: native.subject.digest.sha256,
|
||||
format: native.subject.format,
|
||||
...(native.subject.architecture === null
|
||||
@@ -60,6 +60,29 @@ const nativeEvidenceWithExports = (names: readonly string[]) => {
|
||||
);
|
||||
};
|
||||
|
||||
const inputWithImportScope = (scope: string) => {
|
||||
const input = exampleInput();
|
||||
const managed = input.managed_boundaries;
|
||||
const normalized = managedNativeBoundaryInspectionSchema.parse(
|
||||
managed.normalized_result,
|
||||
);
|
||||
return {
|
||||
...input,
|
||||
managed_boundaries: createEvidence(undefined, managed.provider, {
|
||||
operation: managed.operation,
|
||||
parameters: managed.parameters,
|
||||
result: {
|
||||
...normalized,
|
||||
pinvoke_imports: normalized.pinvoke_imports.map((item) => ({
|
||||
...item,
|
||||
import_scope_name: scope,
|
||||
})),
|
||||
},
|
||||
rawResult: null,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
describe("managed/native boundary Evidence identity", () => {
|
||||
it("rejects managed boundary Evidence whose subject digest differs from its artifact", () => {
|
||||
const input = exampleInput();
|
||||
@@ -108,165 +131,166 @@ describe("managed/native boundary Evidence identity", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("managed/native boundary verification", () => {
|
||||
it("verifies a P/Invoke declaration against native export Evidence", () => {
|
||||
const result = verifyManagedNativeBoundaries(exampleInput());
|
||||
it("verifies a P/Invoke declaration against native export Evidence", () => {
|
||||
const result = verifyManagedNativeBoundaries(exampleInput());
|
||||
|
||||
expect(result).toMatchObject({
|
||||
algorithm: {
|
||||
name: "rea-managed-native-verification",
|
||||
token_to_address_mapping: "not-inferred",
|
||||
},
|
||||
managed_boundary: {
|
||||
artifact_sha256: "6".repeat(64),
|
||||
mvid: "00112233-4455-4677-8899-aabbccddeeff",
|
||||
pinvoke_imports_total: 1,
|
||||
},
|
||||
native_observations: {
|
||||
total: 1,
|
||||
accepted: 1,
|
||||
unsupported: 0,
|
||||
symbols: 1,
|
||||
},
|
||||
summary: {
|
||||
verified: 1,
|
||||
inferred: 0,
|
||||
unresolved: 0,
|
||||
contradicted: 0,
|
||||
},
|
||||
pinvoke_imports: [
|
||||
{
|
||||
managed: {
|
||||
import_name: "open_native",
|
||||
import_scope_name: "nativehelper.dll",
|
||||
declaration_verification: "managed-declaration-only",
|
||||
},
|
||||
status: "verified",
|
||||
basis: "exact-export-name",
|
||||
confidence: "observed",
|
||||
matched_native: {
|
||||
name: "open_native",
|
||||
source: "macho-export",
|
||||
},
|
||||
expect(result).toMatchObject({
|
||||
managed_boundary: {
|
||||
artifact_sha256: "6".repeat(64),
|
||||
mvid: "00112233-4455-4677-8899-aabbccddeeff",
|
||||
pinvoke_imports_total: 1,
|
||||
},
|
||||
summary: { verified: 1 },
|
||||
pinvoke_imports: [
|
||||
{
|
||||
managed: {
|
||||
import_name: "open_native",
|
||||
import_scope_name: "nativehelper.dll",
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(result.verification_id).toMatch(/^mnv_[a-f0-9]{64}$/u);
|
||||
expect(result.limitations.join(" ")).toContain(
|
||||
"does not prove CLR binding",
|
||||
);
|
||||
});
|
||||
|
||||
it("reports module mismatch as a contradiction within supplied Evidence", () => {
|
||||
const input = exampleInput();
|
||||
const native = input.native_observations[0];
|
||||
if (native === undefined || native.subject === null)
|
||||
throw new Error("missing native example");
|
||||
const mismatched = createEvidence(
|
||||
{
|
||||
path: "/examples/other.dll",
|
||||
sha256: native.subject.digest.sha256,
|
||||
format: "pe",
|
||||
...(native.subject.architecture === null
|
||||
? {}
|
||||
: { architecture: native.subject.architecture }),
|
||||
status: "verified",
|
||||
basis: "exact-export-name",
|
||||
confidence: "observed",
|
||||
matched_native: { name: "open_native" },
|
||||
},
|
||||
native.provider,
|
||||
{
|
||||
operation: native.operation,
|
||||
parameters: native.parameters,
|
||||
result: native.normalized_result,
|
||||
rawResult: native.raw_result,
|
||||
confidence: native.confidence,
|
||||
authority: native.authority,
|
||||
environment: native.environment,
|
||||
limitations: native.limitations,
|
||||
locations: native.locations,
|
||||
evidenceLinks: native.evidence_links,
|
||||
},
|
||||
);
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
const result = verifyManagedNativeBoundaries({
|
||||
...input,
|
||||
native_observations: [mismatched],
|
||||
});
|
||||
it("reports module mismatch as a contradiction within supplied Evidence", () => {
|
||||
const input = exampleInput();
|
||||
const native = input.native_observations[0];
|
||||
if (native === undefined || native.subject === null)
|
||||
throw new Error("missing native example");
|
||||
const mismatched = createEvidence(
|
||||
{
|
||||
path: "/examples/other.dll",
|
||||
sha256: native.subject.digest.sha256,
|
||||
format: "pe",
|
||||
...(native.subject.architecture === null
|
||||
? {}
|
||||
: { architecture: native.subject.architecture }),
|
||||
},
|
||||
native.provider,
|
||||
{
|
||||
operation: native.operation,
|
||||
parameters: native.parameters,
|
||||
result: native.normalized_result,
|
||||
rawResult: native.raw_result,
|
||||
confidence: native.confidence,
|
||||
authority: native.authority,
|
||||
environment: native.environment,
|
||||
limitations: native.limitations,
|
||||
locations: native.locations,
|
||||
evidenceLinks: native.evidence_links,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.summary).toMatchObject({
|
||||
verified: 0,
|
||||
contradicted: 1,
|
||||
});
|
||||
expect(result.pinvoke_imports[0]).toMatchObject({
|
||||
status: "contradicted",
|
||||
basis: "module-mismatch",
|
||||
});
|
||||
const result = verifyManagedNativeBoundaries({
|
||||
...input,
|
||||
native_observations: [mismatched],
|
||||
});
|
||||
|
||||
it("treats supported native Evidence with no symbols as unresolved", () => {
|
||||
const result = verifyManagedNativeBoundaries({
|
||||
...exampleInput(),
|
||||
native_observations: [nativeEvidenceWithExports([])],
|
||||
});
|
||||
expect(result.summary).toMatchObject({
|
||||
verified: 0,
|
||||
contradicted: 1,
|
||||
});
|
||||
expect(result.pinvoke_imports[0]).toMatchObject({
|
||||
status: "contradicted",
|
||||
basis: "module-mismatch",
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.native_observations).toMatchObject({
|
||||
accepted: 1,
|
||||
unsupported: 0,
|
||||
symbols: 0,
|
||||
});
|
||||
expect(result.summary).toMatchObject({
|
||||
verified: 0,
|
||||
unresolved: 1,
|
||||
});
|
||||
expect(result.pinvoke_imports[0]).toMatchObject({
|
||||
status: "unresolved",
|
||||
basis: "no-native-candidate",
|
||||
});
|
||||
it("keeps literal module names distinct and accepts the shared-library lib alias", () => {
|
||||
const literalName = verifyManagedNativeBoundaries({
|
||||
...inputWithImportScope("library"),
|
||||
native_observations: [
|
||||
nativeEvidenceWithExports(["open_native"], "/examples/rary.dylib"),
|
||||
],
|
||||
});
|
||||
expect(literalName.pinvoke_imports[0]).toMatchObject({
|
||||
status: "contradicted",
|
||||
basis: "module-mismatch",
|
||||
matched_native: { module_path: "/examples/rary.dylib" },
|
||||
});
|
||||
|
||||
it("does not verify a symbol whose spelling differs only by case", () => {
|
||||
const result = verifyManagedNativeBoundaries({
|
||||
...exampleInput(),
|
||||
native_observations: [nativeEvidenceWithExports(["OPEN_NATIVE"])],
|
||||
});
|
||||
const sharedLibraryAlias = verifyManagedNativeBoundaries({
|
||||
...inputWithImportScope("foo"),
|
||||
native_observations: [
|
||||
nativeEvidenceWithExports(["open_native"], "/examples/libfoo.dylib"),
|
||||
],
|
||||
});
|
||||
expect(sharedLibraryAlias.pinvoke_imports[0]).toMatchObject({
|
||||
status: "verified",
|
||||
basis: "exact-export-name",
|
||||
matched_native: { module_path: "/examples/libfoo.dylib" },
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.summary).toMatchObject({ verified: 0, unresolved: 1 });
|
||||
expect(result.pinvoke_imports[0]).toMatchObject({
|
||||
status: "unresolved",
|
||||
matched_native: null,
|
||||
candidates: [],
|
||||
});
|
||||
it("treats supported native Evidence with no symbols as unresolved", () => {
|
||||
const result = verifyManagedNativeBoundaries({
|
||||
...exampleInput(),
|
||||
native_observations: [nativeEvidenceWithExports([])],
|
||||
});
|
||||
|
||||
it("retains every observed native candidate", () => {
|
||||
const result = verifyManagedNativeBoundaries({
|
||||
...exampleInput(),
|
||||
native_observations: [
|
||||
nativeEvidenceWithExports(["open_native", "_open_native"]),
|
||||
],
|
||||
});
|
||||
expect(result.native_observations).toMatchObject({
|
||||
accepted: 1,
|
||||
unsupported: 0,
|
||||
symbols: 0,
|
||||
});
|
||||
expect(result.summary).toMatchObject({
|
||||
verified: 0,
|
||||
unresolved: 1,
|
||||
});
|
||||
expect(result.pinvoke_imports[0]).toMatchObject({
|
||||
status: "unresolved",
|
||||
basis: "no-native-candidate",
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.coverage).toMatchObject({
|
||||
status: "complete-within-inputs",
|
||||
});
|
||||
expect(result.pinvoke_imports[0]?.candidates).toHaveLength(2);
|
||||
it("does not verify a symbol whose spelling differs only by case", () => {
|
||||
const result = verifyManagedNativeBoundaries({
|
||||
...exampleInput(),
|
||||
native_observations: [nativeEvidenceWithExports(["OPEN_NATIVE"])],
|
||||
});
|
||||
|
||||
it("wraps verification in derived workflow Evidence", () => {
|
||||
const evidence = verifyManagedNativeBoundariesEvidence(exampleInput());
|
||||
|
||||
if (!evidence.ok) throw evidence.error;
|
||||
expect(evidence.value).toMatchObject({
|
||||
operation: "verify_managed_native_boundaries",
|
||||
provider: { id: "rea-dotnet-workflows" },
|
||||
confidence: "inferred",
|
||||
authority: "analyst-inference",
|
||||
normalized_result: {
|
||||
summary: { verified: 1 },
|
||||
},
|
||||
});
|
||||
expect(evidence.value.evidence_links).toHaveLength(2);
|
||||
expect(result.summary).toMatchObject({ verified: 0, unresolved: 1 });
|
||||
expect(result.pinvoke_imports[0]).toMatchObject({
|
||||
status: "unresolved",
|
||||
matched_native: null,
|
||||
candidates: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("retains every observed native candidate", () => {
|
||||
const result = verifyManagedNativeBoundaries({
|
||||
...exampleInput(),
|
||||
native_observations: [
|
||||
nativeEvidenceWithExports(["open_native", "_open_native"]),
|
||||
],
|
||||
});
|
||||
|
||||
expect(result.coverage).toMatchObject({
|
||||
status: "complete-within-inputs",
|
||||
});
|
||||
expect(result.pinvoke_imports[0]?.candidates).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("wraps verification in derived workflow Evidence", () => {
|
||||
const evidence = verifyManagedNativeBoundariesEvidence(exampleInput());
|
||||
|
||||
if (!evidence.ok) throw evidence.error;
|
||||
expect(evidence.value).toMatchObject({
|
||||
operation: "verify_managed_native_boundaries",
|
||||
provider: { id: "rea-dotnet-workflows" },
|
||||
confidence: "inferred",
|
||||
authority: "analyst-inference",
|
||||
normalized_result: {
|
||||
summary: { verified: 1 },
|
||||
},
|
||||
});
|
||||
expect(evidence.value.evidence_links).toHaveLength(2);
|
||||
});
|
||||
|
||||
describe("managed/native verification result algebra", () => {
|
||||
it("rejects states without their required native match", () => {
|
||||
const result = verifyManagedNativeBoundaries(exampleInput());
|
||||
|
||||
Reference in new issue
Block a user