Network Data Analysis (read-only SQL)
MCP Server
- Server:
analysis-mcp(NetClaw-authored, spec 092) - Tools:
analysis_status,analysis_datasets,analysis_query - Engine: DuckDB, in-memory, sandboxed and locked
What this reads, and what it can never read
Loads files from an allowlist of roots — ~/.openclaw/nsm/runs (Zeek/Suricata output
from nsm-mcp), the workspace output directory, and ~/.openclaw/analysis for your own
exports.
NetClaw's own stores are permanently unreachable: ~/.openclaw/memory/,
~/.openclaw/rag/, ~/.openclaw/n2n/ and ~/.openclaw/gait/, plus .ssh, .aws, .kube
and .env. A generic SQL surface over those would be a backdoor, not an analysis tool.
That is not enforced by pattern matching. Datasets are materialised, then DuckDB's own
enable_external_access=false and lock_configuration=true close every filesystem and
network path irreversibly for the life of the process. Verified: read_csv('/etc/passwd'),
glob('/home/**'), ATTACH of the memory or RAG stores, COPY … TO, INSTALL/LOAD, and
re-enabling access all raise.
If you need a file analysed, put it under ~/.openclaw/analysis — do not try to reach it in
SQL, because you cannot.
Workflow: analyse a capture
- Produce the data first.
nsm_analyze(skillnsm-session-pivot) writes Zeek logs and Suricataeve.json. Without a run, this surface has nothing to read and says so. analysis_status— is the sandbox locked, how many datasets loaded, what are the caps?analysis_datasets— table names, row counts, columns. Read the columns before writing SQL; guessing column names wastes a round trip.analysis_query— one read statement per call.
Zeek tables carry their real column names (id_orig_h, id_resp_p, uid), lifted from
the log's #fields header. Without that they would be column0…columnN and unusable.
The pivot that makes this worth using
Every Zeek log shares uid with conn.log, so a join is the session pivot — and unlike
walking logs one at a time, it aggregates:
SELECT c.id_orig_h, c.id_resp_h, c.service, h.method, h.host, h.uri
FROM zeek_<run>_conn c
LEFT JOIN zeek_<run>_http h USING (uid)
ORDER BY c.ts
Counting questions a per-log view cannot answer:
-- top talkers by connection count
SELECT id_orig_h, count(*) AS conns FROM zeek_<run>_conn
GROUP BY 1 ORDER BY conns DESC LIMIT 20
-- which services appeared at all
SELECT service, count(*) FROM zeek_<run>_conn GROUP BY 1 ORDER BY 2 DESC
-- Suricata alert signatures by frequency
SELECT json_extract_string(alert, '$.signature') AS sig, count(*)
FROM suricata_<run>_eve WHERE event_type = 'alert' GROUP BY 1 ORDER BY 2 DESC
Reading results honestly
truncated: truemeans you are looking at a page. Never present a capped result as a total — runCOUNT(*)for the real number. The tool reports this ingaps.- Zeek columns are all text. Datasets load as varchar so a malformed field cannot abort
the load; cast explicitly (
CAST(duration AS DOUBLE)) and say that you did. - A row count reflects what was LOADED, not what existed. Files above the size cap are
skipped and listed in
notes; a per-table row cap applies. Checkanalysis_statusbefore claiming completeness. 0 datasetsmeans no exports exist, never "the network was quiet."- This inherits every caveat of its source. A Zeek run made with checksum validation on
may be missing whole protocol logs (see
nsm-session-pivot) — and SQL over an incomplete log is confidently wrong. Check the posture of the run you are querying.
Important Rules
- Read-only.
INSERT/UPDATE/DELETE/DROP/CREATE/ATTACH/COPY/INSTALL/SETare refused, and independently impossible after lockdown. - One statement per call. A stacked second statement is refused.
- Queries time out (default 30s) and are interrupted, not left running.
- Record in GAIT — log the query and the dataset it ran against, not just the answer.
Integration with Other Skills
| Skill | How They Work Together |
|---|---|
nsm-session-pivot |
Produces the Zeek logs this queries; use it for single-session detail |
nsm-ids-triage |
Produces eve.json; use SQL here to aggregate alerts across a capture |
packet-analysis |
Drop to individual packet decode once SQL narrows the field |
document-generation |
Turn a query result into a report table |
gait-session-tracking |
Record all analysis runs |
Environment Variables
ANALYSIS_QUERY_TIMEOUT— per-query seconds (default 30)ANALYSIS_MAX_RESULT_ROWS— result cap (default 500)ANALYSIS_EXTRA_ROOTS— extra allowlisted roots,os.pathsep-separatedANALYSIS_MAX_FILE_BYTES/ANALYSIS_MAX_ROWS— load caps