Logtest architecture and developer guide
Component overview
The logtest flow involves four layers: a thin REST handler, a transport action that owns request validation, an orchestration service, and the external services it calls.
RestPostLogtestAction → TransportLogtestAction → LogtestService → EngineService + SecurityAnalyticsService
RestPostLogtestNormalizationAction → TransportLogtestNormalizationAction → ↑ ↑
RestPostLogtestDetectionAction → TransportLogtestDetectionAction → ↑ ↑
(REST handlers) (Validation) (Orchestration) (External services)
REST handlers no longer validate requests or contain any business logic — that responsibility moved to the transport action layer. This is a plugin-wide pattern, not specific to logtest: every REST handler in rest/service/ delegates to a same-named Transport*Action via client.execute(), per AbstractContentAction’s own javadoc (“Business logic has been moved to transport actions; REST handlers now delegate to the transport layer via client.execute()”).
RestPostLogtestAction (combined)
Path: rest/service/RestPostLogtestAction.java
The REST handler for POST /_plugins/_content_manager/logtest. It reads the raw request body into a LogtestRequest and calls client.execute(LogtestAction.INSTANCE, logtestRequest, listener). It performs no validation and does not interact with indices or external services directly.
TransportLogtestAction
Path: transport/TransportLogtestAction.java
The validation and dispatch layer for the combined endpoint. Responsibilities:
- Validates the request has content and is valid JSON.
- Validates the required field
space. - Validates that
spaceis not"draft". - Extracts the optional
integrationfield (if present) and strips it from the Engine payload. - Delegates to
LogtestService.executeLogtest(integrationId, space, enginePayload). IfintegrationIdisnull, only engine normalization is performed.
RestPostLogtestNormalizationAction
Path: rest/service/RestPostLogtestNormalizationAction.java
The REST handler for POST /_plugins/_content_manager/logtest/normalization. Reads the request body and calls client.execute(LogtestNormalizationAction.INSTANCE, ...). No validation.
TransportLogtestNormalizationAction
Path: transport/TransportLogtestNormalizationAction.java
Responsibilities:
- Validates the request has content and is valid JSON.
- Validates the required field
space. - Validates that
spaceis not"draft". - Strips the
integrationfield if present (not used for normalization). - Delegates to
LogtestService.executeNormalization(enginePayload).
RestPostLogtestDetectionAction
Path: rest/service/RestPostLogtestDetectionAction.java
The REST handler for POST /_plugins/_content_manager/logtest/detection. Reads the request body and calls client.execute(LogtestDetectionAction.INSTANCE, ...). No validation.
TransportLogtestDetectionAction
Path: transport/TransportLogtestDetectionAction.java
Responsibilities:
- Validates the request has content and is valid JSON.
- Validates the required fields
space,integration, andinput. - Validates that
spaceis not"draft". - Validates that
inputis a JSON object (not a string or array). - Delegates to
LogtestService.executeDetection(integrationId, space, inputEvent).
LogtestService
Path: cti/catalog/service/LogtestService.java
The orchestrator. Provides three public entry points:
executeLogtest()— Full combined flow (normalization + detection)executeNormalization()— Engine-only: forwards payload toEngineService.logtest()and returns the response directly withparseMessageAsJson()executeDetection()— Security Analytics-only: looks up integration, fetches rule IDs/bodies, evaluates viaSecurityAnalyticsService.evaluateRules(), and returns the result
The full logtest flow:
- No-integration shortcut — If
integrationIdisnull, delegates toexecuteEngineOnly(): runs the Engine normalization and returns the result withdetection.status: "skipped"andreason: "No integration provided". Steps 2–5 below are skipped. - Integration lookup — Queries
wazuh-threatintel-integrationsfor a document matchingdocument.id == integrationIdandspace.name == space. Returns 400 if not found. - Engine processing — Sends the event payload to the Wazuh Engine via
EngineService.logtest(). Extracts the normalized event from theoutputfield. The engine result fields (output,asset_traces,validation) are included directly in the response (no wrapper). - Rule fetching — Extracts rule IDs from the integration’s
document.rulesarray, then fetches rule bodies fromwazuh-threatintel-rulesbydocument.id, filtered by the same space. - Security Analytics evaluation — Passes the normalized event JSON and rule bodies to
SecurityAnalyticsService.evaluateRules(). - Response building — Combines engine and Security Analytics results into a single JSON response under the keys
normalizationanddetection.
Error handling:
- If the Engine fails (HTTP error or exception), Security Analytics evaluation is skipped and the response includes
status: "skipped"with the reason. - If no integration is provided, detection is skipped (normalization-only mode).
- If the integration has no rules, Security Analytics returns
rules_evaluated: 0, rules_matched: 0with success status. - If Security Analytics evaluation returns unparseable JSON, the result is
status: "error".
SecurityAnalyticsService / EventMatcher
The Security Analytics evaluation happens in the security-analytics repository:
SecurityAnalyticsServiceImpl.evaluateRules()— Parses Sigma rule YAML strings intoSigmaRuleobjects, then delegates toEventMatcher.EventMatcher.evaluate()— Flattens the normalized event JSON into dot-notation keys, then evaluates each rule’s detection conditions against the flat map. Returns a JSON result string.
The EventMatcher handles:
- Field-equals-value conditions (exact match, case-insensitive)
- Keyword (value-only) conditions (searches all event fields)
- Wildcards (
*for multi-char,?for single-char) via cached compiled regex patterns - String modifiers:
contains,startswith,endswith - Explicit regex (
remodifier) - CIDR subnet matching (IPv4 and IPv6)
- Boolean, numeric (gt, gte, lt, lte), null, and string comparisons
- Composite conditions: AND, OR, NOT
- List values (any element matching counts as a match)
Match results use a nested rule object per match entry:
{
"rule": { "id": "...", "title": "...", "level": "...", "tags": [...] },
"matched_conditions": [...]
}
Data flow
Client request
│
▼
RestPostLogtestAction (combined)
│ reads body into LogtestRequest, no validation
│ client.execute(LogtestAction.INSTANCE, ...)
▼
TransportLogtestAction
│ validates request
│ strips "integration" field
▼
LogtestService.executeLogtest(integrationId, space, payload)
│
├──► [if integrationId == null]
│ → executeEngineOnly(payload)
│ → returns normalization + detection: { status: "skipped" }
│
├──► client.prepareSearch("wazuh-threatintel-integrations")
│ → finds integration in given space (test or standard)
│ → extracts rule IDs from document.rules
│
├──► engineService.logtest(payload)
│ → sends to Wazuh Engine socket
│ → receives normalized event
│ → extracts "output" node as normalized event JSON
│
├──► client.prepareSearch("wazuh-threatintel-rules")
│ → fetches rule bodies by document.id + space filter
│
├──► securityAnalytics.evaluateRules(normalizedEventJson, ruleBodies)
│ → parses YAML → SigmaRule objects
│ → EventMatcher flattens event + evaluates conditions
│ → returns JSON result
│
└──► builds combined response
{ normalization: {...}, detection: {...} }
Split endpoints
In addition to the combined flow, there are two dedicated endpoints that execute normalization and detection independently:
RestPostLogtestNormalizationAction RestPostLogtestDetectionAction
│ no validation, delegates via client.execute() │ no validation, delegates via client.execute()
▼ ▼
TransportLogtestNormalizationAction TransportLogtestDetectionAction
│ validates: space │ validates: space, integration, input
│ strips integration field │
▼ ▼
LogtestService.executeNormalization(payload) LogtestService.executeDetection(id, space, input)
│ │
└──► engineService.logtest(payload) ├──► client.prepareSearch(".cti-integrations")
→ returns engine response directly │ → finds integration
├──► extractRuleIds() + fetchRuleBodies()
│ → fetches rule content from .cti-rules
└──► securityAnalytics.evaluateRules(inputJson, ruleBodies)
→ returns Security Analytics result directly
Key differences from the combined endpoint
- Normalization returns the raw Engine response (no detection wrapper). The
integrationfield is stripped if present but has no effect on behavior. - Detection accepts a pre-normalized event as the
inputJSON object. It does not call the Engine — it goes straight to integration lookup → rule fetch → Security Analytics evaluation.
Index dependencies
| Index | Usage | Query |
|---|---|---|
wazuh-threatintel-integrations | Look up integration by ID in the given space | document.id == X AND space.name == {space} |
wazuh-threatintel-rules | Fetch rule bodies by document IDs in the given space | document.id IN [...] AND space.name == {space} |
Both indices must exist and have document.id mapped as keyword for term queries to work.
Testing
Unit tests
| Test class | Covers |
|---|---|
TransportLogtestActionTests | Request validation for combined endpoint (empty body, invalid JSON, missing fields, wrong space, delegation to service) |
TransportLogtestNormalizationActionTests | Request validation for normalization endpoint (empty body, invalid JSON, missing space, invalid space, delegation, integration stripping) |
TransportLogtestDetectionActionTests | Request validation for detection endpoint (empty body, invalid JSON, missing fields, invalid space, non-object input, delegation) |
LogtestServiceTests | Orchestration logic (integration lookup, engine errors, rule fetching, Security Analytics evaluation, response structure) |
EventMatcherTests | Sigma rule evaluation (field matching, wildcards, numerics, booleans, nulls, AND/OR/NOT conditions) |
Integration tests
| Test class | Covers |
|---|---|
LogtestIT | End-to-end REST workflow against a live test cluster (request validation, integration lookup, promote + logtest, response structure) |
Integration tests extend ContentManagerRestTestCase and run against a real OpenSearch cluster. Since the Wazuh Engine is not available in the test environment, engine-dependent tests validate graceful error handling (engine error → Security Analytics skipped).
Adding new logtest features
Supporting a new validation field
- Add the field constant to
Constants.java. - Add validation logic in the relevant transport action(s):
TransportLogtestAction,TransportLogtestNormalizationAction, and/orTransportLogtestDetectionAction. The REST handlers themselves need no changes — they only read the body and dispatch. - Add unit tests in the corresponding test classes.
- Add integration test in
LogtestIT.
Supporting a new Engine response field
- Update
LogtestService.executeEngine()to extract the field. - Include it in the
normalizationmap withinbuildCombinedResponse(). - Add unit test scenarios in
LogtestServiceTests. - Update the API docs (
api.md) response fields table.
Extending Security Analytics evaluation
- Modify
EventMatcher.matchValue()to handle newSigmaTypesubclasses. - Add test cases in
EventMatcherTests. - Update the Sigma rules doc (Sigma Rules) if new detection modifiers are supported.