fix(ghidra): explain literal search candidate evidence

This commit is contained in:
morluto committed 2026-11-03 23:57:31 +08:00
1 parent 26b033d51f
commit 47ed29a98c
5 files changed
+34 -5

No files matched your search

+3
View File
@@ -72,6 +72,9 @@ its address when selecting the containing function. `search_procedures` matches
the same names, in leaf or qualified spelling: the function appears once with
its primary name as `value`, and `matched_aliases` lists the qualified names of
the secondary symbols that matched.
For Ghidra case-insensitive literal searches of procedures or strings,
`raw_result` contains bridge candidates before REA applies Unicode-aware
matching. Use `normalized_result` for matches; a raw candidate need not match.
Overloads can share a fully qualified name; ambiguity errors return every
matching entry address so the caller can select the intended function directly.
+2 -2
View File
@@ -218,13 +218,13 @@ export const OFFICIAL_TOOL_CONTRACTS = [
),
official(
"search_procedures",
`Search every analyzed procedure name, including secondary names exact lookup accepts, using literal matching by default or regex when requested. Each procedure appears once under its primary name; matched_aliases lists matching secondary names. Results are deterministic and complete. ${hopperRegexDeadline} Select detail summary for counts and retained native-inventory pages; complete remains the default.`,
`Search every analyzed procedure name, including secondary names exact lookup accepts, using literal matching by default or regex when requested. Each procedure appears once under its primary name; matched_aliases lists matching secondary names. Results are deterministic and complete. For Ghidra case-insensitive literal searches, raw_result contains unfiltered candidates; normalized_result contains matches. ${hopperRegexDeadline} Select detail summary for counts and retained native-inventory pages; complete remains the default.`,
z.object({ ...nativeSearchInput, detail: analysisDetailSchema }),
{ evidenceParameters: "omit-detail" },
),
official(
"search_strings",
`Search every provider-reported string object or candidate using literal matching by default or regex when requested. Results are deterministic and complete over that provider inventory, not every text sequence in artifact bytes. Kuna scans mapped sequences of at least five characters, including unterminated fragments, without computing references. Ghidra may omit packed or undefined literals, including Rust strings; an empty result does not prove text is absent. If an address is known, xrefs can look for references; read_bytes can inspect bytes where supported (not available with Kuna). Hopper retains typed byte decoding, native display, and explicit decoding unknowns. Adjacent long-literal fragments match independently. ${hopperRegexDeadline} Select detail summary for counts and retained native-inventory pages; complete remains the default.`,
`Search every provider-reported string object or candidate using literal matching by default or regex when requested. Results are deterministic and complete over that provider inventory, not every text sequence in artifact bytes. Kuna scans mapped sequences of at least five characters, including unterminated fragments, without computing references. Ghidra may omit packed or undefined literals, including Rust strings; an empty result does not prove text is absent. If an address is known, xrefs can look for references; read_bytes can inspect bytes where supported (not available with Kuna). Hopper retains typed byte decoding, native display, and explicit decoding unknowns. Adjacent long-literal fragments match independently. For Ghidra case-insensitive literal searches, raw_result contains unfiltered candidates; normalized_result contains matches. ${hopperRegexDeadline} Select detail summary for counts and retained native-inventory pages; complete remains the default.`,
z.object({ ...nativeSearchInput, detail: analysisDetailSchema }),
{ evidenceParameters: "omit-detail" },
),
+4
View File
@@ -4,6 +4,10 @@ import {
matchProcedureNames,
} from "../domain/analysisSearch.js";
/** Explain the retained bridge observation when Node owns final matching. */
export const GHIDRA_LITERAL_CANDIDATES_LIMITATION =
"For this case-insensitive literal search, raw_result contains Ghidra's candidate rows before REA's Unicode-aware filtering; normalized_result contains the matches.";
/** ASCII-only candidates are exact; every non-ASCII source value stays eligible.
* Node owns Unicode folding, so the Java runtime's Unicode version cannot omit
* a match. Null means no ASCII-only value can match this query.
@@ -1,4 +1,5 @@
import { z } from "zod";
import { GHIDRA_LITERAL_CANDIDATES_LIMITATION } from "./GhidraLiteralSearch.js";
import {
EXISTING_PROJECT_WINDOWS_REMEDIATION,
EXISTING_PROJECT_WINDOWS_UNAVAILABLE,
@@ -5620,18 +5621,27 @@ it.each(["search_procedures", "search_strings"] as const)(
if (!result.ok) throw result.error;
expect(result.value.result).toEqual(rows.slice(0, 2));
expect(result.value.rawResult).toEqual(rows);
expect(result.value.limitations).toContain(
GHIDRA_LITERAL_CANDIDATES_LIMITATION,
);
const regex = await client.execute(operation, {
pattern: "source",
mode: "regex",
});
if (!regex.ok) throw regex.error;
expect(regex.value.result).toEqual(rows);
expect(regex.value.limitations).not.toContain(
GHIDRA_LITERAL_CANDIDATES_LIMITATION,
);
const sensitive = await client.execute(operation, {
pattern: "source",
case_sensitive: true,
});
if (!sensitive.ok) throw sensitive.error;
expect(sensitive.value.result).toEqual(rows);
expect(sensitive.value.limitations).not.toContain(
GHIDRA_LITERAL_CANDIDATES_LIMITATION,
);
} finally {
await client.close();
}
+15 -3
View File
@@ -1,4 +1,7 @@
import { filterGhidraLiteralCandidates } from "./GhidraLiteralSearch.js";
import {
filterGhidraLiteralCandidates,
GHIDRA_LITERAL_CANDIDATES_LIMITATION,
} from "./GhidraLiteralSearch.js";
import { ghidraProjectHistoryEvidence } from "./GhidraProjectHistory.js";
import type { Evidence, EvidenceLocation } from "../domain/evidence.js";
import { ghidraProjectCommitmentSchema } from "./GhidraProjectSnapshot.js";
@@ -845,7 +848,9 @@ export const createGhidraProviderClient = (input: {
? parseGhidraFunctionResult(operation, called.value)
: parseGhidraInventoryResult(operation, called.value);
if (!result.ok) return result;
let { value: normalized, ...observation } = result.value;
const { value: parsedValue, ...parsedObservation } = result.value;
let normalized = parsedValue;
let observation: GhidraExecutionMetadata = parsedObservation;
if (
(operation === "search_procedures" || operation === "search_strings") &&
input.value.mode === "literal" &&
@@ -856,7 +861,14 @@ export const createGhidraProviderClient = (input: {
normalized,
input.value.pattern,
);
observation = { ...observation, rawResult: called.value };
observation = {
...observation,
rawResult: called.value,
limitations: [
...(observation.limitations ?? []),
GHIDRA_LITERAL_CANDIDATES_LIMITATION,
],
};
}
if (
operation === "inspect_native_data_type" &&