CR-2026-02-26 - Wazuh SIEM Network Integration

Comprehensive integration of all network infrastructure with Wazuh SIEM (10.50.1.134:514) for centralized security monitoring, compliance logging, and incident response.

CR ID

CR-2026-02-26-001

Status

In Progress

Priority

P1

Requester

evanusmodestus

Date

2026-02-26


Executive Summary

Metric Value

Total Devices

19 (6 network + 7 servers + 3 workstations + 3 k8s)

Integration Method

Syslog (network devices) + Wazuh Agent (servers/workstations)

Wazuh Endpoints

Indexer: 10.50.1.131:9200
Manager: 10.50.1.134:55000
Syslog: 10.50.1.134:514

Current Blocker

Archives not indexing in OpenSearch (Filebeat pipeline)


Session Log

Session 1: netapi pfsense syslog Commands

Problem: pfSense REST API v2 doesn’t expose syslog configuration.

Solution: SSH-based commands using PHP execution on pfSense.

New Commands Added
# Show current syslog configuration
netapi pfsense syslog show

# Enable remote syslog (default: Wazuh)
netapi pfsense syslog enable --server 10.50.1.134 --categories filter,system

# Enable all log categories
netapi pfsense syslog enable --categories all

# Disable remote syslog
netapi pfsense syslog disable
Commit
2c4b9ff feat(pfsense): Add syslog management commands via SSH

Session 2: pfSense Syslog Enabled

Enable Command
netapi pfsense syslog enable
Output
✓ Remote syslog enabled
  Server: 10.50.1.134
  Categories: filter,system
Verification - Data Reaching Manager
ssh k3s-master-01 "kubectl exec -n wazuh wazuh-manager-master-0 -- tail -5 /var/ossec/logs/archives/archives.log"
Output (paste your output here)
# PASTE OUTPUT HERE

Session 3: Archives Indexing Investigation

BLOCKER IDENTIFIED

Data reaches /var/ossec/logs/archives/archives.log but is NOT being indexed in OpenSearch.

Evidence: No wazuh-archives-4.x-2026.02.25 or wazuh-archives-4.x-2026.02.26 indices exist.


Infrastructure Inventory

Network Infrastructure (Syslog)

Device Type IP Status Notes

pfSense-01

Firewall

10.50.1.1

SENDING

Data reaching manager, not indexed

ISE-01

NAC

10.50.1.20

PENDING

ERS API syslog target

9800-WLC

Wireless

10.50.1.40

PENDING

IOS-XE logging config

C9300-01

Core Switch

10.50.1.11

PENDING

IOS-XE logging config

3560CX-01

Access Switch

10.50.1.10

PENDING

IOS logging config

bind-01

DNS

10.50.1.90

PENDING

BIND query logging

Servers (Wazuh Agent)

Host OS IP Status Notes

vault-01

Rocky Linux 9

10.50.1.60

PENDING

PKI + SSH CA

kvm-01

Arch Linux

10.50.1.110

PENDING

Primary hypervisor

ipa-01

Rocky Linux 9

10.50.1.100

PENDING

FreeIPA

keycloak-01

Fedora 43

10.50.1.80

PENDING

IdP

k3s-master-01

Rocky Linux 9

10.50.1.120

PENDING

k3s control plane

home-dc01

Windows 2025

10.50.1.50

PENDING

Domain Controller

nas-01

Synology DSM

10.50.1.70

PENDING

Syslog (no agent)

Workstations (Wazuh Agent)

Host OS Status Notes

modestus-razer

Arch Linux

PENDING

Primary workstation

modestus-aw

Arch Linux

PENDING

Test endpoint

modestus-p50

Arch Linux

PENDING

Laptop


Phase 1: Fix Wazuh Indexing

This must be resolved before proceeding with other devices.

Archives are being written to disk but NOT indexed in OpenSearch.

1.1 Load Credentials + Cluster Health (Chained)

dsource d000 dev/observability && netapi wazuh health | jq -r '"Cluster: \(.cluster_name) | Status: \(.status) | Nodes: \(.number_of_nodes)"'
Output (paste here)
# EXPECTED: Cluster: wazuh-cluster | Status: green | Nodes: 1

1.2 Index Inventory with awk Formatting

netapi wazuh indices --raw | jq -r '.[] | "\(.index)\t\(.docs.count)\t\(.store.size)"' | \
  awk -F'\t' 'BEGIN {printf "%-50s %10s %12s\n", "INDEX", "DOCS", "SIZE"; print "--------------------------------------------------------------------------------"}
              /archives/ {printf "%-50s %10s %12s\n", $1, $2, $3}'
Output (paste here)
INDEX                                              DOCS         SIZE
--------------------------------------------------------------------------------
# PASTE ARCHIVE INDICES HERE (if empty = PROBLEM)

1.3 Process Check with awk Pivot

ssh k3s-master-01 "kubectl exec -n wazuh wazuh-manager-master-0 -- ps aux" | \
  awk '/filebeat|logcollector|analysisd/ {printf "%-20s PID:%-6s CPU:%-5s MEM:%-5s\n", $11, $2, $3, $4}'
Output (paste here)
# EXPECTED PROCESSES:
# /var/ossec/bin/wazuh-logcollector  PID:xxx   CPU:x.x   MEM:x.x
# /var/ossec/bin/wazuh-analysisd     PID:xxx   CPU:x.x   MEM:x.x
# /usr/share/filebeat/bin/filebeat   PID:xxx   CPU:x.x   MEM:x.x  <-- CRITICAL

1.4 Filebeat Config Deep Dive

ssh k3s-master-01 "kubectl exec -n wazuh wazuh-manager-master-0 -- cat /etc/filebeat/filebeat.yml 2>/dev/null" | \
  awk '/^filebeat\.inputs:/,/^[a-z]/' | head -30
Output (paste here)
# PASTE FILEBEAT INPUTS SECTION HERE
# Check output configuration (should point to wazuh-indexer)
ssh k3s-master-01 "kubectl exec -n wazuh wazuh-manager-master-0 -- cat /etc/filebeat/filebeat.yml 2>/dev/null" | \
  awk '/^output\./,/^[a-z]/' | head -20
Output (paste here)
# PASTE OUTPUT SECTION HERE - VERIFY hosts: points to indexer

1.5 Filebeat Logs - Error Extraction

ssh k3s-master-01 "kubectl exec -n wazuh wazuh-manager-master-0 -- cat /var/log/filebeat/filebeat* 2>/dev/null" | \
  grep -iE 'error|failed|refused|timeout' | tail -20 | \
  awk '{gsub(/T/, " "); print}' | cut -c1-120
Output (paste here)
# ERRORS WILL APPEAR HERE

Common Filebeat Errors:

  • connection refused → Indexer not reachable (firewall/service down)

  • x509: certificate → TLS cert mismatch

  • 401 Unauthorized → Bad credentials in filebeat.yml

  • index_not_found → Missing index template

1.6 Manager Logs - jq Timeline

ssh k3s-master-01 "kubectl logs -n wazuh wazuh-manager-master-0 --since=1h 2>&1" | \
  grep -iE 'indexer|opensearch|elastic|filebeat|archive' | \
  awk '{print strftime("%H:%M:%S", systime()), $0}' | tail -30
Output (paste here)
# PASTE RELEVANT LOG LINES HERE

1.7 ossec.conf Archives Settings

ssh k3s-master-01 "kubectl exec -n wazuh wazuh-manager-master-0 -- cat /var/ossec/etc/ossec.conf" | \
  awk '/<global>/,/<\/global>/' | grep -E 'logall|jsonout|archives'
Output (paste here)
# EXPECTED (for archives to work):
# <logall>yes</logall>
# <logall_json>yes</logall_json>

If <logall> is no, archives.log will be EMPTY regardless of syslog reception!

Fix:

# Edit ossec.conf to enable archives
ssh k3s-master-01 "kubectl exec -n wazuh wazuh-manager-master-0 -- sed -i 's/<logall>no</<logall>yes</' /var/ossec/etc/ossec.conf"

# Restart analysisd
ssh k3s-master-01 "kubectl exec -n wazuh wazuh-manager-master-0 -- /var/ossec/bin/wazuh-control restart"

1.8 Direct Index Write Test

# Single command: write test doc + verify + cleanup
curl -sk -u admin:$WAZUH_INDEXER_PASSWORD \
  -X POST "https://10.50.1.131:9200/wazuh-archives-test/_doc" \
  -H "Content-Type: application/json" \
  -d "{\"timestamp\": \"$(date -Iseconds)\", \"test\": \"manual-write-$(hostname)\"}" | \
  jq -r 'if .result == "created" then "✓ Index write successful (id: \(._id))" else "✗ FAILED: \(.error.type)" end'
Output (paste here)
# EXPECTED: ✓ Index write successful (id: xxxxx)
# Verify + cleanup in one chain
curl -sk -u admin:$WAZUH_INDEXER_PASSWORD "https://10.50.1.131:9200/wazuh-archives-test/_count" | \
  jq -r '"Test docs: \(.count)"' && \
curl -sk -u admin:$WAZUH_INDEXER_PASSWORD -X DELETE "https://10.50.1.131:9200/wazuh-archives-test" | \
  jq -r 'if .acknowledged then "✓ Test index cleaned up" else "⚠ Cleanup failed" end'

1.9 Index Templates - Deep Inspection

curl -sk -u admin:$WAZUH_INDEXER_PASSWORD \
  "https://10.50.1.131:9200/_index_template/wazuh*" | \
  jq -r 'to_entries[] | "\(.key):\n  patterns: \(.value.index_patterns | join(", "))\n  priority: \(.value.priority)"'
Output (paste here)
# EXPECTED TEMPLATES:
# wazuh:
#   patterns: wazuh-alerts-*, wazuh-archives-*, wazuh-statistics-*
#   priority: 1

1.10 Full Diagnostic One-Liner

Run this single command for complete diagnostics:

dsource d000 dev/observability && \
echo "=== CLUSTER ===" && netapi wazuh health | jq -r '.status' && \
echo "=== ARCHIVES ===" && netapi wazuh indices --raw 2>/dev/null | jq -r '.[] | select(.index | contains("archives")) | "\(.index): \(.docs.count) docs"' && \
echo "=== PROCESSES ===" && ssh k3s-master-01 "kubectl exec -n wazuh wazuh-manager-master-0 -- pgrep -a filebeat" 2>/dev/null | head -1 && \
echo "=== ARCHIVE LOG ===" && ssh k3s-master-01 "kubectl exec -n wazuh wazuh-manager-master-0 -- wc -l /var/ossec/logs/archives/archives.log 2>/dev/null" && \
echo "=== FILEBEAT ERRORS ===" && ssh k3s-master-01 "kubectl exec -n wazuh wazuh-manager-master-0 -- grep -c -i error /var/log/filebeat/filebeat* 2>/dev/null || echo 0"
Output (paste here)
=== CLUSTER ===
# green/yellow/red

=== ARCHIVES ===
# wazuh-archives-4.x-2026.02.26: XXX docs (or empty if PROBLEM)

=== PROCESSES ===
# /usr/share/filebeat/bin/filebeat (or empty if NOT RUNNING)

=== ARCHIVE LOG ===
# XXXX /var/ossec/logs/archives/archives.log (lines in archive)

=== FILEBEAT ERRORS ===
# 0 (or error count)

Phase 2: Configure Network Devices

2.1 ISE Syslog Target

# Load network creds + check existing targets with formatted output
dsource d000 dev/network && \
netapi ise api-call ers GET '/config/externalSyslogTarget' | \
  jq -r '.SearchResult.resources[] | "\(.name)\t\(.id)"' | \
  awk -F'\t' 'BEGIN {printf "%-30s %s\n", "TARGET NAME", "UUID"} {printf "%-30s %s\n", $1, $2}'
Output (paste here)
TARGET NAME                    UUID
# EXISTING TARGETS LISTED HERE
# Create Wazuh target + extract Location header for verification
netapi ise api-call ers POST '/config/externalSyslogTarget' --data '{
  "ExternalSyslogTarget": {
    "name": "Wazuh-SIEM",
    "description": "Wazuh SIEM syslog collector - k3s deployment",
    "host": "10.50.1.134",
    "port": 514,
    "protocol": "UDP"
  }
}' 2>&1 | jq -r 'if .SearchResult then "✓ Created: \(.SearchResult.resources[0].id)" elif .message then "✗ Error: \(.message)" else . end'
Output (paste here)
# EXPECTED: ✓ Created: <uuid>
# Verify creation + get full details
netapi ise api-call ers GET '/config/externalSyslogTarget' | \
  jq -r '.SearchResult.resources[] | select(.name == "Wazuh-SIEM") | "Host: \(.host // "N/A")\nPort: \(.port // 514)\nProtocol: \(.protocol // "UDP")"'

2.2 WLC Syslog (netapi wlc config)

# Configure + save in single chain
netapi wlc config \
  "logging host 10.50.1.134" \
  "logging trap informational" \
  "logging source-interface Loopback0" \
  "logging origin-id hostname" \
  --save && echo "✓ WLC syslog configured"
Output (paste here)
# PASTE CONFIG OUTPUT HERE
# Verify logging config with awk extraction
netapi wlc run "show logging" | \
  awk '/Logging to/ || /Trap logging:/ || /host [0-9]/ {print}' | head -10
Output (paste here)
# EXPECTED:
# Logging to 10.50.1.134, Vty-VRF: 0
# Trap logging: level informational

2.3 Switch Syslog (C9300) - IOS-XE

# Configure via netapi ios config (chained)
netapi ios config C9300-01 \
  "logging host 10.50.1.134" \
  "logging trap informational" \
  "logging source-interface Vlan1" \
  "logging origin-id hostname" \
  "logging on" \
  --save && echo "✓ C9300 syslog configured"
Output (paste here)
# PASTE OUTPUT HERE
# Verify + parse with awk
netapi ios run C9300-01 "show logging" | \
  awk '/Syslog logging:/ || /Trap logging:/ || /Logging to [0-9]/' | head -5

2.4 Switch Syslog (3560CX) - Classic IOS

# Configure via SSH heredoc (classic IOS doesn't have all netapi features)
ssh 3560CX-01 << 'EOF'
configure terminal
logging host 10.50.1.134
logging trap informational
logging on
logging buffered 16384
end
write memory
EOF
Output (paste here)
# Building configuration...
# [OK]

2.5 Bulk Verification - All Syslog Sources

Run after configuring all devices:

# Multi-device syslog verification with jq + awk formatting
{
  echo "DEVICE|TYPE|SYSLOG_TARGET|STATUS"
  echo "pfSense|Firewall|$(netapi pfsense syslog show 2>/dev/null | awk '/Remote Server:/ {print $NF}')|$(netapi pfsense syslog show 2>/dev/null | awk '/Remote Logging:/ {print $NF}')"
  echo "ISE-01|NAC|$(netapi ise api-call ers GET '/config/externalSyslogTarget' 2>/dev/null | jq -r '.SearchResult.resources[] | select(.name | contains("Wazuh")) | .host' | head -1)|Configured"
  echo "WLC|Wireless|$(netapi wlc run 'show logging | inc Logging to' 2>/dev/null | awk '{print $3}' | cut -d, -f1)|$(netapi wlc run 'show logging | inc Trap logging' 2>/dev/null | awk '{print $NF}')"
} | awk -F'|' 'NR==1 {printf "%-15s %-12s %-20s %s\n", $1, $2, $3, $4} NR>1 {printf "%-15s %-12s %-20s %s\n", $1, $2, $3, $4}'
Output (paste here)
DEVICE          TYPE         SYSLOG_TARGET        STATUS
pfSense         Firewall     10.50.1.134          Enabled
ISE-01          NAC          10.50.1.134          Configured
WLC             Wireless     10.50.1.134          informational

2.6 Live Syslog Reception Test

# Watch real-time syslog reception on Wazuh manager
ssh k3s-master-01 "kubectl exec -n wazuh wazuh-manager-master-0 -- tail -f /var/ossec/logs/archives/archives.log" | \
  awk -F, '{gsub(/"/, ""); print strftime("%H:%M:%S"), $3, $4}' | head -20
# Count events per source in last 5 minutes
ssh k3s-master-01 "kubectl exec -n wazuh wazuh-manager-master-0 -- tail -1000 /var/ossec/logs/archives/archives.log" | \
  jq -r '.location // "unknown"' 2>/dev/null | sort | uniq -c | sort -rn | head -10
Output (paste here)
# EXPECTED (if working):
#   523 10.50.1.1    (pfSense)
#    47 10.50.1.20   (ISE)
#    12 10.50.1.40   (WLC)

Phase 3: Deploy Wazuh Agents

3.1 Get Agent Registration Key

# Get registration password from Wazuh manager
ssh k3s-master-01 "kubectl exec -n wazuh wazuh-manager-master-0 -- cat /var/ossec/etc/authd.pass"
Output (paste here)
# PASTE OUTPUT HERE (save to gopass)

3.2 Linux Agent Installation (Rocky/Fedora)

# On target host
curl -s https://packages.wazuh.com/key/GPG-KEY-WAZUH | gpg --no-default-keyring --keyring gnupg-ring:/usr/share/keyrings/wazuh.gpg --import && chmod 644 /usr/share/keyrings/wazuh.gpg

echo "deb [signed-by=/usr/share/keyrings/wazuh.gpg] https://packages.wazuh.com/4.x/apt/ stable main" | tee /etc/apt/sources.list.d/wazuh.list

# For RPM-based (Rocky/Fedora)
rpm --import https://packages.wazuh.com/key/GPG-KEY-WAZUH
cat > /etc/yum.repos.d/wazuh.repo << 'EOF'
[wazuh]
gpgcheck=1
gpgkey=https://packages.wazuh.com/key/GPG-KEY-WAZUH
enabled=1
name=EL-$releasever - Wazuh
baseurl=https://packages.wazuh.com/4.x/yum/
protect=1
EOF

dnf install wazuh-agent -y

3.3 Agent Configuration

# Configure agent
cat > /var/ossec/etc/ossec.conf << 'EOF'
<ossec_config>
  <client>
    <server>
      <address>{wazuh-manager-vip}</address>
      <port>1514</port>
      <protocol>tcp</protocol>
    </server>
  </client>
</ossec_config>
EOF

# Register and start
/var/ossec/bin/agent-auth -m {wazuh-manager-vip}
systemctl daemon-reload
systemctl enable wazuh-agent
systemctl start wazuh-agent

3.4 Arch Linux Agent

# Install from AUR
yay -S wazuh-agent

# Configure (same as above)
sudo vim /var/ossec/etc/ossec.conf

# Register
sudo /var/ossec/bin/agent-auth -m {wazuh-manager-vip}
sudo systemctl enable --now wazuh-agent

3.5 Windows Agent

# Download installer
Invoke-WebRequest -Uri https://packages.wazuh.com/4.x/windows/wazuh-agent-4.14.3-1.msi -OutFile wazuh-agent.msi

# Install with manager IP
msiexec.exe /i wazuh-agent.msi /q WAZUH_MANAGER="{wazuh-manager-vip}" WAZUH_REGISTRATION_SERVER="{wazuh-manager-vip}"

# Start service
NET START WazuhSvc

3.6 Verify Agent Registration

# Load creds + get agents with jq formatting
dsource d000 dev/observability && \
netapi wazuh agents --raw | \
  jq -r '.[] | "\(.id)\t\(.name)\t\(.ip)\t\(.status)\t\(.os.name // "N/A")"' | \
  awk -F'\t' 'BEGIN {printf "%-5s %-25s %-15s %-10s %s\n", "ID", "NAME", "IP", "STATUS", "OS"}
              {printf "%-5s %-25s %-15s %-10s %s\n", $1, $2, $3, $4, $5}'
Output (paste here)
ID    NAME                      IP              STATUS     OS
000   wazuh-manager-master-0    127.0.0.1       Active     Rocky Linux 9
001   vault-01                  10.50.1.60      Active     Rocky Linux 9
002   kvm-01                    10.50.1.99      Active     Arch Linux
# ... more agents
# Quick health check: count by status
dsource d000 dev/observability && \
netapi wazuh agents --raw | jq -r '.[].status' | sort | uniq -c | \
  awk '{printf "%s: %d agents\n", $2, $1}'
Output (paste here)
# Active: 8 agents
# Disconnected: 0 agents

Phase 4: Create Dashboards

4.1 pfSense Firewall Analysis

# Top blocked source IPs (last 24h) - OpenSearch aggregation + jq pivot
dsource d000 dev/observability && \
curl -sk -u admin:$WAZUH_INDEXER_PASSWORD \
  -X POST "https://10.50.1.131:9200/wazuh-archives-*/_search" \
  -H "Content-Type: application/json" \
  -d '{
    "size": 0,
    "query": {"bool": {"must": [
      {"match": {"full_log": "filterlog"}},
      {"match": {"full_log": "block"}},
      {"range": {"timestamp": {"gte": "now-24h"}}}
    ]}},
    "aggs": {"top_sources": {"terms": {"field": "data.srcip", "size": 10}}}
  }' | jq -r '.aggregations.top_sources.buckets[] | "\(.key)\t\(.doc_count)"' | \
  awk -F'\t' 'BEGIN {printf "%-20s %10s\n", "SOURCE IP", "BLOCKS"} {printf "%-20s %10d\n", $1, $2}'
Output (paste here)
SOURCE IP                 BLOCKS
192.168.1.100                523
10.20.30.40                  127
# ... top blocked IPs
# Top blocked destination ports
curl -sk -u admin:$WAZUH_INDEXER_PASSWORD \
  -X POST "https://10.50.1.131:9200/wazuh-archives-*/_search" \
  -H "Content-Type: application/json" \
  -d '{
    "size": 0,
    "query": {"bool": {"must": [{"match": {"full_log": "block"}}]}},
    "aggs": {"top_ports": {"terms": {"field": "data.dstport", "size": 10}}}
  }' | jq -r '.aggregations.top_ports.buckets[] | "\(.key)\t\(.doc_count)"' | \
  awk -F'\t' 'BEGIN {printf "%-10s %10s %s\n", "PORT", "COUNT", "SERVICE"}
              {svc="unknown"; if($1==22) svc="SSH"; if($1==23) svc="Telnet"; if($1==80) svc="HTTP"; if($1==443) svc="HTTPS"; if($1==445) svc="SMB"; if($1==3389) svc="RDP"; printf "%-10s %10d %s\n", $1, $2, svc}'

4.2 ISE Authentication Analytics

# Auth success/failure ratio from archives
curl -sk -u admin:$WAZUH_INDEXER_PASSWORD \
  -X POST "https://10.50.1.131:9200/wazuh-archives-*/_search" \
  -H "Content-Type: application/json" \
  -d '{
    "size": 0,
    "query": {"match": {"location": "10.50.1.20"}},
    "aggs": {
      "auth_results": {"terms": {"field": "data.outcome", "size": 5}}
    }
  }' | jq -r '.aggregations.auth_results.buckets[] | "\(.key): \(.doc_count)"'

4.3 Export Dashboards for Backup

# Export all dashboards + parse result
netapi wazuh dashboard-export -o /tmp/wazuh-dashboards-$(date +%F).ndjson && \
wc -l /tmp/wazuh-dashboards-*.ndjson | awk '{print "Exported " $1 " objects to " $2}'

Advanced Troubleshooting

OpenSearch Query DSL - Deep Queries

# Multi-field search with jq transformation pipeline
curl -sk -u admin:$WAZUH_INDEXER_PASSWORD \
  -X POST "https://10.50.1.131:9200/wazuh-archives-*/_search" \
  -H "Content-Type: application/json" \
  -d '{
    "query": {"bool": {"must": [
      {"match": {"location": "10.50.1.1"}},
      {"range": {"timestamp": {"gte": "now-1h"}}}
    ]}},
    "size": 50,
    "sort": [{"timestamp": "desc"}],
    "_source": ["timestamp", "location", "full_log"]
  }' | jq -r '.hits.hits[]._source | "\(.timestamp | split("T")[1] | split(".")[0])\t\(.location)\t\(.full_log | .[0:80])"' | \
  awk -F'\t' 'BEGIN {printf "%-12s %-15s %s\n", "TIME", "SOURCE", "LOG (truncated)"} {printf "%-12s %-15s %s\n", $1, $2, $3}' | head -20
Output (paste here)
TIME         SOURCE          LOG (truncated)
12:34:56     10.50.1.1       Feb 26 12:34:56 filterlog[50531]: 65,,,12004,ixl0,match,b...
# Event count per hour (time histogram) - great for dashboards
curl -sk -u admin:$WAZUH_INDEXER_PASSWORD \
  -X POST "https://10.50.1.131:9200/wazuh-archives-*/_search" \
  -H "Content-Type: application/json" \
  -d '{
    "size": 0,
    "query": {"range": {"timestamp": {"gte": "now-24h"}}},
    "aggs": {"events_per_hour": {"date_histogram": {"field": "timestamp", "calendar_interval": "hour"}}}
  }' | jq -r '.aggregations.events_per_hour.buckets[] | "\(.key_as_string | split("T")[1] | split(":")[0]):00\t\(.doc_count)"' | \
  awk -F'\t' 'BEGIN {printf "%-8s %10s %s\n", "HOUR", "EVENTS", "HISTOGRAM"} {bar=""; for(i=0;i<$2/100;i++) bar=bar"█"; printf "%-8s %10d %s\n", $1, $2, bar}'
Output (paste here)
HOUR       EVENTS HISTOGRAM
00:00         523 █████
01:00         412 ████
02:00         234 ██
# ... hourly distribution

Wazuh API - Token Chain Pattern

# Get token + use immediately (single chain, no temp vars)
curl -sk -u wazuh-wui:$WAZUH_API_PASSWORD \
  -X POST "https://10.50.1.134:55000/security/user/authenticate" | \
  jq -r '.data.token' | \
  xargs -I{} curl -sk -H "Authorization: Bearer {}" \
    "https://10.50.1.134:55000/agents?select=id,name,status,ip" | \
  jq -r '.data.affected_items[] | "\(.id)\t\(.name)\t\(.status)\t\(.ip)"' | \
  awk -F'\t' 'BEGIN {printf "%-5s %-25s %-12s %s\n", "ID", "NAME", "STATUS", "IP"}
              {printf "%-5s %-25s %-12s %s\n", $1, $2, $3, $4}'
# Get agent summary stats with jq aggregation
curl -sk -u wazuh-wui:$WAZUH_API_PASSWORD \
  -X POST "https://10.50.1.134:55000/security/user/authenticate" | \
  jq -r '.data.token' | \
  xargs -I{} curl -sk -H "Authorization: Bearer {}" \
    "https://10.50.1.134:55000/agents/summary/status" | \
  jq -r '.data | "Total: \(.total) | Active: \(.active) | Disconnected: \(.disconnected) | Pending: \(.pending)"'

Restart + Monitor Pattern

# Restart + watch rollout in single chain
ssh k3s-master-01 "kubectl rollout restart deployment/wazuh-manager -n wazuh && \
  kubectl rollout status deployment/wazuh-manager -n wazuh --timeout=120s && \
  echo '✓ Manager restarted successfully' || echo '✗ Rollout failed'"
# Restart all Wazuh components + verify
ssh k3s-master-01 "for comp in wazuh-manager wazuh-dashboard; do \
  kubectl rollout restart deployment/\$comp -n wazuh 2>/dev/null; \
done && \
kubectl rollout restart statefulset/wazuh-indexer -n wazuh && \
sleep 30 && \
kubectl get pods -n wazuh -o custom-columns='NAME:.metadata.name,STATUS:.status.phase,RESTARTS:.status.containerStatuses[0].restartCount'"

Archive Log Analysis

# Event rate calculation (events per minute)
ssh k3s-master-01 "kubectl exec -n wazuh wazuh-manager-master-0 -- sh -c '\
  START=\$(wc -l < /var/ossec/logs/archives/archives.log); \
  sleep 60; \
  END=\$(wc -l < /var/ossec/logs/archives/archives.log); \
  echo \"Rate: \$((END-START)) events/minute\"'"
# Top event types in last 1000 lines (awk frequency analysis)
ssh k3s-master-01 "kubectl exec -n wazuh wazuh-manager-master-0 -- tail -1000 /var/ossec/logs/archives/archives.log" | \
  jq -r '.rule.description // .decoder.name // "unknown"' 2>/dev/null | \
  sort | uniq -c | sort -rn | head -10 | \
  awk '{count=$1; $1=""; printf "%6d  %s\n", count, $0}'
Output (paste here)
#   523  pfSense: Firewall block
#   127  File integrity monitoring
#    45  SSH session opened

Validation Checklist

# Check Status

1

OpenSearch cluster health = green

[ ]

2

Archives index exists for today’s date

[ ]

3

pfSense logs visible in archives

[ ]

4

All network devices sending syslog

[ ]

5

All servers have Wazuh agent installed

[ ]

6

All agents showing "Active" status

[ ]

7

Dashboards created and functional

[ ]

8

Alert rules triggering correctly

[ ]


Rollback

Disable pfSense Syslog

netapi pfsense syslog disable

Remove Wazuh Agent

# Linux
sudo systemctl stop wazuh-agent
sudo dnf remove wazuh-agent  # or apt remove

# Windows
NET STOP WazuhSvc
msiexec.exe /x wazuh-agent.msi /q

Quick Reference - jq + awk Patterns

The jq + awk combination is extremely powerful for security operations.

jq handles JSON parsing, awk handles tabular formatting. Chain them for production-grade output.

Pattern 1: API → jq → awk Table

# Generic pattern: API call | jq extract | awk format
<api_call> | jq -r '.items[] | "\(.field1)\t\(.field2)\t\(.field3)"' | \
  awk -F'\t' 'BEGIN {printf "%-20s %-15s %s\n", "COL1", "COL2", "COL3"}
              {printf "%-20s %-15s %s\n", $1, $2, $3}'

Pattern 2: Conditional jq Output

# Success/failure with emoji indicators
<command> | jq -r 'if .status == "success" then "✓ \(.message)" else "✗ \(.error)" end'

Pattern 3: awk Histogram

# Turn counts into visual bars
<data> | awk '{bar=""; for(i=0;i<$1/10;i++) bar=bar"█"; printf "%6d %s %s\n", $1, bar, $2}'

Pattern 4: jq Aggregation

# Group + count with jq
<json> | jq -r 'group_by(.field) | .[] | {key: .[0].field, count: length} | "\(.key): \(.count)"'

Pattern 5: xargs Chain

# Use result of first command in second (no temp vars)
<get_id_command> | jq -r '.id' | xargs -I{} <second_command_using_{}>

Pattern 6: Multi-Source Data

# Combine multiple sources with process substitution
paste <(cmd1 | jq -r '.field1') <(cmd2 | jq -r '.field2') | \
  awk '{printf "%-20s %s\n", $1, $2}'

Pattern 7: awk Service Lookup

# Inline port→service mapping
awk '{svc="unknown"; if($1==22)svc="SSH"; if($1==443)svc="HTTPS"; if($1==3389)svc="RDP"; print $0, svc}'

Pattern 8: jq Default Values

# Handle null/missing fields gracefully
jq -r '.field // "N/A"'           # Default string
jq -r '.count // 0'               # Default number
jq -r '.items // empty'           # Skip if missing

Pattern 9: Date/Time Extraction

# Extract time components from ISO timestamps
jq -r '.timestamp | split("T")[1] | split(".")[0]'  # HH:MM:SS
jq -r '.timestamp | split("T")[0]'                   # YYYY-MM-DD

Pattern 10: Error-Safe Chaining

# Continue on error, report at end
cmd1 2>/dev/null && echo "✓ Step 1" || echo "✗ Step 1 failed"
cmd2 2>/dev/null && echo "✓ Step 2" || echo "✗ Step 2 failed"

One-Liner Cheatsheet

Operation Command

Load Wazuh creds

dsource d000 dev/observability

Cluster health (green/yellow/red)

netapi wazuh health | jq -r '.status'

Archive index count

netapi wazuh indices --raw | jq '[.[] | select(.index | contains("archives"))] | length'

Events per source

tail -1000 archives.log | jq -r '.location' | sort | uniq -c | sort -rn

Agent status summary

netapi wazuh agents --raw | jq -r '.[].status' | sort | uniq -c

Top 5 rule IDs

netapi wazuh rules --raw | jq -r '.[0:5][] | "\(.rule_id): \(.count)"'

Test OpenSearch write

curl -sk -u admin:$P -X POST host:9200/test/_doc -d '{}' | jq .result

Watch syslog live

ssh k3s-master-01 "kubectl exec -n wazuh wazuh-manager-master-0 — tail -f archives.log"

pfSense syslog status

netapi pfsense syslog show | awk '/Remote Logging:/ {print $NF}'

Restart Wazuh pods

ssh k3s-master-01 "kubectl rollout restart deploy/wazuh-manager -n wazuh"