From 0c63381b83b38fad7778e693b22ddeeb9b1d68a7 Mon Sep 17 00:00:00 2001 From: Efi Jeremiah Date: Wed, 30 Sep 2026 13:06:10 +0300 Subject: [PATCH] feat(serve): Cmd/Ctrl+K command palette MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cmd/Ctrl+K only focused the small header field, and on a narrow window that field is hidden, so the shortcut silently switched to More › Search. It now opens a palette over the page: - empty: recent searches, the findings to look at first, views, actions - typing: fuzzy match over views, More sections, findings (plain title or rule id) and conversations, instantly; matching actions from the evidence via /api/search a moment later, with a visible pending line - keys: up/down/Tab, Enter, Cmd/Ctrl+Enter for the full search, Esc clears then closes; "/" opens it too; header field and a phone search button open the same palette - dialog + combobox + listbox semantics, focus never left in the hidden input, secrets masked in every result, reduced-motion respected Existing shortcuts (physical-key matching, j/k, drawer Esc) unchanged. Co-Authored-By: Claude --- CHANGELOG.md | 10 ++ README.md | 3 +- docs/index.html | 1 + docs/llms.txt | 2 +- docs/serve.md | 3 +- internal/serve/palette_test.go | 29 ++++++ internal/serve/ui.html | 163 ++++++++++++++++++++++++++++++++- 7 files changed, 205 insertions(+), 6 deletions(-) create mode 100644 internal/serve/palette_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bf9a29..fc1f379 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,6 +61,16 @@ next is added, and a run never chokes the machine. hashes, and a MAC over its state under the machine key. Anything that does not match is rebuilt from the sealed evidence. +- **Command palette in the explorer.** `Cmd+K` / `Ctrl+K`, or `/`, opens + a search box over the page instead of only focusing the small header + field (which, on a narrow window, was hidden, so the shortcut silently + switched tabs). It fuzzy-matches views, findings and conversations as you + type, shows matching actions from the evidence a moment later, runs + actions (theme, verify, apply guardrails), keeps recent searches, and is + fully keyboard driven: `↑` `↓` `Tab`, `↵`, `Cmd/Ctrl+↵` for the full + search, `esc`. A search button opens it on a phone. Secrets are masked in + every result. + ### Changed - `CLOUD_CREDENTIAL_EXPORT` also covers listing storage account keys and connection strings, Cosmos DB keys, service-principal credential resets diff --git a/README.md b/README.md index e0e393e..86de33e 100644 --- a/README.md +++ b/README.md @@ -207,7 +207,8 @@ A list of 700 findings is not an answer. The explorer (`agentdfir serve`) turns - **Attack chains (toxic combinations).** Individually unremarkable steps that together are an attack: *injection in a tool result → agent rewrites its own instructions → shell runs*, *secret read → upload*, *orphan agent → config change → tool use*, *poisoned MCP result → destructive command*, *injection → commit → push*, *action → log deletion*… Eight ship built in, matched inside one session or one agent's lineage within a time window, mapped to MITRE ATLAS / ATT&CK. Add your own as `*.chains.json` ([docs](docs/attack-chains.md)). - **How it happened, as a story.** Every finding opens to a swimlane diagram — *You · The AI agent · Tools & outside world* — one card per evidence line in time order, the flagged steps in red, the outside addresses they reach, and the **most likely start** (your request, an automatic skill message, injected text the agent read, or a helper agent) marked on its card. Every card opens the exact sealed log line. - **Plugin (MCP) activity.** Every MCP call in time order with the account, plugin, what the agent sent and what came back. -- **Search everything.** `Ctrl+K`: every event field, every finding, and the raw bytes of every sealed artifact, live, in seconds, regex or literal. +- **Command palette.** `Cmd+K` / `Ctrl+K` (or `/`) opens one box for everything: jump to any view, finding or conversation by fuzzy match, run actions, and see matching actions from the evidence as you type. `Cmd/Ctrl+Enter` runs the full search. +- **Search everything.** Every event field, every finding, and the raw bytes of every sealed artifact, live, in seconds, regex or literal. - **Case file.** Mark findings true / false positive / needs review, pin key evidence, tag sessions, write notes. Saved as a hash-chained log outside the sealed evidence, attributed to you, and rendered as an *Analyst Investigation* section in the PDF and HTML reports. Try it on a synthetic incident: `agentdfir simulate --scenario toxic-chain --out ./sim && HOME=./sim agentdfir run`. diff --git a/docs/index.html b/docs/index.html index d4b58ce..e946836 100644 --- a/docs/index.html +++ b/docs/index.html @@ -622,6 +622,7 @@

Every finding resolves to a line in a sealed file.

diff --git a/docs/llms.txt b/docs/llms.txt index ae85193..d68b999 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -2,7 +2,7 @@ > Open-source digital forensics and incident response (DFIR) framework for AI agents. Collects, preserves, reconstructs and investigates activity from Claude Code, OpenAI Codex CLI, Cursor, Gemini CLI, GitHub Copilot CLI and other agentic AI tools. Core principle: AI-generated text is never automatically treated as factual evidence of execution — every action is classified REQUESTED / REPORTED / OBSERVED / CORROBORATED / CONTRADICTED / UNKNOWN. -Status: released (v3.0). The explorer (`agentdfir serve`) is an investigation workbench: Sessions tab (one risk-sorted card per session with metadata and corroboration bar), attack chains (toxic combinations: ordered event sequences such as injection → self-modification → shell, matched in one session within a time window, 8 built-in, JSON-extensible), the investigation tree under every finding (steps in order and what led to each: prompt before, tool result consumed, spawn), whole-case search (events, findings, raw artifact bytes; Ctrl+K), and a hash-chained analyst case file (verdicts, pins, tags, notes) rendered into PDF/HTML reports. One command does the common case: `agentdfir run` = detect every installed agent → collect into one sealed package → analyze → open the browser explorer; install-and-run in one line — macOS/Linux: `curl -fsSL https://raw.githubusercontent.com/efij/AgentDFIR/main/install.sh | sh && ~/.local/bin/agentdfir run`; Windows (x64/ARM64, PowerShell): `$env:AGENTDFIR_RUN=1; irm https://raw.githubusercontent.com/efij/AgentDFIR/main/install.ps1 | iex`. Since v2.7: `agentdfir mitigate` turns findings into guardrails in the agents' own settings (Claude Code permissions + log-guard hook, Codex rules, Cursor CLI denies, MCP pins), planned first, backed up, ledgered and reversible; `agentdfir export` / `agentdfir open` move a case to another computer in one file. Since v3.0: `agentdfir hunt` checks a machine for eight known 2025–2026 incidents (Nx s1ngularity, Shai-Hulud 1 and 2, the keyv wave, SANDWORM_MODE, postmark-mcp, codexui-android, Amazon Q wiper) plus STIX 2.1 / MISP feeds, classifying each hit as OBSERVED / SEEN_IN_OUTPUT / MENTIONED; `agentdfir scan-repo` checks a repository before an AI agent opens it (committed SessionStart hooks, VS Code folderOpen tasks, project MCP servers, injected AGENTS.md/CLAUDE.md) with SARIF output and a GitHub Action; an offline decoder unwraps nested base64/gzip/hex/UTF-16LE payloads for the rules; `monitor --journal` makes transcripts tamper-evident. 13 supported products (Claude Code, Claude Cowork, Codex CLI and desktop app, Gemini CLI, Cursor, Copilot CLI, Copilot Chat for VS Code, Cline, Roo Code, OpenClaw, OpenCode, Aider, Warp; plus Kiro configuration), 172 deterministic detections (91 built-in incl. 8 attack chains + 81 pack rules), HIGH/CRITICAL ones mapped to MITRE ATLAS 5.6 / ATT&CK (28 ATLAS, 69 ATT&CK techniques; see detection-coverage.md), streaming analysis for multi-GB packages, signed product packs for adding new agents without code. +Status: released (v3.0). The explorer (`agentdfir serve`) is an investigation workbench: Sessions tab (one risk-sorted card per session with metadata and corroboration bar), attack chains (toxic combinations: ordered event sequences such as injection → self-modification → shell, matched in one session within a time window, 8 built-in, JSON-extensible), the investigation tree under every finding (steps in order and what led to each: prompt before, tool result consumed, spawn), whole-case search (events, findings, raw artifact bytes; Cmd/Ctrl+K command palette: fuzzy jump to any view, finding or conversation plus live evidence matches), and a hash-chained analyst case file (verdicts, pins, tags, notes) rendered into PDF/HTML reports. One command does the common case: `agentdfir run` = detect every installed agent → collect into one sealed package → analyze → open the browser explorer; install-and-run in one line — macOS/Linux: `curl -fsSL https://raw.githubusercontent.com/efij/AgentDFIR/main/install.sh | sh && ~/.local/bin/agentdfir run`; Windows (x64/ARM64, PowerShell): `$env:AGENTDFIR_RUN=1; irm https://raw.githubusercontent.com/efij/AgentDFIR/main/install.ps1 | iex`. Since v2.7: `agentdfir mitigate` turns findings into guardrails in the agents' own settings (Claude Code permissions + log-guard hook, Codex rules, Cursor CLI denies, MCP pins), planned first, backed up, ledgered and reversible; `agentdfir export` / `agentdfir open` move a case to another computer in one file. Since v3.0: `agentdfir hunt` checks a machine for eight known 2025–2026 incidents (Nx s1ngularity, Shai-Hulud 1 and 2, the keyv wave, SANDWORM_MODE, postmark-mcp, codexui-android, Amazon Q wiper) plus STIX 2.1 / MISP feeds, classifying each hit as OBSERVED / SEEN_IN_OUTPUT / MENTIONED; `agentdfir scan-repo` checks a repository before an AI agent opens it (committed SessionStart hooks, VS Code folderOpen tasks, project MCP servers, injected AGENTS.md/CLAUDE.md) with SARIF output and a GitHub Action; an offline decoder unwraps nested base64/gzip/hex/UTF-16LE payloads for the rules; `monitor --journal` makes transcripts tamper-evident. 13 supported products (Claude Code, Claude Cowork, Codex CLI and desktop app, Gemini CLI, Cursor, Copilot CLI, Copilot Chat for VS Code, Cline, Roo Code, OpenClaw, OpenCode, Aider, Warp; plus Kiro configuration), 172 deterministic detections (91 built-in incl. 8 attack chains + 81 pack rules), HIGH/CRITICAL ones mapped to MITRE ATLAS 5.6 / ATT&CK (28 ATLAS, 69 ATT&CK techniques; see detection-coverage.md), streaming analysis for multi-GB packages, signed product packs for adding new agents without code. ## Docs diff --git a/docs/serve.md b/docs/serve.md index d22d171..a0d8386 100644 --- a/docs/serve.md +++ b/docs/serve.md @@ -23,7 +23,8 @@ Written for someone who is not a security analyst: every severity, confidence an - **Proof** (a side panel, from anywhere) — the record in plain fields (when, who, conversation, account, command, file, address, result, evidence status), *What stands out* (safety prompts off, a helper agent wrote this, the tool reported an error or a refused login, invisible characters), the findings on it, *Mark as key evidence*, *What led to this*, and the exact sealed log line. - **Protect** — what can be done about this case's findings, in four groups: *Fix now* (config that is unsafe right now: unpinned MCP packages, auto-approve lists), *Prevented by guardrails* (which pack stops it recurring), *Needs a person* (the exact step: rotate the key, review the file) and *Not fixable by settings*. The guardrail table shows each pack's level (ask or block, switchable), friction and how many findings it covers, pre-ticks the two zero-friction packs, and, on the machine the case came from, applies them: *Preview changes* lists every file with its summary and exact diff, *Apply* writes them (backup per file, hash-chained ledger, same code path as the CLI), and *Changes made on this computer* has *Undo* per change and *Undo all*. Findings you marked *Expected* or *Detection mistake* leave the counts. Once guardrails are applied, each rule shows *N since * and each pack *In place* or *Removed since*. Writes are same-origin only and carry a per-process token the page gets on load, so no other page or site can trigger them; a case from another computer gets the `agentdfir mitigate --select … --apply` command to copy and run there. See [mitigate.md](mitigate.md). - **More** - - **Search everything** — `Ctrl+K` / `Cmd+K`. Literal or RE2, optional case, over every event, every finding and — when ticked — the raw bytes of every sealed artifact (bounded to 20 s / 500 hits). + - **Command palette** — `Cmd+K` / `Ctrl+K`, or `/` when you are not typing, opens a search box over the page. With nothing typed it lists recent searches, the findings to look at first, every view and a few actions. As you type it fuzzy-matches views, the More sections, findings (by plain title or rule id) and conversations instantly, and shows up to six matching actions from the evidence (`/api/search`, events only) a moment later. `↑` `↓` or `Tab` move, `↵` opens, `Cmd/Ctrl+↵` runs the full search below, `esc` clears then closes. The header box and, on a phone, the search button open the same palette. Secrets are masked in every result. + - **Search everything** — from the palette with `Cmd/Ctrl+↵`, or More › Search everything. Literal or RE2, optional case, over every event, every finding and — when ticked — the raw bytes of every sealed artifact (bounded to 20 s / 500 hits). - **Plugin (MCP) activity** — every MCP tool call in time order: time, account, plugin, tool, *what the agent sent* (read out of the sealed line) and *what came back* (the matching tool result), filterable per plugin. - **Plugins installed** — one row per plugin: used by, account, runs on this computer or online, fixed version or not, what it is, where it is set. - **Memory files** — instruction and memory files, with the lines an agent wrote and why (copied from a tool answer, after your request…). diff --git a/internal/serve/palette_test.go b/internal/serve/palette_test.go new file mode 100644 index 0000000..1128377 --- /dev/null +++ b/internal/serve/palette_test.go @@ -0,0 +1,29 @@ +package serve + +import ( + "strings" + "testing" +) + +// TestCommandPalette: Cmd/Ctrl+K opens a real palette (a dialog with a +// combobox and a listbox), not just focus on the header box, and closing it +// takes focus out of the hidden input so later keys do not type into it. +func TestCommandPalette(t *testing.T) { + ui := string(uiHTML) + for _, want := range []string{ + `id="pal" role="dialog" aria-modal="true"`, + `id="palq" type="text"`, `role="combobox"`, `aria-controls="pall"`, `id="pall" role="listbox"`, + "isKey(e,'k')){e.preventDefault(); if(PAL.open) closePal(); else openPal('');", + "e.key==='/'", + "$('#palq').blur()", + "scope:'events'", + `id="palbtn"`, + } { + if !strings.Contains(ui, want) { + t.Errorf("ui.html lost %q", want) + } + } + if strings.Contains(ui, "g.focus(); g.select();") { + t.Error("Cmd+K went back to only focusing the header box") + } +} diff --git a/internal/serve/ui.html b/internal/serve/ui.html index 420f81c..64ee409 100644 --- a/internal/serve/ui.html +++ b/internal/serve/ui.html @@ -201,6 +201,38 @@ dl.kv dt{color:var(--muted)} dl.kv dd{margin:0;font-family:var(--mono);font-size:12.5px;word-break:break-all} +/* ---- command palette (Cmd/Ctrl+K) */ +.pal-scrim{position:fixed;inset:0;background:var(--scrim);opacity:0;pointer-events:none;transition:opacity .12s ease;z-index:80} +.pal-scrim.on{opacity:1;pointer-events:auto} +.pal{position:fixed;left:50%;top:12vh;width:min(680px,calc(100vw - 32px));transform:translate(-50%,-8px) scale(.98);opacity:0;pointer-events:none;transition:opacity .12s ease,transform .12s ease;z-index:81; + background:var(--surface);border:1px solid var(--line-2);border-radius:var(--r-lg);box-shadow:0 24px 64px rgba(0,0,0,.28),var(--shadow);display:flex;flex-direction:column;max-height:min(620px,76vh);overflow:hidden} +.pal.on{opacity:1;pointer-events:auto;transform:translate(-50%,0) scale(1)} +.pal-in{display:flex;align-items:center;gap:10px;padding:0 16px;border-bottom:1px solid var(--line);color:var(--muted)} +.pal-in input{flex:1;min-width:0;height:56px;border:0;outline:none;background:none;font:inherit;font-size:17px;color:var(--ink)} +.pal-in input::placeholder{color:var(--muted)} +.pal kbd{font:11px var(--mono);padding:2px 6px;border:1px solid var(--line-2);border-bottom-width:2px;border-radius:5px;color:var(--muted);background:var(--surface-2);white-space:nowrap} +.pal-list{overflow-y:auto;padding:6px;overscroll-behavior:contain} +.pal-g{padding:10px 10px 4px;font-size:11.5px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:var(--muted)} +.pal-i{display:flex;align-items:center;gap:12px;width:100%;padding:9px 10px;border:0;border-radius:var(--r-sm);background:none;color:var(--ink);text-align:left;cursor:pointer;font:inherit;min-height:44px} +.pal-i[aria-selected="true"]{background:var(--accent-soft)} +.pal-i .ic{flex:0 0 28px;height:28px;border-radius:7px;display:grid;place-items:center;background:var(--surface-2);color:var(--ink-2)} +.pal-i[aria-selected="true"] .ic{background:var(--accent);color:var(--accent-ink)} +.pal-i .tx{flex:1;min-width:0} +.pal-i .t{font-size:14.5px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.pal-i .d{font-size:12.5px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:1px} +.pal-i mark{background:none;border:0;outline:0;box-shadow:none;padding:0;border-radius:0;color:var(--accent);font-weight:750;text-decoration:underline;text-decoration-thickness:2px;text-underline-offset:3px} +.pal-i .go{flex:0 0 auto;font-size:12px;color:var(--muted);opacity:0} +.pal-i[aria-selected="true"] .go{opacity:1} +.pal-empty{padding:28px 16px;text-align:center;color:var(--muted);font-size:14px} +.pal-foot{display:flex;gap:14px;flex-wrap:wrap;align-items:center;padding:9px 14px;border-top:1px solid var(--line);background:var(--surface-2);font-size:12px;color:var(--muted)} +.pal-foot span{display:inline-flex;gap:5px;align-items:center} +.pal-spin{width:14px;height:14px;border:2px solid var(--line-2);border-top-color:var(--accent);border-radius:50%;animation:palspin .7s linear infinite;visibility:hidden} +.pal-spin.on{visibility:visible} +@keyframes palspin{to{transform:rotate(360deg)}} +.gsearch{cursor:pointer}.gsearch input{cursor:pointer} +#palbtn{display:none} +@media (max-width:760px){.pal{top:16px;max-height:calc(100vh - 32px)}.pal-foot .hide-s{display:none}} +@media (prefers-reduced-motion:reduce){.pal,.pal-scrim{transition:none}.pal-spin{animation:none}} /* ---- protect */ .pk{width:100%;border-collapse:collapse;border:1px solid var(--line);border-radius:var(--r-lg);overflow:hidden;background:var(--surface);margin-bottom:12px} .pk td,.pk th{padding:12px 14px;border-top:1px solid var(--line);text-align:left;vertical-align:top;font-size:14px} @@ -342,6 +374,7 @@ .top{flex-wrap:wrap;gap:8px 12px;padding:8px 12px} .tabs{order:3;width:100%;overflow-x:auto;min-height:44px} .gsearch{display:none} + #palbtn{display:inline-grid} main{overflow:auto} .fx,.ax{grid-template-columns:1fr;height:auto} .fx.open .fx-list{display:none} .fx:not(.open) .fx-detail{display:none} @@ -399,7 +432,8 @@
- + +
@@ -456,6 +490,12 @@
+
+