You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix: make reveal's Ghidra enrichment opt-in, not automatic (#277)
* fix: make reveal's Ghidra enrichment opt-in, not automatic
Follow-up to #275 (the target_reveal_dir hang fix): revealing a target has
always ALSO auto-triggered optional Ghidra enrichment whenever
features.ghidra.enrich_recon is on — a coupling that predates #275 and
that #275 only made non-blocking, not optional. On review with the
operator: "reveal" reads as a pure visibility toggle, but silently queuing
a potentially long-running background analysis job for every revealed
target is surprising — this coupling is what actually caused the incident
(revealing an Erlang erts/bin directory queued a dozen-plus background
Ghidra jobs the operator never explicitly asked for on that call).
set_visible/reveal_dir gain an explicit `enrich: bool = False` parameter.
Enrichment now requires BOTH the global features.ghidra.enrich_recon
setting AND an explicit per-call enrich=True — reveal alone never queues
analysis. The capability isn't removed (an agent doing deliberate VR work
can still opt in per call), just no longer implicit.
Threaded through the whole stack: engine (reveal.py), MCP tools + catalog
schemas, REST (VisibleUpdate/RevealDir gain enrich: bool = False), and
frontend API types. FilesystemBrowser.tsx's reveal actions don't pass
enrich=true (reveal-to-browse shouldn't trigger analysis); the now-dead
"Ghidra enrichment running" UI message for a directory reveal was removed
since it can no longer fire from that path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: fix stale _maybe_enrich_ghidra docstring after the enrich opt-in change
Flagged in #277's review: the docstring still said "reveal runs this
enrichment then" — no longer true now that reveal requires an explicit
per-call enrich=True, not just the global feature flag.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
("write", "target_set_visible", _t.set_visible, "REVEAL (visible=true) or re-HIDE (visible=false) ONE target in the curated graph. Firmware ELF children are hidden by default — unpack registers each (searchable via target_list(include_hidden=true), addressable, recon-enriched) but they'd flood the graph, so they contribute NOTHING to it until revealed. Revealing materializes the target's recon symbol/string nodes from the already-stored facts (no re-run) so it joins the graph + Targets pane immediately; optional Ghidra enrichment (if enabled) runs DETACHED in the background — a cold headless pass can take minutes, this call does not wait for it. Use after target_list(include_hidden=true)/fs_list surfaces a child worth analyzing. Returns {target_id, name, visible, materialized, enrichment_queued}.",
("write", "target_reveal_dir", _t.reveal_dir, "REVEAL every HIDDEN child of a firmware whose rootfs path is under `prefix` (e.g. prefix='usr/sbin' reveals all ELFs in /usr/sbin) — the bulk counterpart to target_set_visible for bringing a whole directory of binaries into the curated graph at once. An empty prefix reveals ALL hidden children. Materializes each revealed child's recon nodes from stored facts (no re-run) — fast, even for a directory with many binaries; optional per-binary Ghidra enrichment (if enabled) runs DETACHED in the background instead of blocking this call (a dozen+ binaries analyzed sequentially can take hours). `target_id` is the firmware. Returns {firmware_target_id, prefix, revealed, target_ids, enrichment_queued}.",
131
-
{"type": "object", "properties": {"project_id": {"type": "string"}, "target_id": {"type": "string", "description": "the firmware target whose children to reveal"}, "prefix": {"type": "string", "description": "rootfs directory prefix (e.g. 'usr/sbin'); empty reveals all hidden children"}}, "required": ["project_id", "target_id"]}),
128
+
("write", "target_set_visible", _t.set_visible, "REVEAL (visible=true) or re-HIDE (visible=false) ONE target in the curated graph. Firmware ELF children are hidden by default — unpack registers each (searchable via target_list(include_hidden=true), addressable, recon-enriched) but they'd flood the graph, so they contribute NOTHING to it until revealed. Revealing materializes the target's recon symbol/string nodes from the already-stored facts (no re-run) so it joins the graph + Targets pane immediately. Ghidra enrichment does NOT run automatically — pass `enrich=true` to also queue it DETACHED in the background (a cold headless pass can take minutes; still needs features.ghidra.enrich_recon on — enrich=true is an ADDITIONAL opt-in per call, not a replacement). Use after target_list(include_hidden=true)/fs_list surfaces a child worth analyzing. Returns {target_id, name, visible, materialized, enrichment_queued}.",
129
+
{"type": "object", "properties": {"project_id": {"type": "string"}, "target_id": {"type": "string"}, "visible": {"type": "boolean", "description": "true to reveal (default), false to re-hide"}, "enrich": {"type": "boolean", "description": "also queue detached Ghidra enrichment for this target (default false — reveal alone does not trigger analysis)"}}, "required": ["project_id", "target_id"]}),
130
+
("write", "target_reveal_dir", _t.reveal_dir, "REVEAL every HIDDEN child of a firmware whose rootfs path is under `prefix` (e.g. prefix='usr/sbin' reveals all ELFs in /usr/sbin) — the bulk counterpart to target_set_visible for bringing a whole directory of binaries into the curated graph at once. An empty prefix reveals ALL hidden children. Materializes each revealed child's recon nodes from stored facts (no re-run) — fast, even for a directory with many binaries. Ghidra enrichment does NOT run automatically — pass `enrich=true` to also queue it for the WHOLE batch as one detached background process working through them sequentially (a dozen+ binaries can take hours; still needs features.ghidra.enrich_recon on). Don't pass enrich=true reflexively; it's for when you deliberately want deep analysis on everything in that directory, not the default way to browse. `target_id` is the firmware. Returns {firmware_target_id, prefix, revealed, target_ids, enrichment_queued}.",
131
+
{"type": "object", "properties": {"project_id": {"type": "string"}, "target_id": {"type": "string", "description": "the firmware target whose children to reveal"}, "prefix": {"type": "string", "description": "rootfs directory prefix (e.g. 'usr/sbin'); empty reveals all hidden children"}, "enrich": {"type": "boolean", "description": "also queue detached Ghidra enrichment for every revealed binary, as one batched background process (default false)"}}, "required": ["project_id", "target_id"]}),
("read", "re_list_functions", _t.list_functions, "List/GREP the functions discovered in a target (name + the discovered set) for a substring `pattern` (or a regex with regex=true) — the fast function-name search. Server-side filtered + PAGINATED exactly like re_list_strings: pass `pattern` to filter by name, `offset`/`limit` to page (default 200, max 1000); the result reports the total match count + the next offset. With no `pattern` it pages the whole list. QUERY: records a function_list Observation; adds no graph nodes. Requires a saved analysis when Ghidra is the active backend (run re_analyze first on a warm miss).", {"type": "object", "properties": {"target_id": {"type": "string"}, "pattern": {"type": "string", "description": "substring to grep the function names for"}, "regex": {"type": "boolean", "description": "treat pattern as a regex (falls back to substring if it doesn't compile)"}, "offset": {"type": "integer", "description": "page start index into the matches (default 0)"}, "limit": {"type": "integer", "description": "max names to return (default 200, clamped 1-1000)"}}, "required": ["target_id"]}),
0 commit comments