Logtest Architecture and Developer Guide
Component Overview
The logtest flow involves three layers:
RestPostLogtestAction → LogtestService → EngineService + SecurityAnalyticsService
RestPostLogtestNormalizationAction → ↑ ↑
RestPostLogtestDetectionAction → ↑ ↑
(REST handlers) (Orchestration) (External services)
RestPostLogtestAction (combined)
Path: rest/service/RestPostLogtestAction.java
The REST handler for POST /_plugins/_content_manager/logtest. Responsibilities:
- Validates the request has content and is valid JSON.
- Validates the required field
space. - Validates that
spaceis"test"or"standard". - 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.
The handler does not interact with indices or external services directly, all business logic is in the service.
RestPostLogtestNormalizationAction
Path: rest/service/RestPostLogtestNormalizationAction.java
The REST handler for POST /_plugins/_content_manager/logtest/normalization. Responsibilities:
- Validates the request has content and is valid JSON.
- Validates the required field
space. - Validates that
spaceis"test"or"standard". - 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. Responsibilities:
- Validates the request has content and is valid JSON.
- Validates the required fields
space,integration, andinput. - Validates that
spaceis"test"or"standard". - 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()— SAP-only: looks up integration, fetches rule IDs/bodies, evaluates viaSecurityAnalyticsService.evaluateRules(), and returns the SAP 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. - SAP evaluation — Passes the normalized event JSON and rule bodies to
SecurityAnalyticsService.evaluateRules(). - Response building — Combines engine and SAP results into a single JSON response under the keys
normalizationanddetection.
Error handling:
- If the Engine fails (HTTP error or exception), SAP 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, SAP returns
rules_evaluated: 0, rules_matched: 0with success status. - If SAP evaluation returns unparseable JSON, the SAP result is
status: "error".
SecurityAnalyticsService / EventMatcher
The SAP evaluation happens in the security-analytics:
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)
│ 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
│ 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 SAP 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 → SAP 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 |
|---|---|
RestPostLogtestActionTests | Request validation for combined endpoint (empty body, invalid JSON, missing fields, wrong space, delegation to service) |
RestPostLogtestNormalizationActionTests | Request validation for normalization endpoint (empty body, invalid JSON, missing space, invalid space, delegation, integration stripping) |
RestPostLogtestDetectionActionTests | 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, SAP 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 → SAP skipped).
Adding New Logtest Features
Supporting a new validation field
- Add the field constant to
Constants.java. - Add validation logic in the relevant handler(s):
RestPostLogtestAction,RestPostLogtestNormalizationAction, and/orRestPostLogtestDetectionAction. - 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 SAP evaluation
- Modify
EventMatcher.matchValue()to handle newSigmaTypesubclasses. - Add test cases in
EventMatcherTests. - Update the Sigma rules doc (
sigma-rules.md) if new detection modifiers are supported.