mirror of
https://github.com/morluto/rea.git
synced 2026-11-12 01:41:21 +00:00
fix(managed): derive native platform facts before symbol matching (#10090)
fix(managed): derive native verification platform at intake
This commit is contained in:
1 parent
98f796c991
commit
34025d2fcd
5 files changed
+297
-66
No files matched your search
@@ -651,6 +651,9 @@ only when both identities are available and their format-specific comparison
|
||||
establishes a difference. ELF module names retain case, PE module names compare
|
||||
case-insensitively, and a case-only match for Mach-O or an unknown format stays
|
||||
unresolved because the available Evidence does not establish case behavior.
|
||||
The authenticated subject format establishes the native platform: thin and
|
||||
universal Mach-O use the same lookup rules; missing or unrecognized formats
|
||||
remain unknown. Retained source Evidence keeps its original subject format.
|
||||
For PE imports, `.dll`, `.exe`, a trailing period, and absolute paths suppress
|
||||
appending `.dll`; other names may also be tried with `.dll`. Unix loaders use
|
||||
the platform suffix (`.so` or `.dylib`) and `lib` prefix variations. A suffix
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { MANAGED_NATIVE_VERIFICATION_EXAMPLE } from "../../contracts/managed/managedWorkflowExamples.js";
|
||||
import { createEvidence } from "../../domain/evidence.js";
|
||||
import { managedNativeBoundaryInspectionSchema } from "../../domain/managed/managedArtifact.js";
|
||||
import { managedNativeVerificationResultSchema } from "../../domain/managed/managedNativeVerificationSchemas.js";
|
||||
import { functionDossierSchema } from "../../domain/native/functionDossier.js";
|
||||
import { inspectMachoSchema } from "../../domain/native/nativeInspection.js";
|
||||
import { executeManagedNativeBoundaries } from "./ManagedNativeVerificationService.js";
|
||||
|
||||
type NativeFormat = "mach-o" | "mach-o-universal" | "pe" | "elf" | "file";
|
||||
const native = (format: NativeFormat, path: string, decorated: boolean) => {
|
||||
const original = MANAGED_NATIVE_VERIFICATION_EXAMPLE.native_observations[0]!;
|
||||
const name = decorated ? "open_nativeW" : "open_native";
|
||||
const symbolTableName = `_${name}`;
|
||||
const macho = inspectMachoSchema.parse(original.normalized_result);
|
||||
const isMacho = format === "mach-o" || format === "mach-o-universal";
|
||||
const result = isMacho
|
||||
? {
|
||||
...macho,
|
||||
format,
|
||||
exports: {
|
||||
...macho.exports,
|
||||
total: 1,
|
||||
items: [
|
||||
{
|
||||
name: symbolTableName,
|
||||
address: "0x1000",
|
||||
weak: false,
|
||||
reexport: false,
|
||||
source: "nm",
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
: functionDossierSchema.parse({
|
||||
procedure: {
|
||||
address: "0x1000",
|
||||
name,
|
||||
classification: {
|
||||
external: false,
|
||||
thunk: false,
|
||||
thunk_target: null,
|
||||
provenance: "synthetic-provider",
|
||||
},
|
||||
body: { available: false, reason: "Inert authored observation" },
|
||||
signature: null,
|
||||
locals: [],
|
||||
},
|
||||
pseudocode: "",
|
||||
assembly: [],
|
||||
comments: [],
|
||||
callers: [],
|
||||
callees: [],
|
||||
incoming_references: [],
|
||||
outgoing_references: [],
|
||||
referenced_strings: [],
|
||||
referenced_names: [],
|
||||
basic_blocks: [],
|
||||
native_api: null,
|
||||
native_value_flow: null,
|
||||
limitations: [],
|
||||
});
|
||||
return createEvidence(
|
||||
{ path, sha256: "a".repeat(64), format },
|
||||
original.provider,
|
||||
{
|
||||
operation: isMacho ? "inspect_macho" : "analyze_function",
|
||||
parameters: {},
|
||||
result,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const verify = (
|
||||
format: NativeFormat,
|
||||
path: string,
|
||||
scope: string,
|
||||
decorated = false,
|
||||
) => {
|
||||
const original = MANAGED_NATIVE_VERIFICATION_EXAMPLE.managed_boundaries;
|
||||
const managed = managedNativeBoundaryInspectionSchema.parse(
|
||||
original.normalized_result,
|
||||
);
|
||||
const managedEvidence = createEvidence(undefined, original.provider, {
|
||||
operation: original.operation,
|
||||
parameters: {},
|
||||
result: {
|
||||
...managed,
|
||||
pinvoke_imports: managed.pinvoke_imports.map((item) => ({
|
||||
...item,
|
||||
import_scope_name: scope,
|
||||
no_mangle: !decorated,
|
||||
char_set: decorated ? "unicode" : "not-specified",
|
||||
call_convention: "cdecl",
|
||||
})),
|
||||
},
|
||||
});
|
||||
const nativeEvidence = native(format, path, decorated);
|
||||
const execution = executeManagedNativeBoundaries({
|
||||
managed_boundaries: managedEvidence,
|
||||
native_observations: [nativeEvidence],
|
||||
});
|
||||
if (!execution.ok) throw execution.error;
|
||||
expect(execution.value.sourceEvidence).toEqual([
|
||||
managedEvidence,
|
||||
nativeEvidence,
|
||||
]);
|
||||
expect(execution.value.sourceEvidence[1]?.subject?.format).toBe(format);
|
||||
const result = managedNativeVerificationResultSchema.parse(
|
||||
execution.value.evidence.normalized_result,
|
||||
);
|
||||
expect(result.evidence_links).toEqual([
|
||||
managedEvidence.evidence_id,
|
||||
nativeEvidence.evidence_id,
|
||||
]);
|
||||
const item = result.pinvoke_imports[0]!;
|
||||
expect(item.managed.import_scope_name).toBe(scope);
|
||||
return { item, execution: execution.value, nativeEvidence };
|
||||
};
|
||||
|
||||
it.each([
|
||||
{
|
||||
format: "mach-o",
|
||||
path: "/fixtures/libnativehelper.dylib",
|
||||
status: "verified",
|
||||
},
|
||||
{
|
||||
format: "mach-o-universal",
|
||||
path: "/fixtures/libnativehelper.dylib",
|
||||
status: "verified",
|
||||
},
|
||||
{ format: "elf", path: "/fixtures/libnativehelper.so", status: "verified" },
|
||||
{ format: "pe", path: "C:\\fixtures\\nativehelper.dll", status: "verified" },
|
||||
{
|
||||
format: "file",
|
||||
path: "/fixtures/libnativehelper.dylib",
|
||||
status: "unresolved",
|
||||
},
|
||||
] as const)(
|
||||
"uses authenticated $format subject platform for module aliases",
|
||||
({ format, path, status }) => {
|
||||
const { item, execution, nativeEvidence } = verify(
|
||||
format,
|
||||
path,
|
||||
"nativehelper",
|
||||
);
|
||||
expect(item.status).toBe(status);
|
||||
expect(execution.hasUnknowns).toBe(format === "file");
|
||||
expect(item.matched_native?.evidence_id).toBe(nativeEvidence.evidence_id);
|
||||
expect(item.matched_native?.name).toBe(
|
||||
format === "mach-o" || format === "mach-o-universal"
|
||||
? "_open_native"
|
||||
: "open_native",
|
||||
);
|
||||
if (format === "file") expect(item.basis).toBe("module-identity-unknown");
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{
|
||||
format: "mach-o",
|
||||
path: "/fixtures/libnativehelper.dylib",
|
||||
scope: "libnativehelper.dylib",
|
||||
status: "unresolved",
|
||||
},
|
||||
{
|
||||
format: "mach-o-universal",
|
||||
path: "/fixtures/libnativehelper.dylib",
|
||||
scope: "libnativehelper.dylib",
|
||||
status: "unresolved",
|
||||
},
|
||||
{
|
||||
format: "elf",
|
||||
path: "/fixtures/libnativehelper.so",
|
||||
scope: "libnativehelper.so",
|
||||
status: "unresolved",
|
||||
},
|
||||
{
|
||||
format: "pe",
|
||||
path: "C:\\fixtures\\nativehelper.dll",
|
||||
scope: "nativehelper.dll",
|
||||
status: "inferred",
|
||||
},
|
||||
{
|
||||
format: "file",
|
||||
path: "/fixtures/nativehelper",
|
||||
scope: "nativehelper",
|
||||
status: "inferred",
|
||||
},
|
||||
] as const)(
|
||||
"keeps Windows decorated-name candidates within $format platform authority",
|
||||
({ format, path, scope, status }) => {
|
||||
const { item } = verify(format, path, scope, true);
|
||||
expect(item.status).toBe(status);
|
||||
if (status === "unresolved") {
|
||||
expect(item.basis).toBe("no-native-candidate");
|
||||
expect(item.candidates).toEqual([]);
|
||||
} else expect(item.basis).toBe("decorated-name-candidate");
|
||||
},
|
||||
);
|
||||
@@ -1,7 +1,10 @@
|
||||
import { digestCanonicalValue } from "../canonicalDigest.js";
|
||||
import type { Evidence } from "../evidence.js";
|
||||
import { machoRuntimeSymbolName } from "../native/machoSymbolName.js";
|
||||
import type { ManagedNativeObservationFacts } from "./managedNativeVerificationSource.js";
|
||||
import type {
|
||||
ManagedNativeObservationFacts,
|
||||
ManagedNativePlatform,
|
||||
} from "./managedNativeVerificationSource.js";
|
||||
import type { ManagedNativeBoundaryInspection } from "./managedArtifact.js";
|
||||
import type {
|
||||
ManagedNativeVerificationResult,
|
||||
@@ -22,18 +25,21 @@ export const collectNativeSymbols = (
|
||||
readonly unsupported: number;
|
||||
readonly partialExports: boolean;
|
||||
readonly exportLimitations: readonly string[];
|
||||
readonly moduleFormatsByEvidenceId: ReadonlyMap<string, string | null>;
|
||||
readonly modulePlatformsByEvidenceId: ReadonlyMap<
|
||||
string,
|
||||
ManagedNativePlatform
|
||||
>;
|
||||
} => {
|
||||
const symbols: NativeSymbol[] = [];
|
||||
const moduleFormatsByEvidenceId = new Map<string, string | null>();
|
||||
const modulePlatformsByEvidenceId = new Map<string, ManagedNativePlatform>();
|
||||
let accepted = 0;
|
||||
let unsupported = 0;
|
||||
let partialExports = false;
|
||||
const exportLimitations: string[] = [];
|
||||
for (const observation of observations) {
|
||||
moduleFormatsByEvidenceId.set(
|
||||
modulePlatformsByEvidenceId.set(
|
||||
observation.evidence.evidence_id,
|
||||
observation.evidence.subject?.format ?? null,
|
||||
observation.platform,
|
||||
);
|
||||
if (!observation.supported) {
|
||||
unsupported += 1;
|
||||
@@ -55,7 +61,7 @@ export const collectNativeSymbols = (
|
||||
unsupported,
|
||||
partialExports,
|
||||
exportLimitations,
|
||||
moduleFormatsByEvidenceId,
|
||||
modulePlatformsByEvidenceId,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -73,22 +79,26 @@ type ModuleCompatibility = "match" | "mismatch" | "unknown";
|
||||
const moduleFilename = (
|
||||
value: string,
|
||||
caseSensitive: boolean,
|
||||
format?: string | null,
|
||||
platform: ManagedNativePlatform,
|
||||
): string => {
|
||||
const separators = format === "elf" || format === "mach-o" ? /\//u : /[\\/]/u;
|
||||
const separators =
|
||||
platform === "elf" || platform === "mach-o" ? /\//u : /[\\/]/u;
|
||||
const filename = value.split(separators).at(-1) ?? value;
|
||||
return caseSensitive ? filename : filename.toLowerCase();
|
||||
};
|
||||
|
||||
const isAbsoluteModulePath = (value: string, format?: string | null): boolean =>
|
||||
const isAbsoluteModulePath = (
|
||||
value: string,
|
||||
platform: ManagedNativePlatform,
|
||||
): boolean =>
|
||||
value.startsWith("/") ||
|
||||
(format === "pe" &&
|
||||
(platform === "pe" &&
|
||||
(/^[a-z]:[\\/]/iu.test(value) || value.startsWith("\\\\")));
|
||||
|
||||
const hasModulePathSeparator = (
|
||||
value: string,
|
||||
format: string | null,
|
||||
): boolean => (format === "pe" ? /[\\/]/u.test(value) : value.includes("/"));
|
||||
platform: ManagedNativePlatform,
|
||||
): boolean => (platform === "pe" ? /[\\/]/u.test(value) : value.includes("/"));
|
||||
|
||||
const unixModuleCandidates = (
|
||||
value: string,
|
||||
@@ -123,15 +133,15 @@ const unixModuleCandidates = (
|
||||
/** Candidate module names documented for each native loader platform. */
|
||||
const moduleCandidates = (
|
||||
value: string,
|
||||
format: string | null,
|
||||
platform: ManagedNativePlatform,
|
||||
): readonly string[] => {
|
||||
if (isAbsoluteModulePath(value, format)) return [value];
|
||||
if (format === "pe")
|
||||
if (isAbsoluteModulePath(value, platform)) return [value];
|
||||
if (platform === "pe")
|
||||
return /\.(dll|exe)$/iu.test(value) || value.endsWith(".")
|
||||
? [value]
|
||||
: [value, `${value}.dll`];
|
||||
if (format === "elf") return unixModuleCandidates(value, ".so");
|
||||
if (format === "mach-o") return unixModuleCandidates(value, ".dylib");
|
||||
if (platform === "elf") return unixModuleCandidates(value, ".so");
|
||||
if (platform === "mach-o") return unixModuleCandidates(value, ".dylib");
|
||||
return [value];
|
||||
};
|
||||
|
||||
@@ -148,29 +158,29 @@ const plausiblePlatformCandidates = (value: string): readonly string[] => {
|
||||
const moduleValueCompatibility = (
|
||||
expected: string,
|
||||
observed: string,
|
||||
format: string | null,
|
||||
platform: ManagedNativePlatform,
|
||||
): ModuleCompatibility => {
|
||||
const exactCandidates = moduleCandidates(expected, format);
|
||||
const expectedIsAbsolutePath = isAbsoluteModulePath(expected, format);
|
||||
const exactCandidates = moduleCandidates(expected, platform);
|
||||
const expectedIsAbsolutePath = isAbsoluteModulePath(expected, platform);
|
||||
if (expectedIsAbsolutePath) {
|
||||
const observedIsAbsolutePath = isAbsoluteModulePath(observed, format);
|
||||
const observedIsAbsolutePath = isAbsoluteModulePath(observed, platform);
|
||||
if (!observedIsAbsolutePath) {
|
||||
const hasMatchingFilename = exactCandidates.some(
|
||||
(candidate) =>
|
||||
moduleFilename(candidate, format !== "pe", format) ===
|
||||
moduleFilename(observed, format !== "pe", format),
|
||||
moduleFilename(candidate, platform !== "pe", platform) ===
|
||||
moduleFilename(observed, platform !== "pe", platform),
|
||||
);
|
||||
return hasMatchingFilename ? "unknown" : "mismatch";
|
||||
}
|
||||
const fold = format === "pe";
|
||||
const fold = platform === "pe";
|
||||
const normalize = (path: string) =>
|
||||
fold ? path.replaceAll("\\", "/").toLowerCase() : path;
|
||||
return exactCandidates.some(
|
||||
(candidate) => normalize(candidate) === normalize(observed),
|
||||
)
|
||||
? "match"
|
||||
: format !== "pe" &&
|
||||
format !== "elf" &&
|
||||
: platform !== "pe" &&
|
||||
platform !== "elf" &&
|
||||
exactCandidates.some(
|
||||
(candidate) =>
|
||||
normalize(candidate).toLowerCase() ===
|
||||
@@ -179,15 +189,15 @@ const moduleValueCompatibility = (
|
||||
? "unknown"
|
||||
: "mismatch";
|
||||
}
|
||||
if (hasModulePathSeparator(expected, format)) {
|
||||
const isObservedAbsolute = isAbsoluteModulePath(observed, format);
|
||||
if (hasModulePathSeparator(expected, platform)) {
|
||||
const isObservedAbsolute = isAbsoluteModulePath(observed, platform);
|
||||
if (!isObservedAbsolute && exactCandidates.includes(observed))
|
||||
return "match";
|
||||
const normalizePath = (path: string) =>
|
||||
format === "pe" ? path.replaceAll("\\", "/").toLowerCase() : path;
|
||||
platform === "pe" ? path.replaceAll("\\", "/").toLowerCase() : path;
|
||||
if (
|
||||
!isObservedAbsolute &&
|
||||
format === "pe" &&
|
||||
platform === "pe" &&
|
||||
exactCandidates.some(
|
||||
(candidate) => normalizePath(candidate) === normalizePath(observed),
|
||||
)
|
||||
@@ -203,55 +213,53 @@ const moduleValueCompatibility = (
|
||||
}
|
||||
const sameFilename = exactCandidates.some(
|
||||
(candidate) =>
|
||||
moduleFilename(candidate, false, format) ===
|
||||
moduleFilename(observed, false, format),
|
||||
moduleFilename(candidate, false, platform) ===
|
||||
moduleFilename(observed, false, platform),
|
||||
);
|
||||
return sameFilename ? "unknown" : "mismatch";
|
||||
}
|
||||
const observedFilename = moduleFilename(observed, true, format);
|
||||
const observedFilename = moduleFilename(observed, true, platform);
|
||||
if (exactCandidates.includes(observedFilename)) return "match";
|
||||
const caseFoldedCandidates = exactCandidates.map((candidate) =>
|
||||
moduleFilename(candidate, false, format),
|
||||
moduleFilename(candidate, false, platform),
|
||||
);
|
||||
const observedFolded = moduleFilename(observed, false, format);
|
||||
const observedFolded = moduleFilename(observed, false, platform);
|
||||
if (!caseFoldedCandidates.includes(observedFolded)) {
|
||||
if (
|
||||
format !== "pe" &&
|
||||
format !== "elf" &&
|
||||
format !== "mach-o" &&
|
||||
platform === null &&
|
||||
plausiblePlatformCandidates(expected)
|
||||
.map((candidate) => moduleFilename(candidate, false, format))
|
||||
.map((candidate) => moduleFilename(candidate, false, platform))
|
||||
.includes(observedFolded)
|
||||
)
|
||||
return "unknown";
|
||||
return "mismatch";
|
||||
}
|
||||
if (format === "elf") return "mismatch";
|
||||
if (format === "pe") return "match";
|
||||
if (platform === "elf") return "mismatch";
|
||||
if (platform === "pe") return "match";
|
||||
return "unknown";
|
||||
};
|
||||
|
||||
const moduleCompatibility = (
|
||||
scope: string | null,
|
||||
symbol: NativeSymbol,
|
||||
format: string | null,
|
||||
platform: ManagedNativePlatform,
|
||||
): ModuleCompatibility => {
|
||||
if (
|
||||
scope === null ||
|
||||
scope.trim().length === 0 ||
|
||||
moduleFilename(scope, false, format).length === 0
|
||||
moduleFilename(scope, false, platform).length === 0
|
||||
)
|
||||
return "unknown";
|
||||
const observedValues = isAbsoluteModulePath(scope, format)
|
||||
const observedValues = isAbsoluteModulePath(scope, platform)
|
||||
? [symbol.module_path]
|
||||
: [symbol.module_name, symbol.module_path];
|
||||
const values = observedValues.filter(
|
||||
(value): value is string =>
|
||||
value !== null && moduleFilename(value, false, format).length > 0,
|
||||
value !== null && moduleFilename(value, false, platform).length > 0,
|
||||
);
|
||||
if (values.length === 0) return "unknown";
|
||||
const results = values.map((value) =>
|
||||
moduleValueCompatibility(scope, value, format),
|
||||
moduleValueCompatibility(scope, value, platform),
|
||||
);
|
||||
if (results.includes("match")) return "match";
|
||||
if (results.includes("unknown")) return "unknown";
|
||||
@@ -263,26 +271,22 @@ const candidateNames = (
|
||||
PinvokeImport,
|
||||
"import_name" | "char_set" | "call_convention" | "no_mangle"
|
||||
>,
|
||||
nativeFormat: string | null,
|
||||
platform: ManagedNativePlatform,
|
||||
): readonly string[] => {
|
||||
if (managed.import_name.length === 0) return [];
|
||||
const names = new Set<string>([managed.import_name]);
|
||||
// Unix P/Invoke lookup uses the declared entry point directly, regardless of
|
||||
// marshalling charset or calling convention. Keep Windows decorations local
|
||||
// to PE or an unresolved native platform; metadata flags remain unchanged.
|
||||
if (
|
||||
!managed.no_mangle &&
|
||||
nativeFormat !== "elf" &&
|
||||
nativeFormat !== "mach-o"
|
||||
) {
|
||||
if (!managed.no_mangle && (platform === "pe" || platform === null)) {
|
||||
if (
|
||||
managed.char_set === "unicode" ||
|
||||
(nativeFormat === "pe" && managed.char_set === "auto")
|
||||
(platform === "pe" && managed.char_set === "auto")
|
||||
)
|
||||
names.add(`${managed.import_name}W`);
|
||||
if (
|
||||
managed.char_set === "ansi" ||
|
||||
(nativeFormat === "pe" && managed.char_set === "not-specified")
|
||||
(platform === "pe" && managed.char_set === "not-specified")
|
||||
)
|
||||
names.add(`${managed.import_name}A`);
|
||||
if (managed.call_convention === "stdcall") {
|
||||
@@ -312,15 +316,16 @@ interface PinvokeCandidateMatch {
|
||||
const pinvokeCandidates = (
|
||||
managed: PinvokeImport,
|
||||
symbols: readonly NativeSymbol[],
|
||||
moduleFormatsByEvidenceId: ReadonlyMap<string, string | null>,
|
||||
modulePlatformsByEvidenceId: ReadonlyMap<string, ManagedNativePlatform>,
|
||||
): readonly NativeSymbol[] => {
|
||||
const namesByFormat = new Map<string | null, readonly string[]>();
|
||||
const namesByPlatform = new Map<ManagedNativePlatform, readonly string[]>();
|
||||
return symbols.filter((symbol) => {
|
||||
const format = moduleFormatsByEvidenceId.get(symbol.evidence_id) ?? null;
|
||||
let names = namesByFormat.get(format);
|
||||
const platform =
|
||||
modulePlatformsByEvidenceId.get(symbol.evidence_id) ?? null;
|
||||
let names = namesByPlatform.get(platform);
|
||||
if (names === undefined) {
|
||||
names = candidateNames(managed, format);
|
||||
namesByFormat.set(format, names);
|
||||
names = candidateNames(managed, platform);
|
||||
namesByPlatform.set(platform, names);
|
||||
}
|
||||
return names.some((name) => sameSymbolName(name, symbol));
|
||||
});
|
||||
@@ -330,12 +335,12 @@ const matchPinvoke = (
|
||||
managed: PinvokeImport,
|
||||
symbols: readonly NativeSymbol[],
|
||||
supportedNativeEvidence: number,
|
||||
moduleFormatsByEvidenceId: ReadonlyMap<string, string | null>,
|
||||
modulePlatformsByEvidenceId: ReadonlyMap<string, ManagedNativePlatform>,
|
||||
): PinvokeCandidateMatch => {
|
||||
const allCandidates = pinvokeCandidates(
|
||||
managed,
|
||||
symbols,
|
||||
moduleFormatsByEvidenceId,
|
||||
modulePlatformsByEvidenceId,
|
||||
);
|
||||
const [first, ...remaining] = allCandidates;
|
||||
if (first === undefined)
|
||||
@@ -357,7 +362,7 @@ const matchPinvoke = (
|
||||
moduleCompatibility(
|
||||
managed.import_scope_name,
|
||||
symbol,
|
||||
moduleFormatsByEvidenceId.get(symbol.evidence_id) ?? null,
|
||||
modulePlatformsByEvidenceId.get(symbol.evidence_id) ?? null,
|
||||
);
|
||||
const exact = candidates.find(
|
||||
(symbol) =>
|
||||
@@ -460,7 +465,10 @@ interface VerifyPinvokeContext {
|
||||
readonly native: {
|
||||
readonly symbols: readonly NativeSymbol[];
|
||||
readonly accepted: number;
|
||||
readonly moduleFormatsByEvidenceId: ReadonlyMap<string, string | null>;
|
||||
readonly modulePlatformsByEvidenceId: ReadonlyMap<
|
||||
string,
|
||||
ManagedNativePlatform
|
||||
>;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -489,7 +497,7 @@ export const verifyPinvoke = ({
|
||||
item,
|
||||
native.symbols,
|
||||
native.accepted,
|
||||
native.moduleFormatsByEvidenceId,
|
||||
native.modulePlatformsByEvidenceId,
|
||||
);
|
||||
const limitations = pinvokeLimitations(selection.match, item);
|
||||
const context: PinvokeVerificationContext = {
|
||||
|
||||
@@ -30,6 +30,7 @@ describe("managed/native verification source", () => {
|
||||
if (!source.ok) throw source.error;
|
||||
const [observation] = source.value.nativeObservations;
|
||||
expect(observation?.supported).toBe(true);
|
||||
expect(observation?.platform).toBeNull();
|
||||
expect(observation?.symbols.map(({ name }) => name)).toEqual([
|
||||
"open_native",
|
||||
]);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isMachOFormat } from "../binaryTargetTypes.js";
|
||||
import { getEvidenceInputContract } from "../evidenceInputContract.js";
|
||||
import { managedNativeVerificationRequestSchema } from "./managedNativeVerificationSchemas.js";
|
||||
|
||||
@@ -30,10 +31,14 @@ export interface ManagedNativeVerificationSource {
|
||||
readonly nativeObservations: readonly ManagedNativeObservationFacts[];
|
||||
}
|
||||
|
||||
/** Module lookup semantics proved by the native subject's format. */
|
||||
export type ManagedNativePlatform = "pe" | "elf" | "mach-o" | null;
|
||||
|
||||
export interface ManagedNativeObservationFacts {
|
||||
readonly evidence: Evidence;
|
||||
readonly symbols: readonly NativeSymbol[];
|
||||
readonly supported: boolean;
|
||||
readonly platform: ManagedNativePlatform;
|
||||
/** Coverage of an export inventory; function observations are targeted. */
|
||||
readonly exportCoverage?: {
|
||||
readonly exhaustive: boolean;
|
||||
@@ -58,6 +63,12 @@ const readNativeObservation = (
|
||||
const read = readEvidence(raw);
|
||||
if (!read.ok) return read;
|
||||
const evidence = read.value;
|
||||
const format = evidence.subject?.format;
|
||||
const platform: ManagedNativePlatform = isMachOFormat(format)
|
||||
? "mach-o"
|
||||
: format === "pe" || format === "elf"
|
||||
? format
|
||||
: null;
|
||||
if (evidence.operation === "inspect_macho") {
|
||||
const payload = readEvidencePayload(
|
||||
retainedInspectMachoSchema,
|
||||
@@ -66,6 +77,7 @@ const readNativeObservation = (
|
||||
if (!payload.ok) return payload;
|
||||
return ok({
|
||||
evidence,
|
||||
platform,
|
||||
supported: true,
|
||||
exportCoverage: {
|
||||
exhaustive: payload.value.exports.exhaustive,
|
||||
@@ -90,6 +102,7 @@ const readNativeObservation = (
|
||||
if (!payload.ok) return payload;
|
||||
return ok({
|
||||
evidence,
|
||||
platform,
|
||||
supported: true,
|
||||
symbols: [
|
||||
{
|
||||
@@ -104,7 +117,12 @@ const readNativeObservation = (
|
||||
],
|
||||
});
|
||||
}
|
||||
return ok({ evidence, supported: false, symbols: [] });
|
||||
return ok({
|
||||
evidence,
|
||||
platform,
|
||||
supported: false,
|
||||
symbols: [],
|
||||
});
|
||||
};
|
||||
|
||||
/** Authenticate and parse source Evidence once, retaining it for session recording. */
|
||||
|
||||
Reference in new issue
Block a user