mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
fix(ghidra): preserve PE PDB coverage facts (#2933)
Preserve reported Program PDB identity, explicit omissions and nullable load state, retaining declared CodeView facts when equivalence is unknown. Bound JSON locator expansion and advance PE/workflow replay revisions without discarding historical Evidence.
Main already contains the shared canonical workflow collector. Integrate its unknown-PDB repair and fix the CLI fixture for the new artifact-session dependency. No unrelated pcap repair is included.
Validation: 309 focused tests across 15 files pass (2 Windows-only skips), runtime build and check:fast/pre-push pass. Real Windows Ghidra 12.1.4 PUBLIC/JDK 21.0.12.1 uses the public where.exe/PDB pair; 17 relevant production files match verified ca24903f. MCP cold/warm normalized results match and the backward graph has 3 successful nodes. Inputs are unchanged and no owned Java/Node processes remain. CLI inspection keeps its earlier exact-source scope. NB10 omission uses regression fixtures, without a real NB10 provider claim.
Tests keep 2-core affinity and 512 MiB Node/Java heaps. Retain the 600s analysis and 30s initialization timeout observations alongside the successful test-only extended-budget run; no product-default or cross-revision speedup claim.
Fixes #2259
This commit is contained in:
@@ -19,12 +19,12 @@ export const workflowAnalysisProfile = (
|
||||
createAnalysisProfile(REA_WORKFLOW_PROVIDER, {
|
||||
upstream_analysis_profile: upstream,
|
||||
...(operation === "binary_overview"
|
||||
? { workflow_revision: 3 }
|
||||
? { workflow_revision: 4 }
|
||||
: operation === "batch_decompile" ||
|
||||
operation === "get_call_graph" ||
|
||||
operation === "trace_feature"
|
||||
? { workflow_revision: 2 }
|
||||
: {}),
|
||||
? { workflow_revision: 3 }
|
||||
: { workflow_revision: 1 }),
|
||||
});
|
||||
|
||||
/** Provider identity for deterministic artifact inventories. */
|
||||
|
||||
@@ -219,7 +219,7 @@ export const resolveGhidraAnalysisProfile = async (
|
||||
].sort(),
|
||||
import_mode: "ephemeral-source-immutable",
|
||||
...(target.format === "pe"
|
||||
? { pdb_observations: "program-info-v1" }
|
||||
? { pdb_observations: "program-info-v5" }
|
||||
: {}),
|
||||
annotation_policy: "atomic-function-entry-metadata-v1",
|
||||
load_image_observations: "source-mappings-entry-context-v2",
|
||||
|
||||
@@ -1,96 +1,297 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { expect, it } from "vitest";
|
||||
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
|
||||
import { ghidraPeCodeViewLimitations } from "./GhidraPeProfile.js";
|
||||
import { ghidraPePdbLimitations } from "./GhidraPeProfile.js";
|
||||
import {
|
||||
ghidraPdbLimitations,
|
||||
type GhidraSessionInfo,
|
||||
} from "./GhidraSessionValues.js";
|
||||
|
||||
describe("ghidraPeCodeViewLimitations", () => {
|
||||
it("returns empty limitations for non-PE targets", () => {
|
||||
const elfTarget: BinaryTarget = {
|
||||
path: "/test/file",
|
||||
sha256: "abc",
|
||||
kind: "executable",
|
||||
format: "elf",
|
||||
architecture: "x86_64",
|
||||
availableArchitectures: ["x86_64"],
|
||||
const target: BinaryTarget = {
|
||||
path: "/fixture/public.exe",
|
||||
sha256: "a".repeat(64),
|
||||
kind: "executable",
|
||||
format: "pe",
|
||||
architecture: "x86_64",
|
||||
availableArchitectures: ["x86_64"],
|
||||
executableRole: "application",
|
||||
managed: false,
|
||||
codeView: {
|
||||
pdbPath: "public.pdb",
|
||||
pdbFileName: "public.pdb",
|
||||
guid: "01234567-89ab-cdef-0123-456789abcdef",
|
||||
age: 10,
|
||||
},
|
||||
};
|
||||
const absentAttributes = {
|
||||
file: null,
|
||||
guid: null,
|
||||
age: null,
|
||||
signature: null,
|
||||
version: null,
|
||||
loaded: null,
|
||||
};
|
||||
|
||||
it.each(["file", "guid", "age"] as const)(
|
||||
"retains declared identity when only reported %s is omitted",
|
||||
(attribute) => {
|
||||
const pdb: GhidraSessionInfo["pdb"] = {
|
||||
file: "program.pdb",
|
||||
guid: "reported-guid",
|
||||
age: "a",
|
||||
signature: null,
|
||||
version: "RSDS",
|
||||
loaded: null,
|
||||
resource_limit: { omitted_attributes: [attribute], utf16_budget: 4096 },
|
||||
[attribute]: null,
|
||||
};
|
||||
expect(ghidraPeCodeViewLimitations(elfTarget)).toEqual([]);
|
||||
});
|
||||
const caveats = ghidraPePdbLimitations(target, pdb).join(" ");
|
||||
expect(caveats).toContain("omitted");
|
||||
expect(caveats).toContain('PDB "public.pdb"');
|
||||
expect(caveats).toContain("01234567-89ab-cdef-0123-456789abcdef");
|
||||
expect(caveats).toContain("load state unknown");
|
||||
},
|
||||
);
|
||||
|
||||
it("does not claim CodeView symbols were unapplied when the session loaded a PDB", () => {
|
||||
const peTarget: BinaryTarget = {
|
||||
path: "/test/file.exe",
|
||||
sha256: "abc",
|
||||
kind: "executable",
|
||||
architecture: "x86_64",
|
||||
availableArchitectures: ["x86_64"],
|
||||
executableRole: "application",
|
||||
managed: false,
|
||||
format: "pe",
|
||||
it.each([undefined, absentAttributes])(
|
||||
"retains declared identity and unknown load state when reported attributes are %j",
|
||||
(pdb) => {
|
||||
const caveats = ghidraPePdbLimitations(target, pdb);
|
||||
expect(caveats).toHaveLength(1);
|
||||
expect(caveats[0]).toContain(
|
||||
'PDB "public.pdb" (GUID 01234567-89ab-cdef-0123-456789abcdef, age (decimal) 10)',
|
||||
);
|
||||
expect(caveats[0]).toContain("load state unknown");
|
||||
expect(caveats[0]).not.toContain("were not applied");
|
||||
expect(caveats[0]).not.toContain("names are analysis-generated");
|
||||
},
|
||||
);
|
||||
|
||||
it.each([true, false, null])(
|
||||
"uses reported PDB observations once with loaded = %s",
|
||||
(loaded) => {
|
||||
const pdb = {
|
||||
file: "public.pdb",
|
||||
guid: "{01234567-89AB-CDEF-0123-456789ABCDEF}",
|
||||
age: "0X000A",
|
||||
signature: null,
|
||||
version: "RSDS",
|
||||
loaded,
|
||||
};
|
||||
const caveats = ghidraPePdbLimitations(target, pdb);
|
||||
expect(caveats).toEqual(ghidraPdbLimitations(pdb));
|
||||
expect(caveats).toHaveLength(1);
|
||||
expect(caveats.join(" ")).not.toContain('PDB "public.pdb"');
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{ file: "different.pdb", guid: "0123456789abcdef0123456789abcdef", age: "a" },
|
||||
{ file: "public.pdb", guid: "1123456789abcdef0123456789abcdef", age: "a" },
|
||||
{ file: "public.pdb", guid: "0123456789abcdef0123456789abcdef", age: "10" },
|
||||
{ file: "public.pdb", guid: "unrecognized-guid", age: "a" },
|
||||
{ file: "public.pdb", guid: "0123456789abcdef0123456789abcdef", age: "ten" },
|
||||
])("retains non-equivalent declared PDB identity for %j", (identity) => {
|
||||
for (const loaded of [false, null, true]) {
|
||||
const pdb = { ...absentAttributes, ...identity, version: "RSDS", loaded };
|
||||
const caveats = ghidraPePdbLimitations(target, pdb);
|
||||
expect(caveats[0]).toBe(ghidraPdbLimitations(pdb)[0]);
|
||||
expect(caveats[0]).toContain(identity.file);
|
||||
expect(caveats[0]).toContain(identity.guid);
|
||||
expect(caveats[0]).toContain(`age (hex) ${identity.age}`);
|
||||
expect(caveats).toHaveLength(2);
|
||||
expect(caveats[1]).toContain('PDB "public.pdb"');
|
||||
expect(caveats[1]).toContain("01234567-89ab-cdef-0123-456789abcdef");
|
||||
expect(caveats[1]).toContain("age (decimal) 10");
|
||||
expect(caveats[1]).toContain("equivalence is not established");
|
||||
expect(pdb).toMatchObject({ ...identity, loaded });
|
||||
}
|
||||
});
|
||||
|
||||
it.each([
|
||||
"0123456789ABCDEF0123456789ABCDEF",
|
||||
"01234567-89ab-cdef-0123-456789abcdef",
|
||||
])("avoids duplicate identity for normalized matching GUID %s", (guid) => {
|
||||
const pdb = { ...absentAttributes, file: "public.pdb", guid, age: "a" };
|
||||
expect(ghidraPePdbLimitations(target, pdb)).toEqual(
|
||||
ghidraPdbLimitations(pdb),
|
||||
);
|
||||
});
|
||||
|
||||
it("retains explicit failure to load without asserting all names are generated", () => {
|
||||
const caveats = ghidraPePdbLimitations(target, {
|
||||
...absentAttributes,
|
||||
loaded: false,
|
||||
});
|
||||
expect(caveats[0]).toContain("reports not loaded");
|
||||
expect(caveats[0]).not.toContain("names are analysis-generated");
|
||||
});
|
||||
|
||||
it("retains a loaded-state attestation caveat even when reference attributes are absent", () => {
|
||||
const caveats = ghidraPePdbLimitations(target, {
|
||||
...absentAttributes,
|
||||
loaded: true,
|
||||
});
|
||||
expect(caveats[0]).toEqual(
|
||||
ghidraPdbLimitations({ ...absentAttributes, loaded: true })[0],
|
||||
);
|
||||
expect(caveats).toHaveLength(2);
|
||||
expect(caveats[1]).toContain('PDB "public.pdb"');
|
||||
expect(caveats[0]).toContain("not an independent PDB identity");
|
||||
expect(caveats[0]).not.toContain("reports not loaded");
|
||||
});
|
||||
|
||||
it("preserves omitted-attribute resource coverage with declared fallback identity", () => {
|
||||
const pdb: GhidraSessionInfo["pdb"] = {
|
||||
...absentAttributes,
|
||||
resource_limit: { omitted_attributes: ["file"], utf16_budget: 4096 },
|
||||
};
|
||||
const caveats = ghidraPePdbLimitations(target, pdb);
|
||||
expect(caveats).toHaveLength(2);
|
||||
expect(caveats[0]).toContain("omitted");
|
||||
expect(caveats[1]).toContain("public.pdb");
|
||||
});
|
||||
|
||||
it("reports NB10 declared age without inventing a GUID", () => {
|
||||
const caveats = ghidraPePdbLimitations(
|
||||
{
|
||||
...target,
|
||||
codeView: {
|
||||
pdbPath: "C:\\symbols.pdb",
|
||||
pdbFileName: "symbols.pdb",
|
||||
guid: "12345678-1234-1234-1234-123456789abc",
|
||||
age: 1,
|
||||
pdbPath: "legacy.pdb",
|
||||
pdbFileName: "legacy.pdb",
|
||||
guid: null,
|
||||
age: 2,
|
||||
},
|
||||
};
|
||||
expect(ghidraPeCodeViewLimitations(peTarget, true)).toEqual([]);
|
||||
expect(ghidraPeCodeViewLimitations(peTarget, undefined)).toEqual([]);
|
||||
});
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
expect(caveats[0]).toContain('PDB "legacy.pdb" (age (decimal) 2)');
|
||||
expect(caveats[0]).not.toContain("GUID");
|
||||
});
|
||||
|
||||
it("returns empty limitations for PE target without codeView", () => {
|
||||
const peTarget: BinaryTarget = {
|
||||
path: "/test/file.exe",
|
||||
sha256: "def",
|
||||
kind: "executable",
|
||||
format: "pe",
|
||||
architecture: "x86_64",
|
||||
availableArchitectures: ["x86_64"],
|
||||
executableRole: "application",
|
||||
managed: false,
|
||||
};
|
||||
expect(ghidraPeCodeViewLimitations(peTarget)).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns limitation with GUID and age for RSDS CodeView records", () => {
|
||||
const peTarget: BinaryTarget = {
|
||||
path: "/test/file.exe",
|
||||
sha256: "def",
|
||||
kind: "executable",
|
||||
format: "pe",
|
||||
architecture: "x86_64",
|
||||
availableArchitectures: ["x86_64"],
|
||||
executableRole: "application",
|
||||
managed: false,
|
||||
it.each([false, null, true])(
|
||||
"keeps an omitted NB10 GUID unknown with loaded = %s",
|
||||
(loaded) => {
|
||||
const pdbPath = "a".repeat(4070) + "\\legacy.pdb";
|
||||
const legacy = {
|
||||
...target,
|
||||
codeView: {
|
||||
pdbPath: "C:\\build\\app.pdb",
|
||||
pdbFileName: "app.pdb",
|
||||
guid: "01234567-89ab-cdef-0123-456789abcdef",
|
||||
age: 1,
|
||||
},
|
||||
};
|
||||
expect(ghidraPeCodeViewLimitations(peTarget, false)).toEqual([
|
||||
'The target references debug symbols "app.pdb" (GUID 01234567-89ab-cdef-0123-456789abcdef, age 1) that were not applied; procedure names are analysis-generated.',
|
||||
]);
|
||||
});
|
||||
|
||||
it("returns limitation without GUID for NB10 CodeView records", () => {
|
||||
const peTarget: BinaryTarget = {
|
||||
path: "/test/file.exe",
|
||||
sha256: "def",
|
||||
kind: "executable",
|
||||
format: "pe",
|
||||
architecture: "x86",
|
||||
availableArchitectures: ["x86"],
|
||||
executableRole: "application",
|
||||
managed: false,
|
||||
codeView: {
|
||||
pdbPath: "D:\\test\\legacy.pdb",
|
||||
pdbPath,
|
||||
pdbFileName: "legacy.pdb",
|
||||
guid: null,
|
||||
age: 2,
|
||||
},
|
||||
};
|
||||
expect(ghidraPeCodeViewLimitations(peTarget, false)).toEqual([
|
||||
'The target references debug symbols "legacy.pdb" (age 2) that were not applied; procedure names are analysis-generated.',
|
||||
const pdb = {
|
||||
...absentAttributes,
|
||||
file: pdbPath,
|
||||
age: "2",
|
||||
signature: "01234567",
|
||||
version: "NB10",
|
||||
loaded,
|
||||
};
|
||||
expect(ghidraPePdbLimitations(legacy, pdb)).toEqual(
|
||||
ghidraPdbLimitations(pdb),
|
||||
);
|
||||
const omitted: GhidraSessionInfo["pdb"] = {
|
||||
...pdb,
|
||||
resource_limit: { omitted_attributes: ["guid"], utf16_budget: 4096 },
|
||||
};
|
||||
const caveats = ghidraPePdbLimitations(legacy, omitted);
|
||||
expect(caveats).toEqual([
|
||||
...ghidraPdbLimitations(omitted),
|
||||
expect.stringContaining('PDB "legacy.pdb" (age (decimal) 2)'),
|
||||
]);
|
||||
});
|
||||
expect(caveats.at(-1)).toContain("equivalence is not established");
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["signature", "version"] as const)(
|
||||
"keeps matching NB10 identity when only %s is omitted",
|
||||
(attribute) => {
|
||||
const legacy = {
|
||||
...target,
|
||||
codeView: {
|
||||
pdbPath: "legacy.pdb",
|
||||
pdbFileName: "legacy.pdb",
|
||||
guid: null,
|
||||
age: 2,
|
||||
},
|
||||
};
|
||||
const pdb: GhidraSessionInfo["pdb"] = {
|
||||
...absentAttributes,
|
||||
file: "legacy.pdb",
|
||||
age: "2",
|
||||
resource_limit: { omitted_attributes: [attribute], utf16_budget: 4096 },
|
||||
};
|
||||
expect(ghidraPePdbLimitations(legacy, pdb)).toEqual(
|
||||
ghidraPdbLimitations(pdb),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it("retains missing-report uncertainty without a declared CodeView record", () => {
|
||||
const { codeView: _, ...withoutReference } = target;
|
||||
expect(ghidraPePdbLimitations(withoutReference, undefined)).toEqual(
|
||||
ghidraPdbLimitations(undefined),
|
||||
);
|
||||
});
|
||||
|
||||
it("adds no PDB warning for a non-PE target", () => {
|
||||
const elf: BinaryTarget = {
|
||||
path: "/fixture/program",
|
||||
sha256: "a".repeat(64),
|
||||
kind: "executable",
|
||||
format: "elf",
|
||||
architecture: "x86_64",
|
||||
availableArchitectures: ["x86_64"],
|
||||
};
|
||||
expect(ghidraPePdbLimitations(elf, undefined)).toEqual([]);
|
||||
});
|
||||
|
||||
it("bounds an escaped inline locator while preserving its declared source value", () => {
|
||||
const name = '"'.repeat(5000) + ".pdb";
|
||||
const selected = {
|
||||
...target,
|
||||
codeView: { ...target.codeView!, pdbFileName: name },
|
||||
};
|
||||
const caveats = ghidraPePdbLimitations(selected, undefined).join(" ");
|
||||
expect(Buffer.byteLength(caveats)).toBeLessThan(1024);
|
||||
expect(caveats).toContain("locator omitted");
|
||||
expect(caveats).toContain("01234567-89ab-cdef-0123-456789abcdef");
|
||||
expect(selected.codeView.pdbFileName).toBe(name);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["long ASCII", "a".repeat(171)],
|
||||
["ASCII at the byte budget", "a".repeat(1022)],
|
||||
["ASCII beyond the byte budget", "a".repeat(1023)],
|
||||
["quotes at the byte budget", '"'.repeat(511)],
|
||||
["quotes beyond the byte budget", '"'.repeat(512)],
|
||||
["short control escapes", "\n".repeat(511)],
|
||||
["short control escapes beyond the byte budget", "\n".repeat(512)],
|
||||
["six-byte control escapes", "\u0001".repeat(170)],
|
||||
["six-byte control escapes beyond the byte budget", "\u0001".repeat(171)],
|
||||
["UTF-8 BMP text", "中".repeat(340)],
|
||||
["UTF-8 BMP text beyond the byte budget", "中".repeat(341)],
|
||||
["surrogate pairs", "😀".repeat(255)],
|
||||
["surrogate pairs beyond the byte budget", "😀".repeat(256)],
|
||||
["unpaired high surrogates", "\ud800".repeat(170)],
|
||||
["unpaired high surrogates beyond the byte budget", "\ud800".repeat(171)],
|
||||
["unpaired low surrogates", "\udfff".repeat(170)],
|
||||
["unpaired low surrogates beyond the byte budget", "\udfff".repeat(171)],
|
||||
])("uses actual JSON UTF-8 size for %s", (_, name) => {
|
||||
const selected = {
|
||||
...target,
|
||||
codeView: { ...target.codeView!, pdbFileName: name },
|
||||
};
|
||||
const caveat = ghidraPePdbLimitations(selected, undefined).join(" ");
|
||||
const encoded = JSON.stringify(name);
|
||||
if (Buffer.byteLength(encoded) <= 1024) {
|
||||
expect(caveat).toContain(`PDB ${encoded} (`);
|
||||
expect(caveat).not.toContain("locator omitted");
|
||||
} else {
|
||||
expect(caveat).toContain("locator omitted");
|
||||
expect(caveat).not.toContain(encoded);
|
||||
}
|
||||
expect(selected.codeView.pdbFileName).toBe(name);
|
||||
});
|
||||
|
||||
+110
-25
@@ -1,28 +1,113 @@
|
||||
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
|
||||
import type {
|
||||
BinaryTarget,
|
||||
PeCodeViewRecord,
|
||||
} from "../domain/binaryTargetTypes.js";
|
||||
import {
|
||||
ghidraPdbLimitations,
|
||||
type GhidraSessionInfo,
|
||||
} from "./GhidraSessionValues.js";
|
||||
|
||||
/**
|
||||
* Report unapplied PE companion PDB symbols when CodeView debug information is present.
|
||||
*
|
||||
* Ghidra does not automatically locate or apply sidecar PDBs in REA's isolated
|
||||
* runtime directories without companion admission. When the PE target header
|
||||
* references debug symbols, surface this limitation so callers understand that
|
||||
* procedure names remain analysis-generated.
|
||||
*/
|
||||
export const ghidraPeCodeViewLimitations = (
|
||||
target: BinaryTarget,
|
||||
pdbLoaded?: boolean | null,
|
||||
): readonly string[] => {
|
||||
const PDB_LOCATOR_INLINE_BYTE_BUDGET = 1024;
|
||||
|
||||
/** Establish equivalence only for the declared fields and recognized formats. */
|
||||
const reportedPdbMatchesDeclaration = (
|
||||
declared: PeCodeViewRecord,
|
||||
reported: NonNullable<GhidraSessionInfo["pdb"]>,
|
||||
): boolean => {
|
||||
if (
|
||||
target.format !== "pe" ||
|
||||
target.codeView === undefined ||
|
||||
pdbLoaded !== false
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
const { pdbFileName, guid, age } = target.codeView;
|
||||
return [
|
||||
guid !== null
|
||||
? `The target references debug symbols "${pdbFileName}" (GUID ${guid}, age ${age}) that were not applied; procedure names are analysis-generated.`
|
||||
: `The target references debug symbols "${pdbFileName}" (age ${age}) that were not applied; procedure names are analysis-generated.`,
|
||||
];
|
||||
reported.resource_limit?.omitted_attributes.some(
|
||||
(attribute) =>
|
||||
attribute === "file" || attribute === "guid" || attribute === "age",
|
||||
) ||
|
||||
(reported.file !== declared.pdbPath &&
|
||||
reported.file !== declared.pdbFileName) ||
|
||||
reported.age === null ||
|
||||
!/^(?:0x)?[0-9a-f]+$/iu.test(reported.age) ||
|
||||
Number.parseInt(reported.age, 16) !== declared.age
|
||||
)
|
||||
return false;
|
||||
if (declared.guid === null) return reported.guid === null;
|
||||
if (reported.guid === null) return false;
|
||||
let guid = reported.guid;
|
||||
if (guid.startsWith("{") && guid.endsWith("}")) guid = guid.slice(1, -1);
|
||||
if (
|
||||
!/^(?:[0-9a-f]{32}|[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12})$/iu.test(
|
||||
guid,
|
||||
)
|
||||
)
|
||||
return false;
|
||||
return (
|
||||
guid.replaceAll("-", "").toLowerCase() ===
|
||||
declared.guid.replaceAll("-", "").toLowerCase()
|
||||
);
|
||||
};
|
||||
|
||||
/** Measure JSON quotes, escapes and UTF-8 without allocating the encoded string. */
|
||||
const pdbLocatorFitsInlineBudget = (value: string): boolean => {
|
||||
if (value.length * 6 + 2 <= PDB_LOCATOR_INLINE_BYTE_BUDGET) return true;
|
||||
if (value.length + 2 > PDB_LOCATOR_INLINE_BYTE_BUDGET) return false;
|
||||
let bytes = 2;
|
||||
for (let index = 0; index < value.length; index += 1) {
|
||||
const point = value.codePointAt(index) ?? 0;
|
||||
if (point > 0xffff) {
|
||||
bytes += 4;
|
||||
index += 1;
|
||||
} else if (point >= 0xd800 && point <= 0xdfff) {
|
||||
// JSON.stringify escapes unpaired UTF-16 surrogates as six ASCII bytes.
|
||||
bytes += 6;
|
||||
} else if (point < 0x20) {
|
||||
bytes +=
|
||||
point === 8 ||
|
||||
point === 9 ||
|
||||
point === 10 ||
|
||||
point === 12 ||
|
||||
point === 13
|
||||
? 2
|
||||
: 6;
|
||||
} else if (point === 0x22 || point === 0x5c) {
|
||||
bytes += 2;
|
||||
} else {
|
||||
bytes += point < 0x80 ? 1 : point < 0x800 ? 2 : 3;
|
||||
}
|
||||
if (bytes > PDB_LOCATOR_INLINE_BYTE_BUDGET) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
/** Use reported Program facts first, retaining declared PE facts as a fallback. */
|
||||
export const ghidraPePdbLimitations = (
|
||||
target: BinaryTarget,
|
||||
pdb: GhidraSessionInfo["pdb"],
|
||||
): readonly string[] => {
|
||||
if (target.format !== "pe") return [];
|
||||
const reported = ghidraPdbLimitations(pdb);
|
||||
if (
|
||||
target.codeView === undefined ||
|
||||
(pdb !== undefined && reportedPdbMatchesDeclaration(target.codeView, pdb))
|
||||
)
|
||||
return reported;
|
||||
const { pdbFileName, guid, age } = target.codeView;
|
||||
// Include JSON escaping in the inline budget before allocating that representation.
|
||||
const locator = pdbLocatorFitsInlineBudget(pdbFileName)
|
||||
? JSON.stringify(pdbFileName)
|
||||
: `(declared locator omitted from this inline caveat: ${PDB_LOCATOR_INLINE_BYTE_BUDGET}-byte resource budget)`;
|
||||
const identity =
|
||||
guid === null
|
||||
? `age (decimal) ${age}`
|
||||
: `GUID ${guid}, age (decimal) ${age}`;
|
||||
const state =
|
||||
pdb === undefined
|
||||
? "Ghidra did not report PDB attributes; load state unknown"
|
||||
: pdb.loaded === false
|
||||
? "Ghidra reports not loaded"
|
||||
: pdb.loaded === true
|
||||
? "Ghidra reports a loaded PDB; this declared identity is not an independent matching-PDB attestation"
|
||||
: "Ghidra reports load state unknown";
|
||||
const comparison =
|
||||
pdb !== undefined &&
|
||||
[pdb.file, pdb.guid, pdb.age].some((value) => value !== null)
|
||||
? " Reported and declared PDB observations are retained separately; equivalence is not established."
|
||||
: "";
|
||||
const declared = `The PE CodeView record references PDB ${locator} (${identity}); ${state}.${comparison} Original PDB names and types may be missing. Use list_procedures and address-based analysis to inspect recovered functions. REA does not admit a caller-selected PDB companion.`;
|
||||
return pdb === undefined ? [declared] : [...reported, declared];
|
||||
};
|
||||
|
||||
@@ -35,6 +35,11 @@ import { silentLogger } from "../logger.js";
|
||||
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import { ProviderStartupDeadline } from "../process/ProviderDeadline.js";
|
||||
import { createAnalysisProfile } from "../domain/analysisProfile.js";
|
||||
import { createAnalysisExecution } from "../application/AnalysisProvider.js";
|
||||
import { AnalysisSnapshotCache } from "../application/binary/AnalysisSnapshotCache.js";
|
||||
import { createEvidence } from "../domain/evidence.js";
|
||||
import { createEvidenceBundle } from "../domain/evidenceBundle.js";
|
||||
|
||||
const INSTALL = "/opt/ghidra_12.1.4_PUBLIC";
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
@@ -81,6 +86,69 @@ const createElfClient = async (ghidra: GhidraProvider) => {
|
||||
return ghidra.createClient(target, resolved.value.profile);
|
||||
};
|
||||
|
||||
it.each([
|
||||
"program-info-v1",
|
||||
"program-info-v2",
|
||||
"program-info-v3",
|
||||
"program-info-v4",
|
||||
])(
|
||||
"retains historical PE PDB Evidence for %s without replaying its old projection",
|
||||
async (pdbObservations) => {
|
||||
const target = peTarget("x86_64");
|
||||
const resolved = await provider().resolveAnalysisProfile(target);
|
||||
if (!resolved.ok || resolved.value.profile === null)
|
||||
throw new Error("Expected admitted PE profile");
|
||||
const current = resolved.value.profile;
|
||||
const prior = createAnalysisProfile(current.provider, {
|
||||
...current.parameters,
|
||||
pdb_observations: pdbObservations,
|
||||
});
|
||||
const operation = "search_procedures";
|
||||
const parameters = { pattern: "wmain" };
|
||||
const limitations = [
|
||||
pdbObservations === "program-info-v4"
|
||||
? "Legacy projection discarded NB10 declaration when the reported GUID was omitted."
|
||||
: "Legacy projection collapsed unknown PDB load state.",
|
||||
];
|
||||
const cache = new AnalysisSnapshotCache();
|
||||
cache.record({
|
||||
target,
|
||||
profile: prior,
|
||||
operation,
|
||||
parameters,
|
||||
execution: createAnalysisExecution([], prior.provider, {
|
||||
subject: target,
|
||||
analysisProfile: prior,
|
||||
limitations,
|
||||
}),
|
||||
});
|
||||
const evidence = createEvidence(target, prior.provider, {
|
||||
operation,
|
||||
parameters,
|
||||
result: [],
|
||||
analysisProfile: prior,
|
||||
limitations,
|
||||
});
|
||||
const exported = cache.export(
|
||||
target,
|
||||
prior,
|
||||
createEvidenceBundle([evidence]),
|
||||
);
|
||||
if (!exported.ok) throw exported.error;
|
||||
const imported = new AnalysisSnapshotCache();
|
||||
expect(imported.import(exported.value, undefined, () => ok(0)).ok).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
imported.lookup(target, current, operation, parameters),
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
imported.lookup(target, prior, operation, parameters)?.limitations,
|
||||
).toEqual(expect.arrayContaining(limitations));
|
||||
expect(exported.value.evidence_bundle.records).toContainEqual(evidence);
|
||||
},
|
||||
);
|
||||
|
||||
describe("Ghidra provider", () => {
|
||||
it("commits DOS loader and real-mode language while retaining the CPU family", async () => {
|
||||
const ghidra = provider();
|
||||
@@ -957,43 +1025,31 @@ describe("Ghidra result projection", () => {
|
||||
|
||||
describe("Ghidra PE CodeView debug symbol limitations", () => {
|
||||
it.each([
|
||||
{
|
||||
name: "omitted PDB facts",
|
||||
loaded: undefined,
|
||||
expectedLimitation:
|
||||
"Ghidra did not report PDB attributes; debug-symbol coverage is unknown.",
|
||||
forbiddenLimitation: "that were not applied",
|
||||
},
|
||||
{
|
||||
name: "an explicit not-loaded report",
|
||||
loaded: false,
|
||||
expectedLimitation:
|
||||
'The target references debug symbols "test.pdb" (GUID 01234567-89ab-cdef-0123-456789abcdef, age 1) that were not applied; procedure names are analysis-generated.',
|
||||
forbiddenLimitation: "coverage is unknown",
|
||||
},
|
||||
{
|
||||
name: "an explicit loaded report",
|
||||
loaded: true,
|
||||
expectedLimitation: "Ghidra reports a loaded PDB",
|
||||
forbiddenLimitation: "that were not applied",
|
||||
},
|
||||
])(
|
||||
"preserves CodeView coverage state for $name in session Evidence",
|
||||
async ({ loaded, expectedLimitation, forbiddenLimitation }) => {
|
||||
const pdb = {
|
||||
file: "C:\\projects\\app\\build\\test.pdb",
|
||||
guid: "01234567-89ab-cdef-0123-456789abcdef",
|
||||
age: "1",
|
||||
signature: null,
|
||||
version: "RSDS",
|
||||
loaded: loaded ?? null,
|
||||
};
|
||||
["absent report", undefined, "load state unknown"],
|
||||
["unknown load", null, "load state unknown"],
|
||||
["not loaded", false, "reports not loaded"],
|
||||
["loaded", true, "reports a loaded PDB"],
|
||||
] as const)(
|
||||
"preserves %s in operation and health Evidence without duplicate caveats",
|
||||
async (_, loaded, expected) => {
|
||||
const ghidra = provider(installationHost(), () => ({
|
||||
start: () =>
|
||||
Promise.resolve(
|
||||
ok(
|
||||
loaded === undefined ? sessionInfo() : { ...sessionInfo(), pdb },
|
||||
),
|
||||
ok({
|
||||
...sessionInfo(),
|
||||
...(loaded === undefined
|
||||
? {}
|
||||
: {
|
||||
pdb: {
|
||||
file: "test.pdb",
|
||||
guid: "01234567-89ab-cdef-0123-456789abcdef",
|
||||
age: "1",
|
||||
signature: null,
|
||||
version: "RSDS",
|
||||
loaded,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
),
|
||||
callTool: () => Promise.resolve(ok([])),
|
||||
close: () => Promise.resolve(ok(null)),
|
||||
@@ -1001,7 +1057,7 @@ describe("Ghidra PE CodeView debug symbol limitations", () => {
|
||||
const target = {
|
||||
...peTarget("x86_64"),
|
||||
codeView: {
|
||||
pdbPath: "C:\\projects\\app\\build\\test.pdb",
|
||||
pdbPath: "test.pdb",
|
||||
pdbFileName: "test.pdb",
|
||||
guid: "01234567-89ab-cdef-0123-456789abcdef",
|
||||
age: 1,
|
||||
@@ -1019,9 +1075,13 @@ describe("Ghidra PE CodeView debug symbol limitations", () => {
|
||||
const health = await client.execute("health", {});
|
||||
if (!health.ok) throw health.error;
|
||||
for (const observation of [search.value, health.value]) {
|
||||
const limitations = observation.limitations.join("\n");
|
||||
expect(limitations).toContain(expectedLimitation);
|
||||
expect(limitations).not.toContain(forbiddenLimitation);
|
||||
const caveats = observation.limitations.filter((limit) =>
|
||||
limit.includes("PDB"),
|
||||
);
|
||||
expect(caveats).toHaveLength(1);
|
||||
expect(caveats[0]).toContain(expected);
|
||||
expect(caveats[0]).not.toContain("names are analysis-generated");
|
||||
expect(caveats[0]).not.toContain("that were not applied");
|
||||
}
|
||||
} finally {
|
||||
await client.close();
|
||||
|
||||
@@ -73,7 +73,7 @@ import {
|
||||
isGhidraMipsSoftFloatTarget,
|
||||
} from "./GhidraMipsProfile.js";
|
||||
import { ghidraRiscvLimitations } from "./GhidraRiscvProfile.js";
|
||||
import { ghidraPeCodeViewLimitations } from "./GhidraPeProfile.js";
|
||||
import { ghidraPePdbLimitations } from "./GhidraPeProfile.js";
|
||||
import { unverifiedGhidraBuildLimitation } from "./GhidraInstallationPolicy.js";
|
||||
import { GhidraHeadlessLauncher } from "./GhidraLauncher.js";
|
||||
import { attestGhidraNativeLoadImage } from "./GhidraLoadImageAttest.js";
|
||||
@@ -93,10 +93,7 @@ import {
|
||||
limitationsFor,
|
||||
} from "./GhidraProviderCapabilities.js";
|
||||
import type { GhidraSessionError } from "./GhidraSessionError.js";
|
||||
import {
|
||||
ghidraPdbLimitations,
|
||||
type GhidraSessionInfo,
|
||||
} from "./GhidraSessionValues.js";
|
||||
import type { GhidraSessionInfo } from "./GhidraSessionValues.js";
|
||||
import { ghidraExtensionFailure } from "./extensions/GhidraExtensionFailures.js";
|
||||
import {
|
||||
ghidraExtensionSchema,
|
||||
@@ -532,7 +529,6 @@ export const createGhidraProviderClient = (input: {
|
||||
// Replaced by the reported outcome once the session handshake is checked.
|
||||
const seedLimitations: string[] = [...ghidraSeedLimitations(seeds)];
|
||||
const pdbLimitations: string[] = [];
|
||||
let pdbLoaded: boolean | null | undefined;
|
||||
// Replaced by the reported storage outcome once the handshake is checked.
|
||||
const softFloatLimitations: string[] = [
|
||||
...ghidraMipsSoftFloatLimitations(softFloat),
|
||||
@@ -663,11 +659,10 @@ export const createGhidraProviderClient = (input: {
|
||||
info: GhidraSessionInfo,
|
||||
): Promise<AnalysisError | undefined> => {
|
||||
if (target.format === "pe") {
|
||||
pdbLoaded = info.pdb?.loaded;
|
||||
pdbLimitations.splice(
|
||||
0,
|
||||
pdbLimitations.length,
|
||||
...ghidraPdbLimitations(info.pdb),
|
||||
...ghidraPePdbLimitations(target, info.pdb),
|
||||
);
|
||||
}
|
||||
const softFloatReason = ghidraMipsSoftFloatFailure(
|
||||
@@ -722,7 +717,6 @@ export const createGhidraProviderClient = (input: {
|
||||
...ghidraExtensionLimitations(extensions),
|
||||
...seedLimitations,
|
||||
...pdbLimitations,
|
||||
...ghidraPeCodeViewLimitations(target, pdbLoaded),
|
||||
...(releaseLimitation === undefined ? [] : [releaseLimitation]),
|
||||
];
|
||||
const nativeAotOwner =
|
||||
|
||||
@@ -121,21 +121,26 @@ export const ghidraPdbLimitations = (
|
||||
limitations.push(
|
||||
`PDB attributes ${pdb.resource_limit.omitted_attributes.join(", ")} were omitted because the handshake's ${pdb.resource_limit.utf16_budget}-UTF-16-unit PDB attribute budget was exhausted. Omitted values remain unknown; they were not truncated or reported as absent.`,
|
||||
);
|
||||
const hasReference = [
|
||||
pdb.file,
|
||||
pdb.guid,
|
||||
pdb.age,
|
||||
pdb.signature,
|
||||
pdb.version,
|
||||
].some((value) => value !== null);
|
||||
const reference = hasReference
|
||||
? `The Ghidra Program references PDB ${pdb.file ?? "(file unknown)"} (GUID ${pdb.guid ?? "unknown"}, age (hex) ${pdb.age ?? "unknown"}, signature ${pdb.signature ?? "unknown"}, version ${pdb.version ?? "unknown"}); `
|
||||
: "";
|
||||
if (pdb.loaded === true)
|
||||
return [
|
||||
...limitations,
|
||||
"Ghidra reports a loaded PDB; this is not an independent PDB identity or complete symbol/type-coverage attestation. Original names and types may still be missing; an empty name search does not prove a procedure is absent. Use list_procedures and address-based analysis to inspect recovered functions.",
|
||||
`${reference}Ghidra reports a loaded PDB; this is not an independent PDB identity or complete symbol/type-coverage attestation. Original names and types may still be missing; an empty name search does not prove a procedure is absent. Use list_procedures and address-based analysis to inspect recovered functions.`,
|
||||
];
|
||||
if (
|
||||
[pdb.file, pdb.guid, pdb.age, pdb.signature, pdb.version].every(
|
||||
(value) => value === null,
|
||||
)
|
||||
)
|
||||
return limitations;
|
||||
if (!hasReference) return limitations;
|
||||
const state = pdb.loaded === false ? "not loaded" : "load state unknown";
|
||||
return [
|
||||
...limitations,
|
||||
`The target references PDB ${pdb.file ?? "(file unknown)"} (GUID ${pdb.guid ?? "unknown"}, age (hex) ${pdb.age ?? "unknown"}, signature ${pdb.signature ?? "unknown"}, version ${pdb.version ?? "unknown"}); Ghidra reports ${state}. Original names and types may be missing; an empty name search does not prove a procedure is absent. Use list_procedures and address-based analysis to inspect recovered functions. REA does not admit a caller-selected PDB companion.`,
|
||||
`${reference}Ghidra reports ${state}. Original names and types may be missing; an empty name search does not prove a procedure is absent. Use list_procedures and address-based analysis to inspect recovered functions. REA does not admit a caller-selected PDB companion.`,
|
||||
];
|
||||
};
|
||||
|
||||
|
||||
@@ -136,6 +136,7 @@ interface SnapshotProviderOptions {
|
||||
readonly profile?: ReturnType<typeof createAnalysisProfile>;
|
||||
readonly identity?: ProviderIdentity;
|
||||
readonly supportedOperations?: readonly CapabilityDescriptor["operation"][];
|
||||
readonly limitations?: readonly string[];
|
||||
}
|
||||
|
||||
const makeProvider = (
|
||||
@@ -209,6 +210,7 @@ const makeProvider = (
|
||||
createAnalysisExecution(result, profile.provider, {
|
||||
analysisProfile: profile,
|
||||
rawResult: result,
|
||||
limitations: options.limitations ?? [],
|
||||
}),
|
||||
);
|
||||
},
|
||||
@@ -254,11 +256,11 @@ const withAlternateWorkflowProfile = (
|
||||
snapshot: AnalysisSnapshot,
|
||||
path: string,
|
||||
current: Evidence,
|
||||
): AnalysisSnapshot => {
|
||||
const alternateProfile = createAnalysisProfile(REA_WORKFLOW_PROVIDER, {
|
||||
alternateProfile = createAnalysisProfile(REA_WORKFLOW_PROVIDER, {
|
||||
workflow: "binary_overview",
|
||||
fixture: "alternate-profile",
|
||||
});
|
||||
}),
|
||||
): AnalysisSnapshot => {
|
||||
if (
|
||||
typeof current.normalized_result !== "object" ||
|
||||
current.normalized_result === null ||
|
||||
@@ -467,6 +469,78 @@ const verifyWindowsSnapshotAdmission = async (
|
||||
).toHaveLength(1);
|
||||
};
|
||||
|
||||
it.each([
|
||||
{ tool: "binary_overview", arguments: {}, priorRevision: 3 },
|
||||
{
|
||||
tool: "inspect_native_api",
|
||||
arguments: { procedure: "0x1000" },
|
||||
priorRevision: undefined,
|
||||
},
|
||||
] as const)(
|
||||
"refreshes pre-coverage $tool Evidence and replays the current revision",
|
||||
async (scenario) => {
|
||||
const fixture = await createSnapshotReplayFixture("rea-prior-coverage-");
|
||||
const limitation = "Original PDB names may be missing.";
|
||||
const provider = fixture.createProvider({
|
||||
supportedOperations: workflowProviderOperations,
|
||||
limitations: [limitation],
|
||||
});
|
||||
const dependencies = fixture.dependencies(provider);
|
||||
const options = { snapshotPath: fixture.snapshotPath };
|
||||
const first = parseEvidence(
|
||||
await runDirectAnalysis(
|
||||
dependencies,
|
||||
fixture.path,
|
||||
scenario.tool,
|
||||
scenario.arguments,
|
||||
options,
|
||||
),
|
||||
);
|
||||
const saved = await readAnalysisSnapshot(fixture.snapshotPath);
|
||||
if (!saved.ok) throw saved.error;
|
||||
const priorProfile = createAnalysisProfile(REA_WORKFLOW_PROVIDER, {
|
||||
upstream_analysis_profile: saved.value.binding.analysis_profile,
|
||||
...(scenario.priorRevision === undefined
|
||||
? {}
|
||||
: { workflow_revision: scenario.priorRevision }),
|
||||
});
|
||||
const prior = withAlternateWorkflowProfile(
|
||||
saved.value,
|
||||
fixture.path,
|
||||
{ ...first, limitations: ["Derived by an REA composed workflow."] },
|
||||
priorProfile,
|
||||
);
|
||||
const written = await withArtifactResourceScope((resources) =>
|
||||
writeAnalysisSnapshot(prior, fixture.snapshotPath, true, resources),
|
||||
);
|
||||
if (!written.ok) throw written.error;
|
||||
const refreshed = parseEvidence(
|
||||
await runDirectAnalysis(
|
||||
dependencies,
|
||||
fixture.path,
|
||||
scenario.tool,
|
||||
scenario.arguments,
|
||||
options,
|
||||
),
|
||||
);
|
||||
expect(refreshed.limitations).toContain(limitation);
|
||||
expect(refreshed.analysis_profile?.digest).not.toBe(priorProfile.digest);
|
||||
expect(refreshed.normalized_result).toEqual(first.normalized_result);
|
||||
const starts = [...fixture.starts];
|
||||
const calls = [...fixture.calls];
|
||||
const replayed = await runDirectAnalysis(
|
||||
dependencies,
|
||||
fixture.path,
|
||||
scenario.tool,
|
||||
scenario.arguments,
|
||||
options,
|
||||
);
|
||||
expect(replayed).toEqual(refreshed);
|
||||
expect(fixture.starts).toEqual(starts);
|
||||
expect(fixture.calls).toEqual(calls);
|
||||
},
|
||||
);
|
||||
|
||||
describe("direct analysis composed snapshot replay", () => {
|
||||
it.skipIf(process.platform !== "win32")(
|
||||
"does not replay a CLI snapshot through a Windows junction",
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { afterEach, expect, it } from "vitest";
|
||||
import { createAnalysisExecution } from "../../../src/application/AnalysisProvider.js";
|
||||
import { ok } from "../../../src/domain/result.js";
|
||||
import { parseEvidence } from "../../../src/domain/evidence.js";
|
||||
import { connect, closeEnhancedToolResources } from "./enhancedToolsHarness.js";
|
||||
|
||||
afterEach(closeEnhancedToolResources);
|
||||
|
||||
it("retains provider coverage caveats in a composed MCP overview without repeated reads", async () => {
|
||||
const calls: string[] = [];
|
||||
const limitation = "Companion PDB symbol coverage is unverified.";
|
||||
const client = await connect({
|
||||
execute: async (operation) => {
|
||||
calls.push(operation);
|
||||
const value = operation === "current_document" ? "fixture" : [];
|
||||
return ok(
|
||||
createAnalysisExecution(
|
||||
value,
|
||||
{ id: "fixture", name: "Fixture", version: "1" },
|
||||
{ limitations: [limitation] },
|
||||
),
|
||||
);
|
||||
},
|
||||
});
|
||||
const response = await client.callTool({
|
||||
name: "binary_overview",
|
||||
arguments: {},
|
||||
});
|
||||
expect(response.isError).not.toBe(true);
|
||||
const evidence = parseEvidence(response.structuredContent);
|
||||
expect(evidence.limitations).toEqual([
|
||||
"Artifact identity is unavailable for this observation.",
|
||||
"Derived by an REA composed workflow.",
|
||||
limitation,
|
||||
]);
|
||||
expect(evidence.normalized_result).toMatchObject({
|
||||
document: "fixture",
|
||||
procedure_count: 0,
|
||||
string_count: 0,
|
||||
});
|
||||
expect(calls.length).toBe(new Set(calls).size);
|
||||
});
|
||||
@@ -315,7 +315,7 @@ it("retains missing PDB coverage in empty PE name-search Evidence and snapshot r
|
||||
const evidence = sessionEvidence(harness.session, reply.structuredContent);
|
||||
expect(evidence.normalized_result).toEqual([]);
|
||||
expect(evidence.analysis_profile?.parameters.pdb_observations).toBe(
|
||||
"program-info-v1",
|
||||
"program-info-v5",
|
||||
);
|
||||
const limitation = evidence.limitations.find((value) =>
|
||||
value.includes(pdb.file),
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { parseConfig } from "../../src/config/parseConfig.js";
|
||||
import { runDirectAnalysis } from "../../src/application/DirectAnalysis.js";
|
||||
import { createAnalysisExecution } from "../../src/application/AnalysisProvider.js";
|
||||
import { ok } from "../../src/domain/result.js";
|
||||
import {
|
||||
createBinarySessionTargets,
|
||||
createTestBinarySession,
|
||||
} from "../fixtures/binarySession.js";
|
||||
|
||||
it("retains provider coverage caveats in a composed CLI result and closes the session", async () => {
|
||||
const [path] = await createBinarySessionTargets();
|
||||
const limitation = "Companion PDB symbol coverage is unverified.";
|
||||
let reads = 0;
|
||||
let closed = 0;
|
||||
const createSession = () =>
|
||||
createTestBinarySession(() => ({
|
||||
execute: async (operation) => {
|
||||
reads++;
|
||||
return ok(
|
||||
createAnalysisExecution(
|
||||
operation === "current_document" ? "fixture" : [],
|
||||
{ id: "fixture", name: "Fixture", version: "1" },
|
||||
{ limitations: [limitation] },
|
||||
),
|
||||
);
|
||||
},
|
||||
close: async () => {
|
||||
closed++;
|
||||
return ok(null);
|
||||
},
|
||||
}));
|
||||
const output = await runDirectAnalysis(
|
||||
{
|
||||
readConfiguration: () => parseConfig({}),
|
||||
createBinarySession: createSession,
|
||||
createManagedBinarySession: createSession,
|
||||
createArtifactBinarySession: createSession,
|
||||
},
|
||||
path,
|
||||
"binary_overview",
|
||||
{},
|
||||
);
|
||||
expect(output).toMatchObject({
|
||||
limitations: ["Derived by an REA composed workflow.", limitation],
|
||||
normalized_result: {
|
||||
document: "fixture",
|
||||
segment_count: 0,
|
||||
procedure_count: 0,
|
||||
string_count: 0,
|
||||
},
|
||||
});
|
||||
expect(reads).toBe(5);
|
||||
expect(closed).toBe(1);
|
||||
});
|
||||
Reference in New Issue
Block a user