The Problem: Xcitium Client Security places an exclusive lock on its log file (events.cef). Standard log collectors cannot read it. Attempting to read it often results in "Access Denied" or empty data.
The Solution: We utilize a custom PowerShell script that uses .NET FileStream with "Read/Write" share privileges to bypass the lock, print the logs to stdout, and allow Wazuh to ingest them directly from memory.
The Architecture
Instead of fighting file locks with intermediate files, we use a "Direct Output" method:
- Agent Script: A custom PowerShell script uses .NET FileStreams to force-open the locked Xcitium file in "Read/Write" mode.
- State Tracking: The script maintains an "offset file" to remember its last read position, ensuring logs are never duplicated or missed.
- Stdout Execution: Instead of writing to a disk file, the script prints new log lines directly to Standard Output (
stdout). - Wazuh Command: The Wazuh Agent executes this script every 60 seconds and ingests the output directly into the analysis engine.
Step 1: Endpoint Hardening
Before deploying any scripts, we must create a secure enclave. Logs often contain sensitive data, and we cannot allow standard users to tamper with the ingestion script.
Action: Open PowerShell as Administrator and run the following commands exactly as shown(Do this for your script and log file directory):
# 1. Create the secure directory
New-Item -Path "C:\scripts" -ItemType Directory -Force
# 2. Break inheritance (Stop permissions flowing down from C:\)
icacls "C:\scripts" /inheritance:d
# 3. Remove "Users" and "Authenticated Users"
# This ensures only SYSTEM and Administrators can see inside
icacls "C:\scripts" /remove "Users"
icacls "C:\scripts" /remove "Authenticated Users"
# 4. Verify Access
icacls "C:\scripts"
Step 2: The Ingestion Script
Create a new file named PrintXcitiumLogs.ps1 inside the C:\scripts\ folder you just created. Paste the following code. This is the full, working version.
File Path: C:\scripts\PrintXcitiumLogs.ps1
# --- CONFIGURATION ---
$SourceFile = "C:\Logs\Xcitium\events.cef"
$OffsetFile = "C:\Logs\Xcitium\offset.txt"
$DebugLog = "C:\scripts\debug_trace.txt"
function Log-Debug {
param($Message)
$TimeStamp = Get-Date -Format "HH:mm:ss"
Add-Content -Path $DebugLog -Value "$TimeStamp - $Message"
}
# Clear debug log on each run
Clear-Content $DebugLog -ErrorAction SilentlyContinue
Log-Debug "SCRIPT STARTED."
try {
# 1. Check if Source Exists
if (-not (Test-Path $SourceFile)) {
Log-Debug "CRITICAL ERROR: Source file not found at $SourceFile"
exit 1
}
# 2. Open Locked Source File (SIMPLIFIED SYNTAX)
Log-Debug "Attempting to open source file..."
# We use simple strings ("Open", "Read", "ReadWrite") which PowerShell auto-converts
$fileStream = New-Object System.IO.FileStream $SourceFile, "Open", "Read", "ReadWrite"
$reader = New-Object System.IO.StreamReader $fileStream
Log-Debug "Source file opened successfully."
# 3. Get Previous Offset
$StartPos = 0
if (Test-Path $OffsetFile) {
$SavedOffset = Get-Content $OffsetFile
if ($SavedOffset -match "^\d+$") {
$StartPos = [int64]$SavedOffset
Log-Debug "Found previous offset: $StartPos"
}
}
# 4. Check File Size & Rotation
$CurrentLength = $fileStream.Length
Log-Debug "Current File Size: $CurrentLength"
if ($StartPos -gt $CurrentLength) {
Log-Debug "File rotated (shrank). Resetting offset to 0."
$StartPos = 0
}
if ($StartPos -eq $CurrentLength) {
Log-Debug "No new data (Offset matches File Size). Exiting."
$reader.Dispose(); $fileStream.Dispose()
exit
}
# 5. Seek and Read
Log-Debug "Reading from position $StartPos to $CurrentLength"
$fileStream.Seek($StartPos, [System.IO.SeekOrigin]::Begin) | Out-Null
$NewContent = $reader.ReadToEnd()
# 6. OUTPUT PAYLOAD
if (-not [string]::IsNullOrWhiteSpace($NewContent)) {
Log-Debug "New content found! Length: $($NewContent.Length)"
# --- THE PAYLOAD ---
Write-Output $NewContent
# -------------------
# 7. Save New Offset
$NewOffset = $fileStream.Position
$NewOffset | Set-Content $OffsetFile
Log-Debug "New offset saved: $NewOffset"
} else {
Log-Debug "Content was empty."
}
$reader.Dispose()
$fileStream.Dispose()
Log-Debug "SCRIPT FINISHED SUCCESSFULLY."
} catch {
Log-Debug "FATAL CRASH: $($_.Exception.Message)"
Write-Output "ERROR: Script Crashed - $($_.Exception.Message)"
}
Step 3: Agent Configuration
Now we tell the Wazuh Agent to run this script every 60 seconds.
File Path: C:\Program Files (x86)\ossec-agent\ossec.conf
Add this block inside the <ossec_config> section:
<localfile>
<log_format>command</log_format>
<command>powershell.exe -ExecutionPolicy Bypass -File "C:\scripts\PrintXcitiumLogs.ps1"</command>
<frequency>60</frequency>
<alias>xcitium-logs</alias>
</localfile>
Note: After saving, restart the agent: Restart-Service WazuhSvc
Step 4: Server Decoders & Rules
Perform these steps on your Wazuh Manager (Master Node). If you use a cluster, remember to copy these files to all Worker nodes manually.
A. The Decoder
File Path: /var/ossec/etc/decoders/local_decoder.xml
We use a child decoder of ossec to handle the command output wrapper.
<decoder name="xcitium-fields">
<parent>ossec</parent>
<prematch>output: 'xcitium-logs':</prematch>
<!-- CRITICAL: Ensures the rule engine identifies the decoder name -->
<use_own_name>true</use_own_name>
<regex type="pcre2">CEF:0\|([^\|]*)\|([^\|]*)\|([^\|]*)\|([^\|]*)\|([^\|]*)\|([^\|]*)\|(.*)</regex>
<order>xcitium.vendor, xcitium.product, xcitium.version, xcitium.id, xcitium.name, xcitium.severity, xcitium.extension</order>
</decoder>
B. The Rules
File Path: /var/ossec/etc/rules/local_rules.xml
<group name="xcitium,">
<!-- Level 3: Catch-all for Xcitium Events -->
<rule id="100010" level="3">
<decoded_as>xcitium-fields</decoded_as>
<description>Xcitium: $(xcitium.name)</description>
</rule>
<!-- Level 10: Critical Malware & Containment Alert -->
<rule id="100011" level="10">
<if_sid>100010</if_sid>
<!-- Matches keywords: malware, virus, containment, etc. -->
<field name="xcitium.name" type="pcre2">(?i)malware|virus|trojan|ransomware|spyware|blocked|containment</field>
<description>Xcitium Critical: Malware or Threat Detected - $(xcitium.name)</description>
<mitre>
<id>T1204</id>
</mitre>
</rule>
</group>
Note: Restart the manager to apply: systemctl restart wazuh-manager
Step 5: Pro-Tips & Troubleshooting
-
Enable Logall (See Everything):
If alerts aren't firing, you need to see the raw logs Wazuh is receiving. In your Manager's
ossec.conf, set:<logall>yes</logall> <logall_json>yes</logall_json> -
Create the Archives Index Pattern:
By default, the Dashboard only shows alerts. To see the raw logs enabled by
logall, you must manually create an Index Pattern:- Open the Wazuh Dashboard menu.
- Go to Stack Management > Index Patterns.
- Click Create index pattern.
- Name:
wazuh-archives-*(or*:wazuh-archives-*for Cross-Cluster). - Time Field: Select
@timestamp. - Go to Discover and switch the pattern to
wazuh-archives-*to search raw Xcitium logs.
-
Cluster Synchronization:
If you have Worker nodes, you must manually copy the
local_decoder.xmlandlocal_rules.xmlfiles to them. They do not sync automatically. -
Resetting the Script:
If the script seems stuck, delete the offset file at
C:\wazuh_scripts\xcitium_offset.txt. This forces the script to re-read the entire log file from the beginning. -
Enable Log to File in Xcitium:
Please ensure log to file is enabled in Xcitium for this walk through to work