mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
213 lines
7.2 KiB
TypeScript
213 lines
7.2 KiB
TypeScript
import { z } from "zod";
|
|
|
|
import { emptyArraySchema } from "../emptyArraySchema.js";
|
|
import { evidenceSchema } from "../evidence.js";
|
|
import { cliMetadataGuidSchema } from "./managedArtifact.js";
|
|
import { digestSchema } from "../digests.js";
|
|
import { prefixedDigestSchema } from "../digests.js";
|
|
|
|
const evidenceIdSchema = prefixedDigestSchema("ev");
|
|
const tokenSchema = z.string().regex(/^0x[0-9a-f]{8}$/u);
|
|
const textSchema = z.string().min(1);
|
|
|
|
const nativeSymbolSchema = z.strictObject({
|
|
evidence_id: evidenceIdSchema,
|
|
operation: z.string().min(1),
|
|
name: z.string().min(1),
|
|
address: z.string().nullable(),
|
|
module_name: z.string().nullable(),
|
|
module_path: z.string().nullable(),
|
|
source: z.enum(["macho-export", "function-dossier"]),
|
|
});
|
|
|
|
export type NativeSymbol = z.infer<typeof nativeSymbolSchema>;
|
|
|
|
const pinvokeVerificationContextShape = {
|
|
item_id: prefixedDigestSchema("mnv_pinvoke"),
|
|
managed: z.strictObject({
|
|
token: tokenSchema,
|
|
member_token: tokenSchema.nullable(),
|
|
member_name: z.string().nullable(),
|
|
import_name: z.string().min(1),
|
|
import_scope_name: z.string().nullable(),
|
|
no_mangle: z.boolean(),
|
|
char_set: z.enum(["not-specified", "ansi", "unicode", "auto", "unknown"]),
|
|
call_convention: z.enum([
|
|
"not-specified",
|
|
"winapi",
|
|
"cdecl",
|
|
"stdcall",
|
|
"thiscall",
|
|
"fastcall",
|
|
"unknown",
|
|
]),
|
|
declaration_verification: z.literal("managed-declaration-only"),
|
|
}),
|
|
evidence_links: z.array(evidenceIdSchema).min(1),
|
|
limitations: z.array(textSchema),
|
|
};
|
|
|
|
const observedCandidatesSchema = z
|
|
.tuple([nativeSymbolSchema])
|
|
.rest(nativeSymbolSchema);
|
|
|
|
export const pinvokeVerificationSchema = z.discriminatedUnion("status", [
|
|
z.strictObject({
|
|
...pinvokeVerificationContextShape,
|
|
status: z.literal("verified"),
|
|
basis: z.enum(["exact-export-name", "exact-function-name"]),
|
|
confidence: z.literal("observed"),
|
|
matched_native: nativeSymbolSchema,
|
|
candidates: observedCandidatesSchema,
|
|
}),
|
|
z.strictObject({
|
|
...pinvokeVerificationContextShape,
|
|
status: z.literal("inferred"),
|
|
basis: z.literal("decorated-name-candidate"),
|
|
confidence: z.literal("inferred"),
|
|
matched_native: nativeSymbolSchema,
|
|
candidates: observedCandidatesSchema,
|
|
}),
|
|
z.strictObject({
|
|
...pinvokeVerificationContextShape,
|
|
status: z.literal("unresolved"),
|
|
basis: z.enum(["no-native-candidate", "unsupported-native-evidence"]),
|
|
confidence: z.literal("unknown"),
|
|
matched_native: z.null(),
|
|
candidates: emptyArraySchema,
|
|
}),
|
|
z.strictObject({
|
|
...pinvokeVerificationContextShape,
|
|
status: z.literal("contradicted"),
|
|
basis: z.literal("module-mismatch"),
|
|
confidence: z.literal("unknown"),
|
|
matched_native: nativeSymbolSchema,
|
|
candidates: observedCandidatesSchema,
|
|
}),
|
|
]);
|
|
|
|
export type PinvokeVerification = z.infer<typeof pinvokeVerificationSchema>;
|
|
|
|
/** Authenticated managed boundary Evidence plus native observations. */
|
|
export const managedNativeVerificationInputSchema = z
|
|
.strictObject({
|
|
managed_boundaries: evidenceSchema,
|
|
native_observations: z
|
|
.array(evidenceSchema)
|
|
.min(1)
|
|
.describe("Native function or export Evidence records"),
|
|
})
|
|
.superRefine((input, context) => {
|
|
const ids = new Set<string>();
|
|
for (const [index, evidence] of input.native_observations.entries()) {
|
|
if (evidence.evidence_id === input.managed_boundaries.evidence_id) {
|
|
context.addIssue({
|
|
code: "custom",
|
|
path: ["native_observations", index],
|
|
message:
|
|
"Native observation Evidence must be distinct from managed boundary Evidence",
|
|
});
|
|
}
|
|
if (ids.has(evidence.evidence_id)) {
|
|
context.addIssue({
|
|
code: "custom",
|
|
path: ["native_observations", index],
|
|
message: "Native observation Evidence IDs must be unique",
|
|
});
|
|
}
|
|
ids.add(evidence.evidence_id);
|
|
}
|
|
});
|
|
|
|
export type ManagedNativeVerificationInput = z.infer<
|
|
typeof managedNativeVerificationInputSchema
|
|
>;
|
|
|
|
const verificationCoverageSchema = z.strictObject({
|
|
status: z.enum(["complete-within-inputs", "partial"]),
|
|
});
|
|
|
|
/** Provider-neutral managed/native verification result. */
|
|
export const managedNativeVerificationResultSchema = z
|
|
.strictObject({
|
|
verification_id: prefixedDigestSchema("mnv"),
|
|
algorithm: z.strictObject({
|
|
name: z.literal("rea-managed-native-verification"),
|
|
token_identity: z.literal("build-local"),
|
|
token_to_address_mapping: z.literal("not-inferred"),
|
|
}),
|
|
managed_boundary: z.strictObject({
|
|
evidence_id: evidenceIdSchema,
|
|
artifact_sha256: digestSchema,
|
|
artifact_path: z.string().min(1),
|
|
mvid: cliMetadataGuidSchema.nullable(),
|
|
metadata_status: z.enum(["absent", "complete", "partial", "malformed"]),
|
|
pinvoke_imports_total: z.number().int().min(0),
|
|
native_implementations_total: z.number().int().min(0),
|
|
coverage_state: z.enum(["complete", "partial", "unavailable"]),
|
|
}),
|
|
native_observations: z.strictObject({
|
|
total: z.number().int().min(1),
|
|
accepted: z.number().int().min(0),
|
|
unsupported: z.number().int().min(0),
|
|
symbols: z.number().int().min(0),
|
|
}),
|
|
summary: z.strictObject({
|
|
verified: z.number().int().min(0),
|
|
inferred: z.number().int().min(0),
|
|
unresolved: z.number().int().min(0),
|
|
contradicted: z.number().int().min(0),
|
|
native_body_unresolved: z.number().int().min(0),
|
|
}),
|
|
pinvoke_imports: z.array(pinvokeVerificationSchema),
|
|
native_implementations: z.strictObject({
|
|
unresolved: z.number().int().min(0),
|
|
reason: textSchema,
|
|
}),
|
|
coverage: verificationCoverageSchema,
|
|
evidence_links: z.array(evidenceIdSchema).min(2),
|
|
limitations: z.array(textSchema),
|
|
})
|
|
.superRefine((result, context) => {
|
|
const observedNativeCount =
|
|
result.native_observations.accepted +
|
|
result.native_observations.unsupported;
|
|
if (observedNativeCount !== result.native_observations.total)
|
|
context.addIssue({
|
|
code: "custom",
|
|
path: ["native_observations"],
|
|
message: "Native observation counts must account for the total input",
|
|
});
|
|
for (const status of [
|
|
"verified",
|
|
"inferred",
|
|
"unresolved",
|
|
"contradicted",
|
|
] as const)
|
|
if (
|
|
result.summary[status] !==
|
|
result.pinvoke_imports.filter((item) => item.status === status).length
|
|
)
|
|
context.addIssue({
|
|
code: "custom",
|
|
path: ["summary", status],
|
|
message: `P/Invoke ${status} count must match the item results`,
|
|
});
|
|
if (
|
|
result.summary.native_body_unresolved !==
|
|
result.native_implementations.unresolved
|
|
)
|
|
context.addIssue({
|
|
code: "custom",
|
|
path: ["summary", "native_body_unresolved"],
|
|
message: "Native body summary must match unresolved implementations",
|
|
});
|
|
})
|
|
.describe(
|
|
"Counts must agree across the result: native accepted plus unsupported equals total; each P/Invoke summary count equals the number of items with that status; and native_body_unresolved equals native_implementations.unresolved. REA checks these cross-field invariants after constructing the result.",
|
|
);
|
|
|
|
export type ManagedNativeVerificationResult = z.infer<
|
|
typeof managedNativeVerificationResultSchema
|
|
>;
|