Detection Workbench
Detection Workbench is an offline MCP server for case-based Snort 3 rule writing, testing, explanation, and tuning against registered evidence.
Create, open, list, summarize, update, close, and export cases as Markdown or Timesketch CSV.
Register pcaps/pcapng and rules files as hashed evidence within allowed roots.
Validate Snort rules, run them against pcaps, and summarize pcaps.
Inspect run alerts, filter/page them, and explain individual alerts with rule options and bounded payload context.
Profile rule performance, identify noisy SIDs, test suppress/event_filter/rate_filter/rule_edit tuning, and review proposed changes.
Diff runs, record findings/notes/decisions, and write new rules files without overwriting evidence.
Check engine/backend status and configuration. All analysis is offline on files; it does not sniff live traffic.
Supports running the Snort 3 engine through Docker as the preferred, isolated backend (pinned Cisco Talos snort3 image derived locally), with containers run with no network, a read-only root filesystem, no capabilities, and evidence mounted read-only; the backend in use is reported by get_status.
Runs the Snort 3 engine on registered pcap evidence: validates rule files with lint findings, executes rules offline against captures, returns parsed alerts, explains alerts by reassembling the stream, profiles rules and suggests tuning targets, tests tuning changes before/after, diffs runs, and converts Snort 2 configs via snort2lua (Cisco Talos Registered/Subscriber rules can be supplied as local files).
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Detection Workbench create a case on ~/captures/test.pcap, run my rule, and list alerts"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Detection Workbench
An offline workbench, exposed as an MCP server, for writing, testing, explaining and tuning Snort 3 detections against real evidence. Your MCP client's model does the reasoning; the server runs Snort on files you register and returns structured, bounded results. Every answer about what a rule does is backed by actual Snort output, and every run is written to a case file you can export as a report.
It works only on files on disk. It never sniffs an interface, runs inline, or touches a live sensor or SIEM.
A long-form write-up with a worked example, use cases and example output is in docs/writeup.md. A complete example case directory, with every file explained, is in docs/examples/.
Install
Requires Python 3.12+ and uv. It runs on Linux and macOS (Windows through WSL2) and has been developed and tested on Linux under WSL2.
git clone https://github.com/andrewbrooks/detection-workbench.git ~/detection-workbench && cd ~/detection-workbench
uv syncSnort 3 is needed in one of two forms ([snort].backend = "auto" prefers native, then Docker):
Docker (works anywhere Docker runs). The official Talos image is pinned by digest:
docker pull ciscotalos/snort3@sha256:f15f714fd61ade114b1039687dd43352cae26cd6401b89f2b03404e0d21e322fRuns use --network none --read-only --cap-drop ALL --user <you> with inputs mounted read-only. The image is Snort 3.10.0.0.
On first use the workbench builds a small local image from it (detection-workbench/snort3:..., a few seconds, no network). The
published image keeps Snort under a home directory that only its own user (uid 1000) can enter, so a container running as your
user would fail with "permission denied" on any other uid. The derived image adds search permission on those directories and
nothing else; no capability is added to the container. get_status reports the image in use.
Native. Install Snort 3 so snort is on PATH (build from source, or Homebrew on macOS: brew install snort; check snort -V reports 3.x). Stock config is found at <prefix>/etc/snort/, /usr/local/etc/snort/ or
/opt/homebrew/etc/snort/; override with [snort].defaults_lua.
Check with get_status once connected, or run the demo: uv run scripts/demo_noisy_rule.py.
Related MCP server: retriage
Configure
~/.detection-workbench/config.toml (override the path with DW_CONFIG). Everything is optional.
case_dir = "~/.detection-workbench/cases" # cases live outside the repo by default
allowed_roots = ["~/captures", "~/rules"] # evidence may only be registered from here (and the case dir)
[limits]
timeout_s = 120 # per engine process
max_input_bytes = 2147483648
max_rules_bytes = 52428800
max_alerts_returned = 200 # per call; totals are always reported
max_alerts_parsed = 250000
payload_display_bytes = 512
error_excerpt_lines = 20
[snort]
backend = "auto" # auto | native | docker
binary = "snort"
# defaults_lua = "/usr/local/etc/snort/snort_defaults.lua"allowed_roots is empty by default, so only files inside the case directory can be registered until you add yours.
Connect
The server speaks MCP over stdio. Add it to any MCP client's server configuration, for example:
{
"mcpServers": {
"detection-workbench": {
"command": "uv",
"args": ["--directory", "/home/you/detection-workbench", "run", "detection-workbench"]
}
}
}The server's instructions (also exposed as the detection_engineering prompt) teach the client the workflow:
start a case, write → validate → run → confirm it fires and stays quiet, tune the rule before suppressing, prove changes
with test_tuning, and be honest about what one capture proves.
Tools
Area | Tools |
Case |
|
Snort |
|
A case is a directory of JSON files plus an append-only events.jsonl; see docs/case-schema.md.
Runs are immutable and record the engine and version, argument list, input hashes, and rule-set hash.
export_case writes a report with the analyst summary (set with update_case), an overview of the captures, engines and rule sets, findings, per-run alert tables, tuning decisions with before and after counts, caveats (stream warnings, truncated or failed runs, single-sample limits), an event timeline and a run appendix. The report presents recorded data; the conclusions in it are the summary the analyst or client wrote. See the example case.
Tuning is proposed by the model and decided by you. test_tuning records a change as proposed and returns a review: an
option-by-option diff of an edited rule, with checks (did rev go up? does the rule keep a fast pattern?), or the config entry a
filter adds. The report shows the same. When you agree, update_decision records the verdict (accepted or rejected, with its
history), and export_tuning turns the accepted changes into a rules fragment and a config snippet (.rules and .lua files if you
give it a path) to apply to your rules and snort.lua.
Safety model
The server parses untrusted evidence and passes model-written text to native binaries, so:
No shell. Commands are argument lists; there is no
shell=Trueanywhere (a test greps for it).No config injection. Rules and settings go into generated files in the run directory. Values that appear in generated Lua pass through one typed, tested escaping module that rejects anything not plain. Snort's
--luais never used. Rule sets containingincludedirectives are refused, since Snort would open the named file.Path control. Reads are limited to
allowed_rootsand the case directory after symlinks are resolved; devices, FIFOs, sockets and directories are refused. Evidence is hashed at registration. For every run the file is opened without following symlinks, its location is checked on the open file, and it is copied into the run directory while being hashed; the engine analyses that verified copy (deleted afterwards), so a file cannot be swapped between the check and the use, and the engine never sees the original filename.Offline only. The only packet source is
-r <one registered pcap>with the pcap DAQ in read-file mode. A final gate rejects interface, inline, other-DAQ and plugin-path arguments, and no tool has a parameter for them.Limits. Per-run timeout that kills the whole process group (or the named container), input-size caps, alert caps per call, a minimal environment, and refusal to run as root. Docker runs have no network, a read-only root filesystem, and no capabilities.
Choose Docker for evidence you do not trust. The Docker backend runs Snort with no network, a read-only filesystem and no capabilities. The native backend runs Snort as your own user with none of that isolation, so a flaw in Snort's parsing of a hostile capture would run with your privileges. (Access to the Docker daemon is itself powerful; only run this as a user you trust with it.)
Bounded output. Raw stdout/stderr are never returned: only parsed results and a short error excerpt. Payloads are shown as truncated hex plus printable ASCII.
Write discipline. Writes go only inside the case directory or your
allowed_roots, at paths you name, and never through a symlink. Existing files, and anything registered as evidence, are never overwritten:write_rules_filereturns a diff instead.Private by default. Case directories and the files in them are owner-only. Cell values in the CSV export that a spreadsheet would read as a formula (starting with
=,+,-or@) are quoted, and findings and notes cannot add headings to the report.
Talos Registered/Subscriber rules need your Oinkcode and licence: the workbench never downloads them and accepts them only as local files you point to.
Known limitations
Verified on Snort 3.10.0.0 only (the Docker image). The latest upstream is 3.12.4.0; the integration tests pin the behaviours below so an upgrade shows up as a test diff. Native Linux/macOS installs are supported by the code but untested.
Un-ACKed TCP data is not inspected by Snort 3.10, so rules on service buffers (
http_uriand similar) can miss on flows where the peer never acknowledged the request (truncated or one-sided captures). Raw-packet rules still fire.run_pcapreports these flows instream_caveats;flush_one_sided=truerecovers purely one-sided flows at the cost of changing how some raw rules match on them. Details in docs/notes.md (A6b).pkt_numfor alerts on reassembled data is the packet that triggered the flush (usually the peer's ACK), not the packet carrying the matched bytes.explain_alertreassembles the stream itself and reports the data packets.Each run temporarily copies its pcap into the run directory (up to
max_input_bytes) and deletes the copy afterwards, so large captures cost disk space and time while a run is in progress.convert_snort2does not followincludelines (it runssnort2lua -i). A Snort 2 config that pulls rules in withincludeconverts only what is written in the file itself, and the result is reported aspartialwith notes naming the skipped lines. Convert each rules file separately.Snort is silent about a rule with no
sid.validate_rulesadds separateworkbench_lintfindings for this and other hygiene problems.Validation reports cascade noise: a missing
;makes Snort misreport the next rule. The workbench re-checks suspect rules singly.SELinux. On hosts that enforce SELinux, Docker bind mounts may need volume labels (
:z), which the Docker backend does not add.Evidence text reaches the model. Packet payloads, rule messages and filenames appear in tool output, so a hostile capture can contain text written to look like instructions. The server's instructions tell the model to treat it as data, but that is guidance, not enforcement: review what the model proposes, particularly rule changes.
One client per server process. The active case is server state, so concurrent clients would share it. Run one server per client.
One capture is one sample; results on synthetic or small captures say little about production traffic.
Phases 2 (Zeek/tshark pivots) and 3 (Zircolite/Sigma regression harness) are designed in docs/plan.md but not built.
Development
uv run pytest -q # unit + parser tests, no Snort needed
DW_INTEGRATION=1 uv run pytest -q tests/integration # needs Docker or native Snort 3
uv run scripts/make_fixtures.py # regenerate synthetic captures (scapy)
uv run scripts/record_snort_outputs.py # re-record real Snort output used by parser testsReal-world captures are never committed. To run the opt-in real-capture test, set DW_REAL_PCAPS to a directory of your own pcaps
(DW_INTEGRATION=1 DW_REAL_PCAPS=~/captures uv run pytest -q tests/integration -k real); it is skipped when unset.
Contributing and security
See CONTRIBUTING.md for setup, the rules the code relies on, and how to add an engine. Report vulnerabilities privately as described in SECURITY.md.
License
Apache License 2.0. See LICENSE.
Available Tools
21 toolsadd_findingB
Record a short statement supported by specific runs, e.g. 'SID 1000042 fires 300 times, 95% from 10.0.4.20'. Run and evidence ids must exist. Starts as status 'open'. Example: add_finding(statement="SID 1000042 is dominated by one scanner", run_ids=["r-0002"])
Inputs: run_ids (required), statement (required), evidence_ids.
add_noteC
Add free-text context to the case. Example: add_note(text="Capture taken during the 14:00 scan window")
Inputs: text (required).
create_caseB
Create a case and make it active. Every run, finding and decision is recorded in it. Example: create_case(title="Noisy SID 1000042 on the proxy capture")
Inputs: title (required).
diff_runsC
Alerts added, removed and changed between two runs, grouped by SID. Example: diff_runs(run_a="r-0002", run_b="r-0003")
Inputs: run_a (required), run_b (required).
explain_alertA
Facts about one alert: the rule text and parsed options, the 5-tuple/direction/service, a bounded hex+ASCII dump, and which content/pcre options appear in the payload and at what offsets (wire bytes reassembled from the pcap, plus Snort's own inspected buffer). Report facts; you do the explaining. Example: explain_alert(run_id="r-0002", alert_idx=0)
Inputs: run_id (required), alert_idx (required).
export_caseA
Export the case as a Markdown report ('markdown') or a Timesketch-importable CSV ('timesketch_csv'). Without a path the file goes in the case's exports/ folder. A path must end in .md or .csv and an existing file is only replaced with overwrite=true. Example: export_case(format="markdown")
Inputs: path, format, overwrite.
get_alertsB
Page or filter a run's alerts by SID, source/destination address or packet range. Example: get_alerts(run_id="r-0002", sid=1000042, src="10.0.4.20", limit=20)
Inputs: run_id (required), dst, sid, src, +4 more.
get_caseB
Summary of the active case: evidence, findings, decisions, notes and recent runs.
Inputs: no named parameters.
get_statusA
Engine versions and backend (native or Docker), config location, allowed_roots, limits and the active case. Call this first if something is unavailable.
Inputs: no named parameters.
list_casesB
List cases in the case directory with ids, titles and status.
Inputs: no named parameters.
open_caseC
Make an existing case active. Example: open_case(case_id="20261022-noisy-sid-1000042")
Inputs: case_id (required).
pcap_summaryB
Packet count, time range, protocols, top talkers and services for a registered pcap. Example: pcap_summary(evidence_id="e-0001")
Inputs: evidence_id (required).
profile_rulesB
Run the rules with Snort's rule profiler and return the slowest rules with checks, matches and time. Example: profile_rules(pcap_id="e-0001", rules_id="e-0002", top=10)
Inputs: pcap_id (required), top, rules_id, rules_text.
register_evidenceA
Register a pcap/pcapng or rules file in the active case (hashed, type-detected, never copied or modified). The path must be under allowed_roots. Returns the evidence id (e-0001 ...) used by other tools. Example: register_evidence(path="~/captures/proxy.pcap")
Inputs: path (required), type, label.
run_pcapB
Run rules against a registered pcap. Returns the run id, total alerts, counts by SID with msg, the first alerts, and stream_caveats (flows where Snort cannot inspect un-ACKed data). home_net is an IP/CIDR or list like '[10.0.0.0/8,192.168.0.0/16]'; no raw Lua is accepted. Example: run_pcap(pcap_id="e-0001", rules_id="e-0002", home_net="10.0.0.0/8")
Inputs: pcap_id (required), profile, home_net, rules_id, +3 more.
suggest_tuning_targetsB
Rank the noisiest SIDs in a run by count, with distinct sources/destinations and how concentrated the hits are in one source. Example: suggest_tuning_targets(run_id="r-0002")
Inputs: run_id (required), top.
test_tuningA
Apply one structured change to a copy, re-run the same pcap, and return before/after counts per SID. Records a decision. change.kind is one of: suppress: {kind, sid, track:'by_src'|'by_dst', ip} event_filter: {kind, sid, type:'limit'|'threshold'|'both', track, count, seconds} rate_filter: {kind, sid, track, count, seconds, new_action, timeout} rule_edit: {kind, sid, rule:''} Example: test_tuning(before_run_id="r-0002", change={"kind":"suppress","sid":1000042,"track":"by_src","ip":"10.0.4.20"}, reason="known scanner")
Inputs: change (required), reason (required), before_run_id (required).
update_caseA
Write the case summary (your analysis and conclusions, Markdown allowed) and/or set the status to 'open' or 'closed'. The summary appears at the top of the exported report, so cite run and finding ids in it. Call it again to revise; each change is logged. Example: update_case(summary="SID 1000042 is too broad (f-0001, r-0003); the rewrite in r-0005 fixes it.", status="closed")
Inputs: status, summary.
update_findingB
Change a finding's status (open, confirmed, dismissed) or wording; history is kept. Example: update_finding(finding_id="f-0001", status="confirmed")
Inputs: finding_id (required), status, statement.
validate_rulesA
Check rules with Snort itself. Give a registered rules file (rules_id) or inline rules_text. Returns errors as {line, sid, error, plain}, warnings, and workbench_lint hygiene items (e.g. a missing sid, which Snort does not report). Fast enough for a write-test-fix loop. Example: validate_rules(rules_text='alert http any any -> any any ( msg:"x"; http_uri; content:"abc"; sid:1000001; rev:1; )')
Inputs: rules_id, rules_text.
write_rules_fileA
Save rules to a path you name. Existing files are never overwritten unless overwrite=true; instead a diff is returned. Registered evidence is never overwritten. Example: write_rules_file(path="~/rules/local-new.rules", rules_text="alert ...")
Inputs: path (required), rules_text (required), overwrite.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 21 tools
Each tool targets a distinct resource+action (case CRUD, evidence registration, findings/notes, pcap analysis, tuning tests). A few tools operate on the same run object (get_alerts, explain_alert, suggest_tuning_targets, diff_runs) but their descriptions clearly separate enumeration, single-alert forensics, ranking, and comparison. Case tools like get_case/open_case/update_case are close but distinguishable by their descriptions.
Names use snake_case throughout and are mostly verb_noun (create_case, add_finding, run_pcap, test_tuning). Minor deviations exist, notably the noun-first 'pcap_summary' and the verb-less 'diff_runs', but these remain readable and predictable.
21 tools across case management, evidence, rule validation/execution, alert analysis, and tuning is on the heavier side but each earns a distinct role. The surface feels comprehensive rather than padded for a detection-engineering workflow.
Covers the full case lifecycle (create/open/list/get/update/export), evidence registration, findings, notes, pcap analysis, rule validation, running, alert inspection, profiling, tuning tests, run diffing, and rule writing. Minor gaps: no standalone run listing/summary or evidence listing/removal, so discoverability of past runs relies on get_case.
Maintenance
Related MCP Connectors
Governed data discovery, exact queries, decisions, simulations, and runtime utilities over MCP.
Remote MCP for A2A failure replay MCP, structured receipts, audit logs, and reviewer-ready evidence.
Evidence-readiness MCP server: validate, audit, and score briefs, memos, and evidence packs.
Self-hosted MCP server: 26 deterministic dev, security, and EVM tools.
Related MCP Servers
- AlicenseCqualityBmaintenanceEnables coding agents to query, compare, and audit local profiler traces, benchmarks, memory captures, and execution evidence without uploading code or data, using CLI and MCP interfaces.111125 PyPI234MIT
- AlicenseNot gradedqualityCmaintenanceEnables MCP clients to run hardened, read-only radare2-based static binary triage tools, including disassembly, cross-reference following, string decoding, function renaming, and evidence-checked report generation without executing the sample.MIT
- AlicenseNot gradedqualityBmaintenanceEnables AI assistants and security analysts to inspect PCAP/PCAPNG captures through MCP tools or REST endpoints, reassembling TCP/UDP streams, extracting payload artifacts, detecting CTF flags, credential leaks and network anomalies, and generating formal PDF or Markdown forensic reports. Every finding is bound to exact frame numbers, timestamps and endpoints with SHA-256 deduplicated evidence provenance.MIT
- FlicenseNot gradedqualityAmaintenanceEnables security researchers to run network scanning, binary symbol and disassembly analysis, string extraction, and live or pcap traffic inspection through an MCP client, with optional Docker-based sandboxed command execution.-