Files
reverse-engineering-anything/src/domain/analysisView/machoView.test.ts
T
morluto 3afd8a16ee feat(evidence)!: give Evidence bundles a format revision owned by one module (#10861)
* feat(evidence)!: give Evidence bundles a format revision owned by one module

`src/domain/evidenceFormat.ts` owns Evidence format evolution. Bundles now
carry `evidence_format: 1`; every bundle reader admits only the current
revision and refuses an unmarked or other-revision bundle once, with the
single recreate-observations guidance, instead of admitting earlier shapes
one optional field at a time.

The 6.7-era artifact inventory summary identities are no longer rebuilt:
compare_artifacts checks the current projection only and reports a mismatch
with the owner's guidance. The pre-6.6 identity diagnosis uses the same
guidance. SQLite `worker_timeout_ms` and Dart AOT `string_pool.policy`,
optional only for historical Evidence, are now required.

BREAKING CHANGE: Evidence bundles written by REA 6.x (no `evidence_format`)
are refused by import, comparison, reconstruction and analysis-snapshot
readers as an integrity mismatch telling the caller to recreate the
observations; re-exporting an earlier bundle does not update its format or
identities. Bundles constructed by hand must include `"evidence_format": 1`.
6.7-era artifact summaries are refused by compare_artifacts, and SQLite and
Dart AOT Evidence without the newly required fields fails validation.

* fix(evidence): let the ledger application admit its bundle's format

build_reconstruction_obligation_ledger registered the full bundle schema with the SDK, so an unmarked or other-revision bundle was refused as invalid_request before the format owner ran. Register it like coverage: the application parses the request envelope and admits the bundle.

* feat(pcap)!: read capture Evidence through the current result schema only (#10879)

Delete the pcap read-compatibility schema that defaulted executable identity for historical capture Evidence, and require query_labels_hex (null when unknown) now that every producer writes it. Evidence from an earlier format is refused at the bundle format owner; a record that omits these fields is reported as malformed instead of defaulted.

BREAKING CHANGE: inspect_pcap_capture results always carry query_labels_hex, and analysis views no longer default missing tshark/capinfos identity fields.

* feat(desktop)!: require the effective view policy in desktop state Evidence (#10886)

inspect_desktop_application_state always records the complete policy it used, including sqlite_view_bytes. Drop the optional historical shape and the task draft's absent-policy branch.

BREAKING CHANGE: desktop application state results without effective_view_policy, or with a policy missing sqlite_view_bytes, are refused as malformed.

* feat(android)!: require component declaration facets and reference aggregates in Android Evidence (#10904)

* feat(android)!: require component declaration facets and reference aggregates in Android Evidence

The JADX producers always write the manifest component projection with every declaration facet, target_definition and direct_invokes_coverage. Require them, delete the duplicate observed component schema, drop the unknown declaration coverage that only described historical omission, and give manifest view results their explicit facet.

BREAKING CHANGE: Android package, component and reference results missing these fields are refused as malformed; component declaration_coverage and its summary counts no longer include unknown; manifest view results always carry facet manifest.

* refactor(android): parse package results with the domain schema directly

* feat(browser)!: require the DOM length unit and redirect-scoped prior-hop facts (#10897)

* feat(browser)!: require the DOM length unit and redirect-scoped prior-hop facts

Both CDP producers always declare node_value_length_unit, and the observation session writes redirect_response on every redirect event and no other. Require the unit, drop the diff's absent-unit branch, and admit redirect_response exactly on redirect rows.

BREAKING CHANGE: DOM snapshots without node_value_length_unit and redirect timeline rows without redirect_response are refused as malformed, including inline inspections passed to compare_web_captures.

* docs(browser): drop the historical redirect_response omission note

* feat(native)!: require native Evidence fields that were optional only for historical Evidence (#10946)

Mach-O encryption, slice malformed cause, symbol runtime names and import
facts; ELF export encoding facts and visibility; value-flow limits; load-image
permissions and relocation symbols; Swift type-reference kind and GPU AMD
target metadata are required. Legacy enum values (unrecorded, unknown
visibility, undecodable, modinfo_field_exceeds_limit) and the retained
schemas, view defaults and reinterpretations that served historical Evidence
are deleted.

BREAKING CHANGE: native records missing these fields are refused as
malformed; recreate the observation.
2026-11-15 17:25:31 +08:00

482 lines
14 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { inspectMachoSchema } from "../native/nativeInspection.js";
import { projectAnalysisView } from "./analysisView.js";
import type { AnalysisViewParent } from "./analysisViewParent.js";
import { analysisViewResultSchema } from "./analysisViewResult.js";
it("binds every Mach-O facet to its rows, including empty pages", () => {
for (const facet of [
"entrypoints",
"architectures",
"build_metadata",
"load_commands",
"dependencies",
"imports",
"exports",
"segments",
"provenance",
] as const) {
for (const offset of [0, 999]) {
const projected = projectAnalysisView(parent(), {
kind: "macho",
facet,
offset,
limit: 64,
});
if (!projected.ok) throw projected.error;
expect(analysisViewResultSchema.safeParse(projected.value).success).toBe(
true,
);
if (offset === 0)
expect(
analysisViewResultSchema.safeParse({
...projected.value,
view: {
kind: "macho",
facet: facet === "entrypoints" ? "exports" : "entrypoints",
offset,
limit: 64,
},
}).success,
).toBe(false);
}
}
});
it("refuses symbols and encryption rows missing fields every producer writes", () => {
const { runtime_name: _runtimeName, ...withoutRuntimeName } =
symbol("_malloc");
const current = inspectMachoSchema.parse(report);
const { malformed_kind: _malformedKind, ...withoutCause } =
current.encryption.items[0]!;
for (const [source, path] of [
[
{ ...report, exports: coverage([withoutRuntimeName]) },
["exports", "items", 0, "runtime_name"],
],
[
{ ...report, encryption: coverage([withoutCause]) },
["encryption", "items", 0, "malformed_kind"],
],
[{ ...report, encryption: undefined }, ["encryption"]],
] as const) {
const parsed = inspectMachoSchema.safeParse(source);
expect(parsed.error?.issues.map((issue) => issue.path)).toContainEqual(
path,
);
expect(
projectAnalysisView(parent(source), {
kind: "macho",
facet: "encryption",
offset: 0,
limit: 10,
}).ok,
).toBe(false);
}
});
const symbol = (name: string) => ({
name,
runtime_name: name.slice(1),
address: null,
weak: null,
reexport: null,
source: "libSystem",
});
const importSymbol = (name: string) => ({
...symbol(name),
raw_name: null,
name_status: "exact" as const,
addend: null,
});
const coverage = <T>(items: T[], exhaustive = true) => ({
items,
total: exhaustive ? items.length : null,
exhaustive,
limitations: exhaustive ? [] : ["nm output may omit stripped symbols."],
});
const report = {
format: "mach-o",
selected_architecture: { name: "arm64", container_file_offset: 0 },
endian: null,
word_size: 64,
file_type: "2",
flags: ["0x00200085"],
uuid: "4C4C44E9-5555-3144-A1E9-D858E24A1CCE",
entrypoints: coverage([{ file_offset: 16384 }]),
architectures: coverage([
{
name: "arm64",
cpu_type: "16777228",
cpu_subtype: "0",
file_offset: 0,
size: 4096,
alignment: 14,
},
]),
build_metadata: coverage([
{ platform: "1", minimum_os: "14.0", sdk: "15.0", tools: [] },
]),
load_commands: coverage([{ index: 0, kind: "LC_MAIN", fields: {} }]),
dependencies: coverage([
{
path: "/usr/lib/libSystem.B.dylib",
kind: "LC_LOAD_DYLIB",
current_version: "1351.0.0",
compatibility_version: "1.0.0",
},
]),
imports: coverage(
["_malloc", "_free", "_open"].map((name) => importSymbol(name)),
false,
),
exports: coverage([symbol("__mh_execute_header")]),
segments: coverage([
{
name: "__TEXT",
vm_address: "0x100000000",
vm_size: 16384,
file_offset: 0,
file_size: 16384,
maximum_permissions: {
read: true,
write: false,
execute: true,
raw: "5",
},
initial_permissions: {
read: true,
write: false,
execute: true,
raw: "5",
},
sections: coverage([]),
},
]),
encryption: coverage([
{
architecture: "arm64",
cpu_type: 16777228,
cpu_subtype: 0,
container_file_offset: 0,
size: 4096,
status: "not-encrypted",
malformed_kind: null,
reason: null,
commands: [
{
kind: "LC_ENCRYPTION_INFO_64",
index: 1,
cmdsize: 24,
cryptoff: 16384,
cryptsize: 0,
cryptid: 0,
pad: 0,
},
],
range: {
content: "plaintext",
slice_start: 16384,
slice_end: 16384,
container_start: 16384,
container_end: 16384,
mapping: { status: "mapped", fragments: [], sections: [] },
},
},
]),
provenance: [
{
tool: "nm",
command: ["/usr/bin/nm", "$ARTIFACT"],
tool_version: null,
version_reason: "fixture",
executable_sha256: "a".repeat(64),
exit: { code: 0, signal: null },
stdout_bytes: 10,
stderr_bytes: 0,
},
],
limitations: ["Imports and exports combine dyld_info and nm."],
};
const parent = (
normalizedResult: unknown = report,
subject: AnalysisViewParent["subject"] = {
name: "tool",
digest: { sha256: "b".repeat(64) },
format: "mach-o",
architecture: "arm64",
local_path: "/bin/tool",
},
): AnalysisViewParent => ({
predicateType: "rea.analysis",
evidenceId: `ev_${"c".repeat(64)}`,
operation: "inspect_macho",
normalizedResult,
subject,
limitations: ["Parent limitation."],
});
it("retains binary names and exact numbers alongside partial command fields", () => {
const segment = {
...report.segments.items[0]!,
name: null,
name_bytes_hex: "ff",
vm_size: null,
vm_size_hex: "0x20000000000000",
file_offset: null,
file_offset_hex: "0x20000000000001",
file_size: null,
file_size_hex: "0x20000000000002",
sections: coverage([
{
segment: null,
segment_bytes_hex: "ff",
name: null,
name_bytes_hex: "fe",
address: "0x1",
size: null,
size_hex: "0x20000000000000",
file_offset: 0,
alignment: null,
alignment_exponent: 53,
flags: [],
},
]),
};
const dependency = {
path: null,
path_bytes_hex: "fd",
kind: "LC_LOAD_DYLIB",
current_version: null,
compatibility_version: null,
};
const command = {
index: 0,
kind: "LC_UNKNOWN_0x7fff",
fields: {
command_code: 32767,
command_offset: 184,
cmdsize: 8,
command_sha256: "a".repeat(64),
},
};
const observed = {
...report,
segments: coverage([segment], false),
dependencies: coverage([dependency], false),
load_commands: {
items: [command],
total: 1,
exhaustive: true,
limitations: ["Some binary command fields are not decoded."],
},
};
expect(inspectMachoSchema.safeParse(observed).success).toBe(true);
for (const [facet, expected] of [
["segments", segment],
["dependencies", dependency],
["load_commands", command],
] as const) {
const projected = projectAnalysisView(parent(observed), {
kind: "macho",
facet,
offset: 0,
limit: 64,
});
if (!projected.ok || projected.value.kind !== "macho")
throw new Error("Expected Mach-O page");
expect(projected.value.item).toEqual([expected]);
expect(projected.value.source_exhaustive).toBe(facet === "load_commands");
if (facet === "load_commands")
expect(projected.value.limitations).toContain(
"Some binary command fields are not decoded.",
);
expect(projected.value.unknowns).not.toEqual([]);
expect(analysisViewResultSchema.safeParse(projected.value).success).toBe(
true,
);
}
// Names that decode as UTF-8 carry no exact-byte fields.
expect(inspectMachoSchema.safeParse(report).success).toBe(true);
expect(report.segments.items[0]).not.toHaveProperty("name_bytes_hex");
});
it("preserves ambiguous import facts without adding import fields to exports", () => {
const imported = {
...symbol("unused"),
name: null,
runtime_name: null,
raw_name: "_value+0x4",
name_status: "ambiguous",
addend: null,
};
const observed = { ...report, imports: coverage([imported], false) };
for (const facet of ["imports", "exports"] as const) {
const projected = projectAnalysisView(parent(observed), {
kind: "macho",
facet,
offset: 0,
limit: 64,
});
if (!projected.ok) throw projected.error;
expect(projected.value.kind === "macho" && projected.value.item).toEqual(
observed[facet].items,
);
}
});
describe("Mach-O analysis views", () => {
it("summarizes slice identity and collection counts without rows", () => {
const projected = projectAnalysisView(parent(), { kind: "summary" });
if (!projected.ok) throw projected.error;
expect(projected.value).toMatchObject({
kind: "summary",
parent_operation: "inspect_macho",
artifact: { path: "/bin/tool", sha256: "b".repeat(64) },
summary: {
selected_architecture: { name: "arm64", container_file_offset: 0 },
uuid: report.uuid,
collections: {
imports: { returned: 3, total: null, exhaustive: false },
exports: { returned: 1, total: 1, exhaustive: true },
segments: { returned: 1, total: 1, exhaustive: true },
encryption: { returned: 1, total: 1, exhaustive: true },
},
provenance_count: 1,
limitation_count: 3,
},
coverage: { status: "complete-within-view", examined: 11, total: 11 },
limitations: [
"Imports and exports combine dyld_info and nm.",
"Parent limitation.",
"nm output may omit stripped symbols.",
],
unknowns: [expect.stringContaining("exhaustive imports;")],
});
expect(JSON.stringify(projected.value)).not.toContain("_malloc");
});
it("pages original rows with the producer's own coverage", () => {
const first = projectAnalysisView(parent(), {
kind: "macho",
facet: "imports",
offset: 1,
limit: 1,
});
if (!first.ok) throw first.error;
expect(first.value).toMatchObject({
kind: "macho",
item: [importSymbol("_free")],
source_total: null,
source_exhaustive: false,
coverage: { status: "page", examined: 1, total: 3, next_offset: 2 },
});
expect(first.value.limitations).toContain(
"nm output may omit stripped symbols.",
);
// Rows whose keys include an earlier row shape's keys stay whole.
for (const facet of ["architectures", "segments", "encryption"] as const) {
const page = projectAnalysisView(parent(), {
kind: "macho",
facet,
offset: 0,
limit: 64,
});
if (!page.ok) throw page.error;
expect(page.value.kind === "macho" && page.value.item).toEqual(
report[facet].items,
);
}
const provenance = projectAnalysisView(parent(), {
kind: "macho",
facet: "provenance",
offset: 0,
limit: 64,
});
if (!provenance.ok) throw provenance.error;
expect(provenance.value).toMatchObject({
item: report.provenance,
source_total: 1,
source_exhaustive: true,
coverage: { exhausted: true, next_offset: null },
});
const past = projectAnalysisView(parent(), {
kind: "macho",
facet: "exports",
offset: 5,
limit: 64,
});
if (!past.ok) throw past.error;
expect(past.value.coverage).toMatchObject({
status: "empty",
exhausted: true,
});
});
});
describe("Mach-O view uncertainty", () => {
it("keeps a missing subject unknown instead of inventing identity", () => {
const projected = projectAnalysisView(parent(report, null), {
kind: "summary",
});
if (!projected.ok) throw projected.error;
expect(projected.value.artifact).toEqual({ path: "", sha256: null });
expect(projected.value.unknowns).toContain(
"The parent has no recorded subject, so the inspected artifact path and SHA-256 are unknown.",
);
});
it("rejects other view kinds and malformed retained results", () => {
const wrongKind = projectAnalysisView(parent(), {
kind: "wasm",
facet: "exports",
offset: 0,
limit: 64,
});
if (wrongKind.ok) throw new Error("expected a view-kind refusal");
expect(wrongKind.error).toMatchObject({
_tag: "AnalysisInputError",
issues: [
{
path: ["view"],
message: expect.stringMatching(/summary and macho views/iu),
},
],
});
const contradictory = projectAnalysisView(
parent(report, {
name: "tool",
digest: { sha256: "b".repeat(64) },
format: "elf",
architecture: null,
local_path: "/bin/tool",
}),
{ kind: "summary" },
);
if (contradictory.ok) throw new Error("expected a subject refusal");
expect(contradictory.error).toMatchObject({
issues: [{ path: ["source", "subject"] }],
});
const malformed = projectAnalysisView(
parent({ ...report, imports: { items: "x" } }),
{ kind: "summary" },
);
expect(malformed.ok).toBe(false);
if (!malformed.ok) expect(malformed.error._tag).toBe("AnalysisInputError");
});
});
it("validates a prototype-named command field rather than dropping an invalid value", () => {
const fields =
inspectMachoSchema.shape.load_commands.shape.items.element.shape.fields;
const parsed = fields.safeParse(
Object.fromEntries([["__proto__", { invalid: true }]]),
);
expect(parsed.success).toBe(false);
if (parsed.success) throw new Error("Expected an invalid command field");
expect(parsed.error.issues).toContainEqual(
expect.objectContaining({ path: ["__proto__"] }),
);
});