Skip to main content
Glama

Klaxon

The alarm tells you that. Klaxon tells you what.

Klaxon is a read-only proxy in front of your Wazuh 5 indexer. A language model asks questions in plain language; Klaxon queries the indexer, reads the schema, tests decoders, and reports what it actually found — including when it found nothing, and why. Works with Claude Desktop, Claude Code, local models through Ollama, and Open WebUI.

Every query runs through Klaxon, and the response is masked before it reaches an external LLM: a value under a configured field (user.name, source.ip, …) always becomes the same deterministic token ([USER_…], [IP_…]), aggregation keys included. This is pseudonymization, not anonymization — tokens are deterministic and reversible by anyone holding the salt (see LLM-safety guarantees).


Quick start

Requirements

  • Wazuh 5.x indexer reachable over HTTPS (search/schema also work against 4.x)

  • Python 3.11+ or Docker

  • an MCP client — Claude Desktop, Claude Code, ollmcp, Open WebUI 0.6.31+

1. Install

python3 -m venv .venv
.venv/bin/pip install klaxon-mcp

Or build the Docker image (klaxon-mcp is the entry point):

docker build -t klaxon-mcp .

2. Point it at your indexer

export KLAXON_INDEXER_URL=https://indexer.example:9200
export KLAXON_INDEXER_USER=wazuh-readonly
export KLAXON_INDEXER_PASSWORD=...

KLAXON_INDEXER_URL is the only required variable. Add KLAXON_MANAGER_URL for manager/detectors and KLAXON_ENGINE_URL for tester_sessions; set KLAXON_VERIFY_SSL=false only for a self-signed lab cluster.

Masking is off by default and opt-in:

export KLAXON_ANONYMIZE_EXTERNAL_LLM=true   # mask tool output for external models
export KLAXON_ANONYMIZATION_SALT=change-me-to-a-long-random-secret  # stable tokens

With the switch on, output is masked unless KLAXON_LLM_BASE_URL points at a loopback address (http://localhost:11434 for Ollama) — a local model keeps receiving unchanged data. An optional config.yaml (KLAXON_CONFIG) holds only what you change; environment variables always win:

anonymization:
  mask_fields:                 # or KLAXON_ANONYMIZATION_MASK_FIELDS
    - "source.ip"
    - "user.name"
    - "host.hostname"
  mask_aggregation_keys: true  # ON by default; false disables agg-key masking

4. Start it

klaxon-mcp    # stdio — your MCP client spawns it (klaxon is an alias)

With Docker: docker run --rm -i --env-file .env klaxon-mcp.

5. First masked result

Ask your client: "Show me the last login by user.name=alice in wazuh-events-v5-*." Klaxon runs search(index="wazuh-events-v5-*", body=…) and returns the masked response:

{
  "hits": { "hits": [ { "_source": {
      "user":    { "name": "[USER_9f2a1c467dd5e2b8]" },
      "source":  { "ip": "[IP_5c01e73f9a2b4c1d]" },
      "message": "user [USER_9f2a1c467dd5e2b8] logged in via ssh from [IP_5c01e73f9a2b4c1d]"
  } } ] }
}

The same value always maps to the same token. Masking is pseudonymization, not anonymization, and it has documented blind spots — see docs/llm-safety.md (in particular the verified leaks in "Known limitations") before pointing an external model at Klaxon.


Related MCP server: wazuh-mcp

Basic usage

The handful of tools a normal user needs (full reference: docs/TOOLS.md):

Tool

What it does

One-liner example

search

Any query against any index, raw JSON back

search(index="wazuh-events-v5-*", body={"query": {"match_all": {}}})

schema

Which fields exist — and which actually carry data

schema(index="wazuh-events-v5-*", prefix="wazuh.agent.")

field_coverage

How complete each field is, window vs all history

field_coverage(index="wazuh-events-v5-*", prefix="event.")

findings_overview

Findings by severity, agent, title, category

findings_overview(hours=48)

logtest

Push a raw line through the decoder chain

logtest(event="<raw line>")

gdpr_check

Find sensitive fields the mask list should cover

gdpr_check(index="wazuh-events-v5-*")

klaxon_posture_check

Read-only security posture: facts + gaps, no verdict

klaxon_posture_check(tenant="customer-a")

On an unfamiliar cluster, start with field_coverage. Every thin result gets a notice block before the data (empty aggregation, capped size, missing index — all return HTTP 200 with nothing).


Configuration (essentials)

The keys a normal user changes day to day. Full reference: docs/configuration.md.

Variable / key

What it does

Default

KLAXON_INDEXER_URL

Indexer endpoint

— (required)

KLAXON_INDEXER_USER / KLAXON_INDEXER_PASSWORD

Basic-auth credentials

empty

KLAXON_ANONYMIZE_EXTERNAL_LLM

Master masking switch

false

KLAXON_ANONYMIZATION_SALT

Secret for token derivation (stable tokens)

random+persisted

KLAXON_ANONYMIZATION_MASK_FIELDS

Fields masked wholesale (user.name, source.ip, …)

built-in list

KLAXON_ANONYMIZATION_MASK_AGGREGATION_KEYS

Mask aggregation bucket keys too

true (fail-closed)

KLAXON_ANONYMIZATION_MASK_FREE_TEXT_USERS

Mask usernames inside free text

true

KLAXON_VERIFY_SSL

TLS verification

true

KLAXON_MCP_AUTH_TOKEN

Required bearer token when serving over HTTP

empty


Advanced topics

The deep material lives in dedicated docs — linked, not duplicated:

  • GDPR plausibility checker (classification layers, custom rules, sampling, reports) → docs/gdpr-checker.md

  • Ingest masking / Option B masked stream (pipeline, ISM, index templates, quarantine for masking failures, sync job) → docs/option-b-masked-stream.md

  • Multi-tenant setup (fields.yaml, klaxon masking generate, salt, namespacing) → docs/multi-tenant.md

  • Drift prevention & CI (pre-commit drift hook, provenance fingerprints, fail-closed startup, sync preflight, --verify-config) → docs/drift-prevention.md

  • Token scheme & security model (HMAC, salt, self-test, why 16 hex) → docs/security-model.md

  • Security concept: brute-force re-identification risk (pseudonymization vs anonymization, salt as secret) → docs/security-concept.md

  • Salt rotation runbook (no scheduled rotation; only on suspicion; response-layer + masked-stream paths) → docs/salt-rotation-runbook.md

  • LLM safety: using Klaxon safely with an LLM (masked-stream-first routing, ready-to-copy system prompt, pseudonymization boundary, residual gate) → docs/llm-safety.md

  • Running it on another machine (HTTP transport, auth, TLS, CORS) → docs/TOOLS.md, ARCHITECTURE.md


Development

.venv/bin/pip install -e ".[dev]"
.venv/bin/pytest        # full suite
.venv/bin/mypy          # strict type check
.venv/bin/ruff check src

Option B generator self-tests (see docs/drift-prevention.md):

klaxon masking selftest --tenant customer-a
klaxon masking generate --check   # CI/pre-commit drift check

Deploy the masking artifacts to the indexer in one idempotent, ordered, self-verifying step (preflight + GET-back verification + a _simulate smoke test; --dry-run / --rollback):

klaxon masking deploy --tenant customer-a --dry-run   # plan only, no writes
klaxon masking deploy --tenant customer-a             # needs KLAXON_INDEXER_*

Remove the Option B masking infrastructure from the indexer cleanly, leaving the raw Wazuh streams untouched (destructive — preview with --dry-run; a mandatory verification phase proves nothing klaxon-* is left and the raw streams are intact):

klaxon masking teardown --tenant customer-a --dry-run       # plan only, no writes
klaxon masking teardown --tenant customer-a --yes           # needs KLAXON_INDEXER_*
klaxon masking teardown --tenant customer-a --yes --purge-sync-state
#   ^ also delete the sync checkpoint marker (default: keep it so a future
#     re-setup can resume from the last checkpoint)

The live integration test (klaxon masking test) needs real indexer credentials — see docs/option-b-masked-stream.md. Release history: CHANGELOG.md.


Documentation


License

Apache-2.0 — see LICENSE.

Wazuh is a registered trademark of Wazuh Inc. Klaxon is an independent project and is not affiliated with, endorsed by, or sponsored by Wazuh Inc.

Support

For professional support, please contact support@sec73.io.

Available Tools

10 tools
detectorsA

List or fetch OpenSearch Security Analytics detectors.

Detection in Wazuh 5 lives in the indexer, not in the Engine. These detectors are what produces the documents in wazuh-findings-v5-*.

The plugin exposes no list-all endpoint, so list is implemented as POST /_plugins/_security_analytics/detectors/_search with match_all. Detector documents are nested under the detector path, which matters if you search them by name.

Args: action: "list" for all detectors, "get" for a single one by id. detector_id: Required when action is "get". size: Maximum number of detectors to return for "list". Defaults to 50.

Inputs: size, action, detector_id. Returns: result

field_coverageA

Measure what share of the documents actually carries a value per field.

The normalisation-quality measurement, callable without query DSL. For each mapped field it reports the document count and coverage inside a time window and across the whole datastream, because those are different questions:

event.action, wazuh-events-v5-network-activity*
  whole datastream (10,238,381 docs)     8.1%
  last 24 hours       (348,247 docs)    71.0%
  last 12 hours                        100.0%

A decoder fix had landed hours earlier. All three numbers are correct. The window describes the pipeline as it runs now, the datastream describes the stored history — quote the one you mean. When they differ by more than 20 percentage points the diagnostics block says so explicitly, because that gap is the signature of a change in normalisation inside the datastream.

Fields with 0% coverage are listed, never filtered: mapped-but-never- populated is the agent.id trap and the most important result this measurement produces.

Coverage is three-valued — populated, not populated, not measurable. An exists aggregation returns 0 for a field the mapping declares "index": false no matter what the documents hold, so the mapping is read first and such fields are reported as not measurable, with dashes, never as 0%. Verified: event.original in wazuh-events-v5-network-activity* is index:false and doc_values:false, matches 0 of 10,243,389 documents, and carries the complete raw log line in _source of every document sampled. For those fields the tool samples _source and reports in how many of the sampled documents the key is present — evidence rather than a coverage figure.

Cost scales with the field count — the schema has 2351 fields — so the listing is capped at KLAXON_SCHEMA_FIELD_LIMIT (default 200) and the cap is reported. Pass a prefix to measure a namespace instead of a truncation.

Args: index: Index or datastream pattern, e.g. "wazuh-events-v5-network-activity*". prefix: Restrict to a field namespace, e.g. "source." or "wazuh.". hours: Size of the time window ending now, in hours. Default 24. min_docs: Hide fields below this document count in the window. Default 0, which hides nothing. Any higher value removes the 0% fields — the ones worth looking at — so the output says how many it dropped.

Inputs: index (required), hours, prefix, min_docs. Returns: result

findings_overviewA

Summarise wazuh-findings-v5-* by severity, agent, rule title and category.

A frozen query for the breakdown every report starts with, so that producing it needs no valid OpenSearch query DSL. search still covers everything else; this tool only removes the need to hand-write the one aggregation that recurs.

Severity is wazuh.rule.level, a keyword holding a string — critical, high, medium, low, informational — not a Wazuh 4.x numeric level. The whole scale is printed in canonical order with an explicit 0 for the levels that did not occur, because a terms aggregation returns only the values it found: a missing critical bucket cannot distinguish "none occurred" from "never populated". Any value outside the scale is listed as well and marked UNKNOWN. Before aggregating, the tool probes whether wazuh.rule.level is populated at all and says so instead of printing a table of zeros.

Output is a compact set of tables, not raw JSON. The request that produced them is in the footer if you want to re-run or extend it via search.

Args: hours: Size of the time window ending now, in hours. Default 24. top_agents: How many agents to list, by finding count. Default 10. top_titles: How many rule titles to list. Default 10.

Inputs: hours, top_agents, top_titles. Returns: result

gdpr_checkA

Run the DSGVO plausibility check on an index: find sensitive fields.

Reads the index mappings, samples a few documents, and classifies the fields by three heuristics in decreasing certainty: custom rules from config.yaml (gdpr_checker.custom_patterns), field-name patterns (source.ip, user.name, host.hostname, user.email, ...), and sampled values (an actual value like 192.168.1.100 reveals an IP even when the field name does not).

Priorities: IPs, usernames and e-mails are directly personal (high); hostnames and agent ids are indirectly personal (medium); free-text fields that embed personal data are flagged as such. Fields already in the anonymization mask_fields are reported as covered, not re-suggested.

With apply=true the suggested fields are merged into anonymization.mask_fields of config.yaml (KLAXON_CONFIG), the action is appended to gdpr_check.log, and gdpr_compliance_report.json is written. The change takes effect for the running server on restart unless KLAXON_ANONYMIZATION_MASK_FIELDS is set, which always overrides the file. apply=false (default) is a dry run: suggestions only, nothing changed.

Args: index: Index or datastream pattern, e.g. "wazuh-events-v5-*". prefix: Restrict to a field namespace, e.g. "user." or "source.". sample_docs: Documents to sample for content analysis. Defaults to KLAXON_GDPR_SAMPLE_SIZE (10). 0 disables sampling. apply: When true, merge the suggested fields into config.yaml and log. exclude: Field names to skip (e.g. internal fields without GDPR relevance). as_json: When true, return a machine-readable JSON report instead of the table.

Inputs: index (required), apply, prefix, as_json, +2 more. Returns: result

klaxon_posture_checkA

Read-only security/DSGVO posture check: facts + gaps, never a verdict.

Returns one check: status — fact line per item: masking, response gate, mode (response-layer vs Option B masked stream), pipeline drift, salt strength, quarantine backlog, RBAC roles, retention and the startup fail-closed check. Statuses are OK / WARN / unknown only — there is no overall compliance verdict and no legal judgment (the same principle as gdpr_check: report what can be established, nothing more).

The salt is NEVER emitted — not even partially, not hashed. No PII, raw values, tokens, hostnames, usernames, IPs or sampled values appear in the output; only counts, booleans, statuses, index patterns, durations and role names. When the indexer is unreachable a check reports "unknown — " instead of a guessed value. Read-only: nothing is written to the indexer, nothing deployed, no config change.

Args: tenant: Tenant whose masked/quarantine streams and RBAC roles are checked (default "customer-a"). hours: Quarantine-backlog window in hours (default 24).

Inputs: hours, tenant. Returns: result

logtestA

Run a raw log line through the Wazuh 5 decoder chain and return the result.

Calls the Content Manager plugin on the indexer. The response shows which decoders matched and what the normalised WCS document looks like, which is the way to find out why a field is empty in the index.

Args: event: The raw log line to decode. location: Log source path, e.g. "/var/ossec/logs/opnsense_syslog.log". queue: Queue id of the originating source. Defaults to 49. space: One of test, custom, standard — logtest supports no others. Custom decoders live in "custom"; "standard" carries only the shipped ruleset. A valid name does not guarantee the environment is provisioned — use the tester_sessions tool to see which ones exist and are enabled. trace_level: One of NONE, ASSET_ONLY, ALL. Defaults to ASSET_ONLY, which is the level that reveals the matched decoder chain. NONE returns the normalised event only; ALL adds per-asset trace detail. integration: Integration name for the detection phase. Without it the plugin normalises the event and reports detection as "skipped".

Inputs: event (required), location (required), queue, space, +2 more. Returns: result

managerA

Issue a GET against the Wazuh manager API and return the response unchanged.

A deliberately thin passthrough. The manager API is the volatile half of Wazuh 5 and breaks further at GA (/var/ossec moves to /var/wazuh-manager, clustering becomes the default, agent id 000 disappears), so this tool adds no interpretation on top of it.

Non-2xx responses are returned as they are, status code included. In Wazuh 5 several 4.x endpoints are gone and their 404 is the correct answer, not a failure to hide:

  • /rules 404, the Engine has no RULE content type any more

  • /manager/logs 404

  • /manager/stats/remoted 404 Verified working: /agents, /syscollector/{agent_id}/... Changed response schemas: /cluster/healthcheck (no enabled field), /cluster/nodes (no node_type field).

The security root is restricted to /security/users/me and /security/users/me/policies — enough to tell RBAC filtering apart from an empty deployment when /agents returns less than expected, without enumerating the deployment's accounts, roles and policies through a tool meant for agent and event data.

Args: path: Manager API path, e.g. "/agents". params: Optional query parameters.

Inputs: path (required), params. Returns: result

schemaA

List the fields of a Wazuh 5 index and how many documents actually fill them.

Use this before writing any aggregation. The Wazuh 5 engine schema defines 2351 fields, and a mapped field is not necessarily a populated one: agent.id and wazuh.agent.id are both mapped as keyword, but only wazuh.agent.id ever carries a value. Aggregating on the wrong one returns zero buckets and HTTP 200 — no error at all. With only_populated=true this tool reports the document count per field, which makes that distinction visible.

Namespace sizes in the engine schema: wazuh=492, threat=444, process=391, file=144, tls=77, host=57, observer=53, dll=46, user=46, client=35, destination=35, server=35.

Args: index: Index or datastream pattern, e.g. "wazuh-events-v5-network-activity*". prefix: Restrict to a field namespace, e.g. "wazuh." or "source.". Strongly recommended — an unfiltered listing over 2351 fields is capped. only_populated: When true (default), issue a second pass with exists aggregations and return only fields holding a value in at least one document.

Inputs: index (required), prefix, only_populated. Returns: result

tester_sessionsA

List the Wazuh 5 engine test sessions — the environments logtest can use.

logtest answers a call naming an environment that does not exist with HTTP 200 and "The '' environment does not exist" buried in the body. This tool is how to find out which environments there actually are, and whether they are enabled.

Calls POST /_internal/tester/table/get on the engine's internal HTTP API. That server runs inside the manager container on its own port, so it needs KLAXON_ENGINE_URL — the indexer and manager URLs do not reach it.

Read-only by design. The engine also exposes session/post, session/delete and session/reload; none of them are wired up here. Sessions are recreated on every policy import through the Content Manager API, so a hand-made session disappears at the next import — a create tool would only invite a workaround that does not hold.

Args: action: Only "list" is supported.

Inputs: action. Returns: result

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A4.5/5.0

    Scored across 10 tools

    Disambiguation4/5

    Each tool covers a distinct operation: raw search, schema introspection, log decoding, manager passthrough, detector listing, test session listing, prebuilt findings summary, field coverage, GDPR classification, and posture check. search and findings_overview overlap somewhat, and schema vs field_coverage both address field population, but the descriptions clearly differentiate their scopes and outputs.

    Naming Consistency4/5

    All tool names use lower_snake_case and are descriptive, but there is no uniform verb_noun pattern: search is a bare verb, schema/manager/detectors are nouns, and others are noun+noun or noun+verb compounds. The style is consistent enough that names remain predictable, with only minor deviations from a stricter convention.

    Tool Count5/5

    Ten tools is well within the ideal range, and each serves a distinct investigative or compliance function. No tool feels redundant or superfluous, and the count matches the server's multi-faceted security analytics scope without becoming unwieldy.

    Completeness4/5

    The set covers the core Wazuh analysis lifecycle: querying, schema understanding, decoding, findings summarization, field population measurement, GDPR checks, and posture review. Notable gaps exist around detector lifecycle management (only list/get, no create/update/delete) and the manager passthrough is GET-only, though the descriptions explicitly justify these omissions as intentional.

    Maintenance

    ActivitySlowing
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      B
      quality
      D
      maintenance
      A Model Context Protocol server that enables querying and analyzing Wazuh security logs stored in OpenSearch, with features for searching alerts, getting detailed information, generating statistics, and visualizing trends.
      9
      2
      MIT
    • A
      license
      B
      quality
      B
      maintenance
      An MCP server for the Wazuh SIEM/XDR platform that enables users to query agents, security alerts, detection rules, and decoders through Claude or other MCP clients. It provides specialized tools and prompts for investigating security alerts, performing agent health checks, and generating environmental security overviews.
      28
      78 npm
      9
      MIT
    • A
      license
      A
      quality
      B
      maintenance
      AI-powered MCP server that enables security analysts to query Wazuh SIEM/XDR for alert triage, threat hunting, compliance audits, and incident response through natural language prompts.
      28
      13
      MIT