refactor(hopper): move verifier assertions out of runtime
This commit is contained in:
1 parent
cd10ee1f8a
commit
008931a178
7 files changed
+50
-61
No files matched your search
@@ -11,7 +11,6 @@
|
||||
"src/composition/webBundleAnalysisWorkerProcess.ts",
|
||||
"src/contracts/errorSchemas.ts",
|
||||
"src/evaluation/AgentEvaluationFixtures.ts",
|
||||
"src/application/RealHopperAssertions.ts",
|
||||
"scripts/verify/fridaBindingsCompatibility.ts",
|
||||
"scripts/verify/termux/browser.mjs",
|
||||
"scripts/verify-real-ghidra-noreturn.mjs",
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/** Extract one real procedure address from the complete inline inventory. */
|
||||
export function firstProcedureAddress(input: unknown): string;
|
||||
|
||||
/** Reject empty and success-shaped embedded decompilation failures. */
|
||||
export function requirePseudocode(input: unknown, operation: string): string;
|
||||
|
||||
/** Validate a complete Hopper function dossier semantically. */
|
||||
export function requireFunctionDossier(
|
||||
input: unknown,
|
||||
expectedAddress: string,
|
||||
): Record<string, unknown>;
|
||||
|
||||
/** Positive semantic facts required from one source-owned Hopper fixture. */
|
||||
export interface FunctionDossierOracle {
|
||||
readonly procedure_address: string;
|
||||
readonly callee_address?: string;
|
||||
readonly caller_address?: string;
|
||||
readonly referenced_string?: string;
|
||||
readonly referenced_name?: string;
|
||||
readonly comment?: string;
|
||||
readonly require_cfg_successor?: boolean;
|
||||
readonly require_assembly?: boolean;
|
||||
}
|
||||
|
||||
/** Reject a structurally valid dossier that omits fixture-proven semantics. */
|
||||
export function requireFunctionDossierOracle(
|
||||
input: unknown,
|
||||
oracle: FunctionDossierOracle,
|
||||
): Record<string, unknown>;
|
||||
|
||||
/** Require every relationship to be an actual hexadecimal address. */
|
||||
export function requireAddressArray(
|
||||
input: unknown,
|
||||
operation: string,
|
||||
): string[];
|
||||
@@ -3,7 +3,7 @@ import { z } from "zod";
|
||||
const addressSchema = z.string().regex(/^0x[0-9a-f]+$/iu);
|
||||
const identitySchema = z.object({ address: addressSchema, name: z.string() });
|
||||
/** Extract one real procedure address from the complete inline inventory. */
|
||||
export const firstProcedureAddress = (input: unknown): string => {
|
||||
export const firstProcedureAddress = (input) => {
|
||||
const first = z
|
||||
.array(z.object({ address: addressSchema }))
|
||||
.min(1)
|
||||
@@ -13,10 +13,7 @@ export const firstProcedureAddress = (input: unknown): string => {
|
||||
};
|
||||
|
||||
/** Reject empty and success-shaped embedded decompilation failures. */
|
||||
export const requirePseudocode = (
|
||||
input: unknown,
|
||||
operation: string,
|
||||
): string => {
|
||||
export const requirePseudocode = (input, operation) => {
|
||||
if (typeof input !== "string" || input.trim().length === 0)
|
||||
throw new TypeError(`${operation} returned empty pseudocode`);
|
||||
if (input === "No output" || input.startsWith("Error:"))
|
||||
@@ -25,10 +22,7 @@ export const requirePseudocode = (
|
||||
};
|
||||
|
||||
/** Validate a complete Hopper function dossier semantically. */
|
||||
export const requireFunctionDossier = (
|
||||
input: unknown,
|
||||
expectedAddress: string,
|
||||
): Record<string, unknown> => {
|
||||
export const requireFunctionDossier = (input, expectedAddress) => {
|
||||
const value = objectValue(
|
||||
input,
|
||||
"analyze_function returned no function dossier",
|
||||
@@ -40,29 +34,14 @@ export const requireFunctionDossier = (
|
||||
throw new TypeError("analyze_function omitted the procedure name");
|
||||
requirePseudocode(value.pseudocode, "analyze_function");
|
||||
for (const field of COLLECTION_FIELDS) list(value, field);
|
||||
for (const field of ["callers", "callees"] as const)
|
||||
for (const field of ["callers", "callees"])
|
||||
z.array(identitySchema).parse(list(value, field));
|
||||
validateBlocks(list(value, "basic_blocks"));
|
||||
return value;
|
||||
};
|
||||
|
||||
/** Positive semantic facts required from one source-owned Hopper fixture. */
|
||||
export interface FunctionDossierOracle {
|
||||
readonly procedure_address: string;
|
||||
readonly callee_address?: string;
|
||||
readonly caller_address?: string;
|
||||
readonly referenced_string?: string;
|
||||
readonly referenced_name?: string;
|
||||
readonly comment?: string;
|
||||
readonly require_cfg_successor?: boolean;
|
||||
readonly require_assembly?: boolean;
|
||||
}
|
||||
|
||||
/** Reject a structurally valid dossier that omits fixture-proven semantics. */
|
||||
export const requireFunctionDossierOracle = (
|
||||
input: unknown,
|
||||
oracle: FunctionDossierOracle,
|
||||
): Record<string, unknown> => {
|
||||
export const requireFunctionDossierOracle = (input, oracle) => {
|
||||
const value = requireFunctionDossier(input, oracle.procedure_address);
|
||||
if (oracle.callee_address !== undefined) {
|
||||
const callees = z.array(identitySchema).parse(list(value, "callees"));
|
||||
@@ -158,12 +137,7 @@ const referenceEdgeSchema = z.object({
|
||||
target_address: addressSchema,
|
||||
});
|
||||
|
||||
const requireReferencedValue = (
|
||||
dossier: Record<string, unknown>,
|
||||
field: "referenced_strings" | "referenced_names",
|
||||
expected: string,
|
||||
normalize: (value: string) => string,
|
||||
): void => {
|
||||
const requireReferencedValue = (dossier, field, expected, normalize) => {
|
||||
const values = z.array(referencedValueSchema).parse(list(dossier, field));
|
||||
const references = z
|
||||
.array(referenceEdgeSchema)
|
||||
@@ -183,10 +157,7 @@ const requireReferencedValue = (
|
||||
};
|
||||
|
||||
/** Require every relationship to be an actual hexadecimal address. */
|
||||
export const requireAddressArray = (
|
||||
input: unknown,
|
||||
operation: string,
|
||||
): string[] => {
|
||||
export const requireAddressArray = (input, operation) => {
|
||||
const parsed = z.array(addressSchema).parse(input);
|
||||
if (parsed.some((address) => address.length === 0))
|
||||
throw new TypeError(`${operation} returned an invalid address list`);
|
||||
@@ -203,21 +174,17 @@ const COLLECTION_FIELDS = [
|
||||
"referenced_strings",
|
||||
"referenced_names",
|
||||
"basic_blocks",
|
||||
] as const;
|
||||
];
|
||||
|
||||
const list = (value: Record<string, unknown>, field: string): unknown[] =>
|
||||
z.array(z.unknown()).parse(value[field]);
|
||||
const list = (value, field) => z.array(z.unknown()).parse(value[field]);
|
||||
|
||||
const validateBlocks = (items: readonly unknown[]): void => {
|
||||
const validateBlocks = (items) => {
|
||||
for (const block of items)
|
||||
if (!Array.isArray(objectValue(block, "Invalid basic block").successors))
|
||||
throw new TypeError("analyze_function omitted CFG successor evidence");
|
||||
};
|
||||
|
||||
const objectValue = (
|
||||
input: unknown,
|
||||
message: string,
|
||||
): Record<string, unknown> => {
|
||||
const objectValue = (input, message) => {
|
||||
const parsed = z.record(z.string(), z.unknown()).safeParse(input);
|
||||
if (!parsed.success) throw new TypeError(message);
|
||||
return parsed.data;
|
||||
@@ -1,17 +1,6 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
/**
|
||||
* `requireFunctionDossierOracle` is only used inside `verifyRealHopperFixture`,
|
||||
* which runs against the real Hopper bridge after `build:cached`. Loading it
|
||||
* lazily keeps this module importable from `test:fast` (which docs promise to
|
||||
* run "without build" — see docs/testing.md § test scripts) so that mock-only
|
||||
* callers such as `requireCurrentDocument` and `resolveFixtureProcedure` do
|
||||
* not require `dist/`.
|
||||
*/
|
||||
const loadFunctionDossierOracle = () =>
|
||||
import("../../dist/application/RealHopperAssertions.js").then(
|
||||
(module) => module.requireFunctionDossierOracle,
|
||||
);
|
||||
import { requireFunctionDossierOracle } from "./real-hopper-assertions.mjs";
|
||||
|
||||
/** Require truthful tri-state permissions from real Hopper memory regions. */
|
||||
export const requireTruthfulMemoryRegions = (segments) => {
|
||||
@@ -99,7 +88,6 @@ export async function verifyRealHopperFixture({
|
||||
oracle,
|
||||
normalizedResult,
|
||||
}) {
|
||||
const requireFunctionDossierOracle = await loadFunctionDossierOracle();
|
||||
const main = await resolveFixtureProcedure(
|
||||
client,
|
||||
options,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { firstProcedureAddress } from "../dist/application/RealHopperAssertions.js";
|
||||
import { firstProcedureAddress } from "./lib/real-hopper-assertions.mjs";
|
||||
import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js";
|
||||
import { ArtifactResourceScope } from "../dist/artifacts/ArtifactResourceScope.js";
|
||||
import { AnalysisCleanupLease } from "../dist/artifacts/AnalysisCleanupLease.js";
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
requireAddressArray,
|
||||
requireFunctionDossier,
|
||||
requirePseudocode,
|
||||
} from "../dist/application/RealHopperAssertions.js";
|
||||
} from "./lib/real-hopper-assertions.mjs";
|
||||
import { HOPPER_PROVIDER_IDENTITY } from "../dist/hopper/HopperProviderCapabilities.js";
|
||||
import { HOPPER_TARGET_LEASE_DIRECTORY } from "../dist/process/HopperLease.js";
|
||||
import { REA_WORKFLOW_PROVIDER } from "../dist/domain/investigationProviders.js";
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
requireFunctionDossierOracle,
|
||||
requireFunctionDossier,
|
||||
requirePseudocode,
|
||||
} from "../../../../src/application/RealHopperAssertions.js";
|
||||
} from "../../../../scripts/lib/real-hopper-assertions.mjs";
|
||||
|
||||
describe("Hopper verifier semantic rejection", () => {
|
||||
it("rejects empty and embedded per-item decompilation failures", () => {
|
||||
|
||||
Reference in new issue
Block a user