This workflow corresponds to n8n.io template #18029 — we link there as the canonical source.
This workflow follows the Datatable → HTTP Request recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"id": "z2nim3P7mB6rFHaI",
"meta": {
"builderVariant": "mcp",
"aiBuilderAssisted": true
},
"name": "Assess Wazuh platform health and email recommendations",
"tags": [],
"nodes": [
{
"id": "0aad61cd-ed2f-4fa7-b14c-6bf58c661fcc",
"name": "Run health check manually",
"type": "n8n-nodes-base.manualTrigger",
"position": [
-64,
96
],
"parameters": {},
"typeVersion": 1
},
{
"id": "a3ab8f1b-df56-42e4-9c38-8371d69a470f",
"name": "Configure health check",
"type": "n8n-nodes-base.set",
"position": [
224,
96
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "assessment-label",
"name": "assessmentLabel",
"type": "string",
"value": "Manual Wazuh health check"
},
{
"id": "deployment-type",
"name": "deploymentType",
"type": "string",
"value": "on-premises"
},
{
"id": "organization-name",
"name": "organizationName",
"type": "string",
"value": "Your Organization"
},
{
"id": "manager-url",
"name": "wazuhManagerUrl",
"type": "string",
"value": "https://wazuh-manager.example.internal:55000"
},
{
"id": "indexer-url",
"name": "wazuhIndexerUrl",
"type": "string",
"value": "https://wazuh-indexer.example.internal:9200"
},
{
"id": "recipient",
"name": "reportRecipient",
"type": "string",
"value": "user@example.com"
},
{
"id": "sender-name",
"name": "senderName",
"type": "string",
"value": "Security Automation"
},
{
"id": "sender-email",
"name": "senderEmail",
"type": "string",
"value": "user@example.com"
},
{
"id": "expected-agents",
"name": "expectedAgentCount",
"type": "number",
"value": 0
},
{
"id": "expected-peak-eps",
"name": "expectedPeakEps",
"type": "number",
"value": 0
},
{
"id": "disk-warning",
"name": "diskWarningPercent",
"type": "number",
"value": 75
},
{
"id": "disk-critical",
"name": "diskCriticalPercent",
"type": "number",
"value": 85
},
{
"id": "cpu-warning",
"name": "cpuWarningPercent",
"type": "number",
"value": 80
},
{
"id": "cpu-critical",
"name": "cpuCriticalPercent",
"type": "number",
"value": 90
},
{
"id": "memory-warning",
"name": "memoryWarningPercent",
"type": "number",
"value": 85
},
{
"id": "memory-critical",
"name": "memoryCriticalPercent",
"type": "number",
"value": 95
},
{
"id": "heap-warning",
"name": "heapWarningPercent",
"type": "number",
"value": 75
},
{
"id": "heap-critical",
"name": "heapCriticalPercent",
"type": "number",
"value": 85
},
{
"id": "disconnected-warning",
"name": "disconnectedWarningPercent",
"type": "number",
"value": 10
},
{
"id": "disconnected-critical",
"name": "disconnectedCriticalPercent",
"type": "number",
"value": 50
},
{
"id": "dropped-warning",
"name": "droppedWarningRatePercent",
"type": "number",
"value": 0.1
},
{
"id": "dropped-critical",
"name": "droppedCriticalRatePercent",
"type": "number",
"value": 1
}
]
}
},
"typeVersion": 3.4
},
{
"id": "3cac4994-000f-499c-bc15-b439cf6c3b78",
"name": "Authenticate to Wazuh API",
"type": "n8n-nodes-base.httpRequest",
"onError": "continueRegularOutput",
"maxTries": 3,
"position": [
336,
320
],
"parameters": {
"url": "={{ $(\"Configure health check\").first().json.wazuhManagerUrl.replace(/\\/$/, \"\") + \"/security/user/authenticate?raw=true\" }}",
"method": "POST",
"options": {
"timeout": 15000,
"response": {
"response": {
"responseFormat": "text",
"outputPropertyName": "token"
}
},
"allowUnauthorizedCerts": true
},
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth"
},
"executeOnce": true,
"retryOnFail": true,
"typeVersion": 4.4,
"waitBetweenTries": 5000
},
{
"id": "5b632e23-3cd0-41ea-8133-acfff53cc9c0",
"name": "Get Wazuh manager information",
"type": "n8n-nodes-base.httpRequest",
"onError": "continueRegularOutput",
"maxTries": 3,
"position": [
448,
96
],
"parameters": {
"url": "={{ $(\"Configure health check\").first().json.wazuhManagerUrl.replace(/\\/$/, \"\") + \"/manager/info\" }}",
"options": {
"timeout": 20000,
"response": {
"response": {
"responseFormat": "json"
}
},
"allowUnauthorizedCerts": true
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "=Bearer {{ $(\"Authenticate to Wazuh API\").first().json.token }}"
}
]
}
},
"executeOnce": true,
"retryOnFail": true,
"typeVersion": 4.4,
"waitBetweenTries": 5000
},
{
"id": "4c9cd8ba-6a23-4673-8e6c-9946653e8343",
"name": "Check Wazuh manager processes",
"type": "n8n-nodes-base.httpRequest",
"onError": "continueRegularOutput",
"maxTries": 3,
"position": [
560,
320
],
"parameters": {
"url": "={{ $(\"Configure health check\").first().json.wazuhManagerUrl.replace(/\\/$/, \"\") + \"/manager/status\" }}",
"options": {
"timeout": 20000,
"response": {
"response": {
"responseFormat": "json"
}
},
"allowUnauthorizedCerts": true
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "=Bearer {{ $(\"Authenticate to Wazuh API\").first().json.token }}"
}
]
}
},
"executeOnce": true,
"retryOnFail": true,
"typeVersion": 4.4,
"waitBetweenTries": 5000
},
{
"id": "cd5d85b3-4039-47e6-ab88-0620da1672fa",
"name": "Collect Wazuh manager statistics",
"type": "n8n-nodes-base.httpRequest",
"onError": "continueRegularOutput",
"maxTries": 3,
"position": [
672,
96
],
"parameters": {
"url": "={{ $(\"Configure health check\").first().json.wazuhManagerUrl.replace(/\\/$/, \"\") + \"/manager/stats\" }}",
"options": {
"timeout": 20000,
"response": {
"response": {
"responseFormat": "json"
}
},
"allowUnauthorizedCerts": true
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "=Bearer {{ $(\"Authenticate to Wazuh API\").first().json.token }}"
}
]
}
},
"executeOnce": true,
"retryOnFail": true,
"typeVersion": 4.4,
"waitBetweenTries": 5000
},
{
"id": "8e2bbc38-97a1-4fd1-bae3-26ea0a964fb9",
"name": "Collect Wazuh daemon statistics",
"type": "n8n-nodes-base.httpRequest",
"onError": "continueRegularOutput",
"maxTries": 3,
"position": [
784,
320
],
"parameters": {
"url": "={{ $(\"Configure health check\").first().json.wazuhManagerUrl.replace(/\\/$/, \"\") + \"/manager/daemons/stats\" }}",
"options": {
"timeout": 20000,
"response": {
"response": {
"responseFormat": "json"
}
},
"allowUnauthorizedCerts": true
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "=Bearer {{ $(\"Authenticate to Wazuh API\").first().json.token }}"
}
]
}
},
"executeOnce": true,
"retryOnFail": true,
"typeVersion": 4.4,
"waitBetweenTries": 5000
},
{
"id": "18a98c40-9358-422e-b219-49b24514e6e8",
"name": "Summarize Wazuh agent status",
"type": "n8n-nodes-base.httpRequest",
"onError": "continueRegularOutput",
"maxTries": 3,
"position": [
896,
96
],
"parameters": {
"url": "={{ $(\"Configure health check\").first().json.wazuhManagerUrl.replace(/\\/$/, \"\") + \"/agents/summary/status\" }}",
"options": {
"timeout": 20000,
"response": {
"response": {
"responseFormat": "json"
}
},
"allowUnauthorizedCerts": true
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "=Bearer {{ $(\"Authenticate to Wazuh API\").first().json.token }}"
}
]
}
},
"executeOnce": true,
"retryOnFail": true,
"typeVersion": 4.4,
"waitBetweenTries": 5000
},
{
"id": "c377a864-d51d-4aec-a12e-59123fd0c7cd",
"name": "Check Wazuh server cluster",
"type": "n8n-nodes-base.httpRequest",
"onError": "continueRegularOutput",
"maxTries": 3,
"position": [
1008,
320
],
"parameters": {
"url": "={{ $(\"Configure health check\").first().json.wazuhManagerUrl.replace(/\\/$/, \"\") + \"/cluster/healthcheck\" }}",
"options": {
"timeout": 20000,
"response": {
"response": {
"responseFormat": "json"
}
},
"allowUnauthorizedCerts": true
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "=Bearer {{ $(\"Authenticate to Wazuh API\").first().json.token }}"
}
]
}
},
"executeOnce": true,
"retryOnFail": true,
"typeVersion": 4.4,
"waitBetweenTries": 5000
},
{
"id": "8011feb5-80c9-411b-9833-a75044959bc1",
"name": "Check Wazuh indexer cluster health",
"type": "n8n-nodes-base.httpRequest",
"onError": "continueRegularOutput",
"maxTries": 3,
"position": [
1120,
96
],
"parameters": {
"url": "={{ $(\"Configure health check\").first().json.wazuhIndexerUrl.replace(/\\/$/, \"\") + \"/_cluster/health\" }}",
"options": {
"timeout": 20000,
"response": {
"response": {
"responseFormat": "json"
}
},
"allowUnauthorizedCerts": true
},
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth"
},
"executeOnce": true,
"retryOnFail": true,
"typeVersion": 4.4,
"waitBetweenTries": 5000
},
{
"id": "6ef167c2-4812-4bff-9636-5ed2c19e8056",
"name": "Collect indexer storage allocation",
"type": "n8n-nodes-base.httpRequest",
"onError": "continueRegularOutput",
"maxTries": 3,
"position": [
1232,
320
],
"parameters": {
"url": "={{ $(\"Configure health check\").first().json.wazuhIndexerUrl.replace(/\\/$/, \"\") + \"/_cat/allocation?format=json&bytes=gb\" }}",
"options": {
"timeout": 20000,
"response": {
"response": {
"responseFormat": "json"
}
},
"allowUnauthorizedCerts": true
},
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth"
},
"executeOnce": true,
"retryOnFail": true,
"typeVersion": 4.4,
"waitBetweenTries": 5000
},
{
"id": "3cd80c56-1d4c-4e79-9590-64c05e884942",
"name": "Collect indexer node pressure",
"type": "n8n-nodes-base.httpRequest",
"onError": "continueRegularOutput",
"maxTries": 3,
"position": [
1344,
96
],
"parameters": {
"url": "={{ $(\"Configure health check\").first().json.wazuhIndexerUrl.replace(/\\/$/, \"\") + \"/_cat/nodes?format=json&h=name,ip,node.role,cpu,load_1m,heap.percent,ram.percent\" }}",
"options": {
"timeout": 20000,
"response": {
"response": {
"responseFormat": "json"
}
},
"allowUnauthorizedCerts": true
},
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth"
},
"executeOnce": true,
"retryOnFail": true,
"typeVersion": 4.4,
"waitBetweenTries": 5000
},
{
"id": "09b5dec0-58b4-4080-a439-c0ff40b78bec",
"name": "Collect host and runtime metrics",
"type": "n8n-nodes-base.ssh",
"onError": "continueRegularOutput",
"position": [
1520,
96
],
"parameters": {
"command": "printf \"HOSTNAME=%s\\n\" \"$(hostname)\"; printf \"CPU_CORES=%s\\n\" \"$(nproc)\"; awk '{printf \"LOAD_1M=%s\\n\", $1}' /proc/loadavg; vmstat 1 2 | tail -1 | awk '{printf \"CPU_USED_PERCENT=%s\\n\", 100-$15}'; free -b | awk '/^Mem:/ {printf \"MEM_TOTAL=%s\\nMEM_USED=%s\\nMEM_AVAILABLE=%s\\n\", $2,$3,$7} /^Swap:/ {printf \"SWAP_TOTAL=%s\\nSWAP_USED=%s\\n\", $2,$3}'; df -PB1 / | awk 'NR==2 {gsub(/%/,\"\",$5); printf \"ROOT_TOTAL=%s\\nROOT_USED=%s\\nROOT_AVAILABLE=%s\\nROOT_USED_PERCENT=%s\\n\",$2,$3,$4,$5}'; df -Pi / | awk 'NR==2 {gsub(/%/,\"\",$5); printf \"ROOT_INODE_PERCENT=%s\\n\",$5}'; awk '{printf \"UPTIME_SECONDS=%.0f\\n\", $1}' /proc/uptime; if command -v docker >/dev/null 2>&1; then printf \"RUNTIME_TYPE=docker\\n\"; docker ps -q | while read id; do docker inspect \"$id\" | python3 -c 'import json,sys; x=json.load(sys.stdin)[0]; print(\"CONTAINER=%s|%s|%s|%s\"%(x[\"Name\"].lstrip(\"/\"),x[\"State\"][\"Status\"],x[\"State\"].get(\"Health\",{}).get(\"Status\",\"none\"),x.get(\"RestartCount\",0)))'; done; else printf \"RUNTIME_TYPE=none\\n\"; fi",
"authentication": "privateKey"
},
"executeOnce": true,
"typeVersion": 1
},
{
"id": "1c6cdc20-4734-4647-91ab-a156457129ff",
"name": "Load previous health check",
"type": "n8n-nodes-base.dataTable",
"position": [
1936,
304
],
"parameters": {
"limit": 1,
"orderBy": true,
"operation": "get",
"dataTableId": {
"__rl": true,
"mode": "name",
"value": "Wazuh Health Runs",
"cachedResultName": "Wazuh Health Runs"
}
},
"executeOnce": true,
"typeVersion": 1.1,
"alwaysOutputData": true
},
{
"id": "2553ee62-b0b2-4327-8241-e5e892e8ad45",
"name": "Evaluate Wazuh health and recommendations",
"type": "n8n-nodes-base.code",
"position": [
2160,
304
],
"parameters": {
"jsCode": "const config = $('Configure health check').first().json;\nconst now = new Date().toISOString();\nconst runId = 'wazuh-health-' + Date.now();\nconst deploymentAliases = { baremetal: 'on-premises', 'bare-metal': 'on-premises', onprem: 'on-premises', vm: 'virtual-machine', container: 'docker', containers: 'docker', k8s: 'kubernetes' };\nconst requestedDeploymentType = String(config.deploymentType ?? 'on-premises').trim().toLowerCase();\nconst deploymentType = deploymentAliases[requestedDeploymentType] ?? requestedDeploymentType;\nconst supportedDeploymentTypes = new Set(['on-premises', 'virtual-machine', 'docker', 'kubernetes']);\n\nconst valueOf = (name) => {\n const item = $(name).first()?.json ?? {};\n return item;\n};\nconst affected = (name) => valueOf(name)?.data?.affected_items ?? [];\nconst checks = [];\nconst add = (id, component, status, value, threshold, recommendation, evidence = '') => {\n checks.push({ id, component, status, value, threshold, recommendation, evidence });\n};\nconst numeric = (value, fallback = 0) => {\n const parsed = Number(value);\n return Number.isFinite(parsed) ? parsed : fallback;\n};\nconst statusForPercent = (value, warning, critical) => value >= critical ? 'Critical' : value >= warning ? 'Warning' : 'Healthy';\nconst sumNumericLeaves = (value) => {\n if (typeof value === 'number' && Number.isFinite(value)) return value;\n if (!value || typeof value !== 'object') return 0;\n return Object.values(value).reduce((total, entry) => total + sumNumericLeaves(entry), 0);\n};\nconst formatGiB = (bytes) => (numeric(bytes) / 1073741824).toFixed(1) + ' GiB';\n\nconst managerInfo = affected('Get Wazuh manager information')[0] ?? {};\nadd('manager_api', 'Wazuh Manager', managerInfo.version ? 'Healthy' : 'Critical', managerInfo.version ?? 'Unavailable', 'API reachable', 'Verify Wazuh API availability and credentials.');\n\nconst processState = affected('Check Wazuh manager processes')[0] ?? {};\nconst essentialDaemons = ['wazuh-analysisd', 'wazuh-remoted', 'wazuh-modulesd', 'wazuh-db', 'wazuh-apid'];\nconst stoppedDaemons = essentialDaemons.filter(name => processState[name] !== 'running');\nadd('manager_daemons', 'Wazuh Manager', stoppedDaemons.length ? 'Critical' : 'Healthy', stoppedDaemons.length ? stoppedDaemons.join(', ') : 'All essential daemons running', 'All essential daemons running', 'Investigate and restore stopped essential Wazuh processes.', JSON.stringify(processState));\n\nconst agentData = valueOf('Summarize Wazuh agent status')?.data?.connection ?? affected('Summarize Wazuh agent status')[0] ?? {};\nconst activeAgents = numeric(agentData.active);\nconst disconnectedAgents = numeric(agentData.disconnected);\nconst pendingAgents = numeric(agentData.pending);\nconst neverConnectedAgents = numeric(agentData.never_connected ?? agentData.neverConnected);\nconst totalAgents = numeric(agentData.total, activeAgents + disconnectedAgents + pendingAgents + neverConnectedAgents);\nconst disconnectedPercent = totalAgents > 0 ? (disconnectedAgents / totalAgents) * 100 : 0;\nconst agentStatus = totalAgents === 0\n ? 'Unknown'\n : activeAgents === 0\n ? 'Critical'\n : statusForPercent(disconnectedPercent, numeric(config.disconnectedWarningPercent), numeric(config.disconnectedCriticalPercent));\nadd('agent_connectivity', 'Wazuh Agents', agentStatus, activeAgents + ' active; ' + disconnectedAgents + ' disconnected; ' + pendingAgents + ' pending; ' + neverConnectedAgents + ' never connected (' + disconnectedPercent.toFixed(2) + '% disconnected)', 'Warning ' + config.disconnectedWarningPercent + '% / Critical ' + config.disconnectedCriticalPercent + '% or no active agents', 'Review agent connectivity, endpoint availability, certificates and network paths.', JSON.stringify({ activeAgents, disconnectedAgents, pendingAgents, neverConnectedAgents, totalAgents }));\n\nconst analysisd = affected('Collect Wazuh daemon statistics').find(item => item.name === 'wazuh-analysisd') ?? {};\nconst analysisdMetrics = analysisd.metrics ?? {};\nconst received = numeric(analysisdMetrics?.events?.received);\nconst droppedBreakdown = analysisdMetrics?.events?.received_breakdown?.dropped_breakdown ?? {};\nconst dropped = sumNumericLeaves(droppedBreakdown);\nconst droppedRate = received > 0 ? (dropped / received) * 100 : dropped > 0 ? 100 : 0;\nconst droppedStatus = analysisd.name ? statusForPercent(droppedRate, numeric(config.droppedWarningRatePercent), numeric(config.droppedCriticalRatePercent)) : 'Unknown';\nadd('dropped_events', 'Wazuh Analysis', droppedStatus, analysisd.name ? received + ' received; ' + dropped + ' dropped (' + droppedRate.toFixed(3) + '%)' : 'Analysis daemon metrics unavailable', 'Warning ' + config.droppedWarningRatePercent + '% / Critical ' + config.droppedCriticalRatePercent + '%', 'Correlate EPS, CPU, memory, storage and integration behavior to identify ingestion bottlenecks.', JSON.stringify(analysisdMetrics));\n\nconst managerCluster = affected('Check Wazuh server cluster');\nconst clusterDisconnected = managerCluster.filter(node => ['disconnected', 'failed'].includes(String(node.status ?? '').toLowerCase()));\nconst managerClusterValue = managerCluster.length\n ? (clusterDisconnected.length ? clusterDisconnected.length + ' unhealthy node(s)' : managerCluster.length + ' node(s) connected')\n : 'Single-node deployment; manager cluster not enabled';\nadd('manager_cluster', 'Wazuh Server Cluster', clusterDisconnected.length ? 'Critical' : 'Healthy', managerClusterValue, 'All configured nodes connected; not applicable for single-node', 'Review Wazuh server cluster synchronization and node connectivity.', JSON.stringify(managerCluster));\n\nconst indexer = valueOf('Check Wazuh indexer cluster health');\nconst indexerStatus = String(indexer.status ?? 'unknown').toLowerCase();\nconst indexerAvailable = !indexer.error && ['green', 'yellow', 'red'].includes(indexerStatus);\nconst indexerSeverity = !indexerAvailable ? 'Unknown' : indexerStatus === 'red' ? 'Critical' : indexerStatus === 'yellow' ? 'Warning' : 'Healthy';\nconst indexerErrorStatus = indexer?.error?.status ?? indexer?.error?.statusCode ?? 'unavailable';\nadd(\n 'indexer_cluster',\n 'Wazuh Indexer',\n indexerSeverity,\n indexerAvailable\n ? indexerStatus + '; nodes ' + numeric(indexer.number_of_nodes) + '; data nodes ' + numeric(indexer.number_of_data_nodes)\n : 'Cluster health unavailable (HTTP ' + indexerErrorStatus + ')',\n 'Green preferred; Yellow requires topology review; Red is critical',\n indexerAvailable\n ? 'Review unassigned shards, replicas, allocation rules and indexer node availability.'\n : 'Grant the read-only cluster_monitor action group and verify the Indexer credential.',\n JSON.stringify(indexer),\n);\nadd(\n 'indexer_shards',\n 'Wazuh Indexer',\n indexerSeverity,\n indexerAvailable\n ? 'Active primary: ' + numeric(indexer.active_primary_shards) +\n '; active total: ' + numeric(indexer.active_shards) +\n '; unassigned: ' + numeric(indexer.unassigned_shards) +\n '; initializing: ' + numeric(indexer.initializing_shards) +\n '; relocating: ' + numeric(indexer.relocating_shards) +\n '; active: ' + numeric(indexer.active_shards_percent_as_number).toFixed(2) + '%'\n : 'Shard allocation unavailable',\n 'No unassigned primary shards; investigate replicas when status is Yellow',\n indexerAvailable\n ? 'Investigate unassigned primary shards immediately; review replica allocation in the context of the cluster topology.'\n : 'Restore read-only cluster health access before evaluating shard allocation.',\n JSON.stringify(indexer),\n);\n\nconst allocations = Array.isArray(valueOf('Collect indexer storage allocation')) ? valueOf('Collect indexer storage allocation') : $('Collect indexer storage allocation').all().map(i => i.json);\nconst validAllocations = allocations.filter(row => !row.error && (row['disk.percent'] != null || row.disk_percent != null));\nconst allocationAvailable = validAllocations.length > 0;\nconst maxIndexerDisk = allocationAvailable ? Math.max(...validAllocations.map(row => numeric(row['disk.percent'] ?? row.disk_percent))) : null;\nadd('indexer_storage', 'Wazuh Indexer', allocationAvailable ? statusForPercent(maxIndexerDisk, numeric(config.diskWarningPercent), numeric(config.diskCriticalPercent)) : 'Unknown', allocationAvailable ? maxIndexerDisk.toFixed(1) + '%' : 'Storage allocation unavailable', 'Warning ' + config.diskWarningPercent + '% / Critical ' + config.diskCriticalPercent + '%', allocationAvailable ? 'Review ISM retention, index growth, shard sizing and available capacity.' : 'Grant read-only cluster monitoring permission and retry the collection.', JSON.stringify(allocations));\n\nconst indexerNodes = $('Collect indexer node pressure').all().map(i => i.json);\nconst validIndexerNodes = indexerNodes.filter(row => !row.error && (row['heap.percent'] != null || row.heap_percent != null));\nconst indexerNodesAvailable = validIndexerNodes.length > 0;\nconst maxHeap = indexerNodesAvailable ? Math.max(...validIndexerNodes.map(row => numeric(row['heap.percent'] ?? row.heap_percent))) : null;\nadd('indexer_heap', 'Wazuh Indexer', indexerNodesAvailable ? statusForPercent(maxHeap, numeric(config.heapWarningPercent), numeric(config.heapCriticalPercent)) : 'Unknown', indexerNodesAvailable ? maxHeap.toFixed(1) + '%' : 'Node pressure unavailable', 'Warning ' + config.heapWarningPercent + '% / Critical ' + config.heapCriticalPercent + '%', indexerNodesAvailable ? 'Review JVM pressure, query load, shard count and available indexer memory.' : 'Grant read-only cluster monitoring permission and retry the collection.', JSON.stringify(indexerNodes));\n\nconst hostResult = valueOf('Collect host and runtime metrics');\nconst hostValues = {};\nconst containers = [];\nfor (const rawLine of String(hostResult.stdout ?? '').split(String.fromCharCode(10))) {\n const line = rawLine.replace(String.fromCharCode(13), '');\n if (line.startsWith('CONTAINER=')) {\n const [name, state, health, restarts] = line.slice(10).split('|');\n containers.push({ name, state, health, restarts: numeric(restarts) });\n } else {\n const index = line.indexOf('=');\n if (index > 0) hostValues[line.slice(0, index)] = line.slice(index + 1);\n }\n}\nconst hostMetricsAvailable = Object.keys(hostValues).length > 0;\nconst runtimeType = String(hostValues.RUNTIME_TYPE ?? 'none').toLowerCase();\nconst cpuUsed = numeric(hostValues.CPU_USED_PERCENT);\nconst cpuCores = numeric(hostValues.CPU_CORES);\nconst load1m = numeric(hostValues.LOAD_1M);\nconst memoryTotal = numeric(hostValues.MEM_TOTAL);\nconst memoryAvailable = numeric(hostValues.MEM_AVAILABLE);\nconst memoryUsedPercent = memoryTotal > 0 ? ((memoryTotal - memoryAvailable) / memoryTotal) * 100 : 0;\nconst swapTotal = numeric(hostValues.SWAP_TOTAL);\nconst swapUsed = numeric(hostValues.SWAP_USED);\nconst rootTotal = numeric(hostValues.ROOT_TOTAL);\nconst rootAvailable = numeric(hostValues.ROOT_AVAILABLE);\nconst rootUsedPercent = numeric(hostValues.ROOT_USED_PERCENT);\nconst inodeUsedPercent = numeric(hostValues.ROOT_INODE_PERCENT);\nconst hostEvidence = JSON.stringify({ hostname: hostValues.HOSTNAME, cpuUsed, cpuCores, load1m, memoryTotal, memoryAvailable, swapTotal, swapUsed, rootTotal, rootAvailable, rootUsedPercent, inodeUsedPercent });\nadd('host_cpu', 'Host Infrastructure', hostMetricsAvailable ? statusForPercent(cpuUsed, numeric(config.cpuWarningPercent), numeric(config.cpuCriticalPercent)) : 'Unknown', hostMetricsAvailable ? cpuUsed.toFixed(1) + '% used; load ' + load1m.toFixed(2) + ' / ' + cpuCores + ' cores (point-in-time sample)' : 'Host metrics unavailable', 'Warning ' + config.cpuWarningPercent + '% / Critical ' + config.cpuCriticalPercent + '%', 'Review sustained CPU consumption, EPS volume and container-level usage.', hostEvidence);\nadd('host_memory', 'Host Infrastructure', hostMetricsAvailable && memoryTotal > 0 ? statusForPercent(memoryUsedPercent, numeric(config.memoryWarningPercent), numeric(config.memoryCriticalPercent)) : 'Unknown', hostMetricsAvailable && memoryTotal > 0 ? memoryUsedPercent.toFixed(1) + '% used; ' + formatGiB(memoryAvailable) + ' available of ' + formatGiB(memoryTotal) + '; swap ' + formatGiB(swapUsed) + ' used of ' + formatGiB(swapTotal) : 'Memory metrics unavailable', 'Warning ' + config.memoryWarningPercent + '% / Critical ' + config.memoryCriticalPercent + '%', 'Review memory pressure, swap activity and indexer heap allocation.', hostEvidence);\nadd('host_storage', 'Host Infrastructure', hostMetricsAvailable && rootTotal > 0 ? statusForPercent(rootUsedPercent, numeric(config.diskWarningPercent), numeric(config.diskCriticalPercent)) : 'Unknown', hostMetricsAvailable && rootTotal > 0 ? rootUsedPercent.toFixed(1) + '% used; ' + formatGiB(rootAvailable) + ' available of ' + formatGiB(rootTotal) : 'Storage metrics unavailable', 'Warning ' + config.diskWarningPercent + '% / Critical ' + config.diskCriticalPercent + '%', 'Review Wazuh data paths, index retention and historical storage growth for the selected deployment architecture.', hostEvidence);\nadd('host_inodes', 'Host Infrastructure', hostMetricsAvailable ? statusForPercent(inodeUsedPercent, 75, 90) : 'Unknown', hostMetricsAvailable ? inodeUsedPercent.toFixed(1) + '% used' : 'Inode metrics unavailable', 'Warning 75% / Critical 90%', 'Locate directories creating excessive numbers of small files.', hostEvidence);\nconst unhealthyContainers = containers.filter(container => container.state !== 'running' || !['healthy', 'none'].includes(container.health) || container.restarts > 5);\nif (deploymentType === 'docker') {\n add('container_health', 'Deployment Runtime', runtimeType === 'docker' && containers.length ? (unhealthyContainers.length ? 'Critical' : 'Healthy') : 'Unknown', runtimeType === 'docker' && containers.length ? (unhealthyContainers.length ? unhealthyContainers.map(c => c.name).join(', ') : containers.length + ' container(s) running; no unhealthy containers') : 'Docker runtime or container metrics unavailable', 'Docker available; containers running; health healthy/none; restarts <= 5', 'Inspect unhealthy containers, recent logs, restart causes, and Docker access for the SSH account.', JSON.stringify({ deploymentType, runtimeType, containers }));\n} else if (deploymentType === 'kubernetes') {\n add('container_health', 'Deployment Runtime', 'Unknown', 'Kubernetes workload health is not collected by the SSH baseline', 'Kubernetes API plus node and pod metrics required', 'Add authenticated Kubernetes API, metrics-server, or Prometheus collection for node, pod, restart, and persistent-volume health.', JSON.stringify({ deploymentType, runtimeType }));\n} else if (['on-premises', 'virtual-machine'].includes(deploymentType)) {\n add('container_health', 'Deployment Runtime', 'Healthy', 'Not applicable for ' + deploymentType, 'No container runtime check required', 'No runtime action required; retain the host, Wazuh API, and Indexer checks.', JSON.stringify({ deploymentType, runtimeType }));\n} else {\n add('container_health', 'Deployment Runtime', 'Unknown', 'Unsupported deploymentType: ' + deploymentType, 'Use on-premises, virtual-machine, docker, or kubernetes', 'Correct deploymentType in Configure health check before interpreting the assessment.', JSON.stringify({ deploymentType, runtimeType }));\n}\n\nconst criticalCount = checks.filter(check => check.status === 'Critical').length;\nconst warningCount = checks.filter(check => check.status === 'Warning').length;\nconst unknownCount = checks.filter(check => check.status === 'Unknown').length;\nconst greenCount = checks.filter(check => check.status === 'Healthy').length;\nconst verifiedCount = Math.max(1, checks.length - unknownCount);\nconst greenPercent = (greenCount / verifiedCount) * 100;\nconst yellowPercent = (warningCount / verifiedCount) * 100;\nconst redPercent = (criticalCount / verifiedCount) * 100;\nconst coreChecks = new Set(['manager_api', 'manager_daemons', 'agent_connectivity', 'dropped_events', 'indexer_cluster', 'indexer_shards', 'host_cpu', 'host_memory', 'host_storage', 'container_health']);\nconst hasCoreRed = checks.some(check => check.status === 'Critical' && coreChecks.has(check.id));\nconst overallStatus = hasCoreRed || redPercent >= 20\n ? 'Red'\n : unknownCount > 0 || greenPercent < 80 || yellowPercent >= 20\n ? 'Yellow'\n : 'Green';\nconst checkWeights = {\n manager_api: 10,\n manager_daemons: 12,\n agent_connectivity: 8,\n dropped_events: 12,\n manager_cluster: 5,\n indexer_cluster: 10,\n indexer_shards: 10,\n indexer_storage: 8,\n indexer_heap: 5,\n host_cpu: 5,\n host_memory: 5,\n host_storage: 5,\n host_inodes: 2,\n container_health: 3,\n};\nconst statusFactors = { Healthy: 1, Warning: 0.5, Critical: 0, Unknown: 0.25 };\nconst healthScore = Math.round(checks.reduce((score, check) => score + numeric(checkWeights[check.id]) * numeric(statusFactors[check.status]), 0));\n\nconst recommendations = [];\nconst addRecommendation = (priority, title, reason, action) => recommendations.push({ priority, title, reason, action });\nif (rootUsedPercent >= numeric(config.diskWarningPercent) || (allocationAvailable && maxIndexerDisk >= numeric(config.diskWarningPercent))) {\n addRecommendation(rootUsedPercent >= numeric(config.diskCriticalPercent) || (allocationAvailable && maxIndexerDisk >= numeric(config.diskCriticalPercent)) ? 'Critical' : 'High', 'Review Wazuh index lifecycle and retention', 'Storage consumption is approaching or exceeding the configured threshold.', 'Validate ISM policies, retention requirements, index growth, rollover and available capacity.');\n}\nif (dropped > 0 && (cpuUsed >= numeric(config.cpuWarningPercent) || memoryUsedPercent >= numeric(config.memoryWarningPercent))) {\n addRecommendation('Critical', 'Investigate capacity-driven event loss', 'Dropped events coincide with host resource pressure.', 'Correlate EPS peaks with CPU, memory and analysis daemon statistics; resize or tune the environment before increasing ingestion.');\n} else if (dropped > 0) {\n addRecommendation('High', 'Review ingestion and integration configuration', 'Events are being dropped without clear host resource saturation.', 'Inspect recent integrations, decoder/rule load, queue utilization and unexpected EPS sources.');\n}\nif (!indexerAvailable) {\n addRecommendation('High', 'Restore Indexer health visibility', 'The read-only account could not retrieve cluster health and shard allocation.', 'Grant the cluster_monitor action group, verify the role mapping, and repeat the health check.');\n} else if (indexerStatus === 'yellow' || indexerStatus === 'red' || numeric(indexer.unassigned_shards) > 0) {\n addRecommendation(indexerStatus === 'red' ? 'Critical' : 'High', 'Resolve indexer shard allocation', 'The indexer cluster is not fully allocated.', 'Review replica count, single-node topology, disk watermarks and allocation explanations before changing shard settings.');\n}\nif (disconnectedPercent >= numeric(config.disconnectedWarningPercent)) {\n addRecommendation(agentStatus === 'Critical' ? 'Critical' : 'Medium', 'Investigate disconnected Wazuh agents', 'The disconnected-agent ratio exceeds the configured environment threshold.', 'Segment the affected agents by network, operating system and last keepalive to identify a common cause.');\n}\nif (deploymentType === 'docker' && unhealthyContainers.length) {\n addRecommendation('Critical', 'Restore unhealthy containers', 'One or more platform containers are stopped, unhealthy or repeatedly restarting.', 'Review container logs, health checks, resource limits and dependent services.');\n}\nif (deploymentType === 'kubernetes') {\n addRecommendation('High', 'Connect Kubernetes workload telemetry', 'The generic SSH baseline cannot verify Kubernetes node, pod, restart, and persistent-volume health.', 'Add a least-privilege Kubernetes API, metrics-server, or Prometheus integration, then map its evidence into the deployment runtime check.');\n}\nif (!supportedDeploymentTypes.has(deploymentType)) {\n addRecommendation('High', 'Correct the deployment profile', 'The configured deploymentType is not supported.', 'Set deploymentType to on-premises, virtual-machine, docker, or kubernetes and run the assessment again.');\n}\nif (!recommendations.length) addRecommendation('Informational', 'Maintain the current operating baseline', 'No threshold violations were detected.', 'Store this run as a reference and execute the workflow again after significant integrations, upgrades or configuration changes.');\n\nlet previous = $input.first()?.json ?? {};\nlet previousMetrics = {};\ntry { previousMetrics = JSON.parse(previous.metrics_object || '{}'); } catch {}\nconst comparison = previous.runId ? {\n previousRunId: previous.runId,\n previousRunAt: previous.runAt ?? previous.createdAt ?? null,\n previousStatus: previous.overallStatus ?? null,\n previousScore: numeric(previous.healthScore, null),\n scoreChange: previous.healthScore == null ? null : healthScore - numeric(previous.healthScore),\n statusChanged: previous.overallStatus !== overallStatus,\n previousMetricsAvailable: Object.keys(previousMetrics).length > 0,\n} : { baselineCreated: true };\n\nconst escapeHtml = value => String(value ?? '')\n .replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>')\n .replaceAll('\"', '"').replaceAll(\"'\", ''');\nconst statusColor = status => ({ Green: '#15803d', Yellow: '#b45309', Red: '#b91c1c', Healthy: '#15803d', Warning: '#b45309', Critical: '#b91c1c', Unknown: '#475569' }[status] ?? '#475569');\nconst checkRows = checks.map(check => '<tr>' +\n '<td style=\"padding:8px;border-bottom:1px solid #e5e7eb\">' + escapeHtml(check.component) + '</td>' +\n '<td style=\"padding:8px;border-bottom:1px solid #e5e7eb\">' + escapeHtml(check.id) + '</td>' +\n '<td style=\"padding:8px;border-bottom:1px solid #e5e7eb;color:' + statusColor(check.status) + ';font-weight:700\">' + escapeHtml(check.status) + '</td>' +\n '<td style=\"padding:8px;border-bottom:1px solid #e5e7eb\">' + escapeHtml(check.value) + '</td>' +\n '<td style=\"padding:8px;border-bottom:1px solid #e5e7eb\">' + escapeHtml(check.recommendation) + '</td>' +\n '</tr>').join('');\nconst recommendationRows = recommendations.map(item => '<li style=\"margin-bottom:12px\"><strong>' + escapeHtml(item.priority) + ': ' + escapeHtml(item.title) + '</strong><br>' + escapeHtml(item.reason) + '<br><em>Action:</em> ' + escapeHtml(item.action) + '</li>').join('');\nconst comparisonText = comparison.baselineCreated ? 'This is the first stored baseline.' : 'Previous status: ' + escapeHtml(comparison.previousStatus) + '; score change: ' + (comparison.scoreChange >= 0 ? '+' : '') + comparison.scoreChange + '.';\nconst htmlReport = '<div style=\"font-family:Arial,sans-serif;color:#111827;max-width:1000px;margin:auto\">' +\n '<div style=\"background:#111827;color:white;padding:24px;border-radius:10px 10px 0 0\"><h1 style=\"margin:0\">Wazuh Platform Health Check</h1><p>' + escapeHtml(config.organizationName) + ' \u00b7 ' + escapeHtml(now) + '</p></div>' +\n '<div style=\"padding:24px;border:1px solid #e5e7eb\"><h2 style=\"color:' + statusColor(overallStatus) + '\">Overall status: ' + overallStatus + ' \u00b7 Score: ' + healthScore + '/100</h2>' +\n '<p><strong>Assessment:</strong> ' + escapeHtml(config.assessmentLabel) + '<br><strong>Deployment:</strong> ' + escapeHtml(deploymentType) + '<br><strong>Comparison:</strong> ' + comparisonText + '</p>' +\n '<p><strong>Green checks:</strong> ' + greenCount + ' (' + greenPercent.toFixed(1) + '%) \u00b7 <strong>Yellow checks:</strong> ' + warningCount + ' (' + yellowPercent.toFixed(1) + '%) \u00b7 <strong>Red checks:</strong> ' + criticalCount + ' (' + redPercent.toFixed(1) + '%) \u00b7 <strong>Unverified:</strong> ' + unknownCount + '</p>' +\n '<p style=\"color:#475569\"><strong>Overall rule:</strong> Green when at least 80% of verified checks are green, fewer than 20% are yellow, no core check is red, and no check is unverified. Unverified data produces Yellow because the assessment is incomplete.</p>' +\n '<h2>Prioritized recommendations</h2><ol>' + recommendationRows + '</ol>' +\n '<h2>Technical checks</h2><table style=\"border-collapse:collapse;width:100%;font-size:13px\"><thead><tr style=\"background:#f3f4f6\"><th>Component</th><th>Check</th><th>Status</th><th>Value</th><th>Recommendation</th></tr></thead><tbody>' + checkRows + '</tbody></table>' +\n '<p style=\"font-size:12px;color:#6b7280;margin-top:24px\">Thresholds are configurable. Interpret this report with the environment profile, expected agent count, expected EPS, architecture and retention requirements.</p></div></div>';\n\nreturn [{ json: {\n runId,\n runAt: now,\n assessmentLabel: config.assessmentLabel,\n deploymentType,\n overallStatus,\n healthScore,\n criticalCount,\n warningCount,\n greenCount,\n unknownCount,\n greenPercent: Number(greenPercent.toFixed(2)),\n yellowPercent: Number(yellowPercent.toFixed(2)),\n redPercent: Number(redPercent.toFixed(2)),\n checks,\n recommendations,\n comparison,\n metrics_object: JSON.stringify({ deploymentType, runtimeType, checks, hostValues, containers, managerInfo, agentData, indexer }),\n recommendations_object: JSON.stringify(recommendations),\n comparison_object: JSON.stringify(comparison),\n emailSubject: '[' + overallStatus.toUpperCase() + '] ' + config.organizationName + ' Wazuh Health Check - ' + now.slice(0, 10),\n htmlReport,\n}}];"
},
"typeVersion": 2
},
{
"id": "7a2ba934-668f-42f5-a0c4-953042b1a4b0",
"name": "Store health check run",
"type": "n8n-nodes-base.dataTable",
"position": [
2640,
32
],
"parameters": {
"columns": {
"value": {
"runAt": "={{ $json.runAt }}",
"runId": "={{ $json.runId }}",
"reportSent": true,
"healthScore": "={{ $json.healthScore }}",
"warningCount": "={{ $json.warningCount }}",
"criticalCount": "={{ $json.criticalCount }}",
"overallStatus": "={{ $json.overallStatus }}",
"metrics_object": "={{ $json.metrics_object }}",
"assessmentLabel": "={{ $json.assessmentLabel }}",
"comparison_object": "={{ $json.comparison_object }}",
"recommendations_object": "={{ $json.recommendations_object }}"
},
"schema": [
{
"id": "runId",
"type": "string",
"display": true,
"required": false,
"displayName": "runId",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "runAt",
"type": "dateTime",
"display": true,
"required": false,
"displayName": "runAt",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "assessmentLabel",
"type": "string",
"display": true,
"required": false,
"displayName": "assessmentLabel",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "overallStatus",
"type": "string",
"display": true,
"required": false,
"displayName": "overallStatus",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "healthScore",
"type": "number",
"display": true,
"required": false,
"displayName": "healthScore",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "criticalCount",
"type": "number",
"display": true,
"required": false,
"displayName": "criticalCount",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "warningCount",
"type": "number",
"display": true,
"required": false,
"displayName": "warningCount",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "metrics_object",
"type": "string",
"display": true,
"required": false,
"displayName": "metrics_object",
"defaultMatch": false,
"canBeUsedToMatch": false
},
{
"id": "recommendations_object",
"type": "string",
"display": true,
"required": false,
"displayName": "recommendations_object",
"defaultMatch": false,
"canBeUsedToMatch": false
},
{
"id": "comparison_object",
"type": "string",
"display": true,
"required": false,
"displayName": "comparison_object",
"defaultMatch": false,
"canBeUsedToMatch": false
},
{
"id": "reportSent",
"type": "boolean",
"display": true,
"required": false,
"displayName": "reportSent",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"dataTableId": {
"__rl": true,
"mode": "name",
"value": "Wazuh Health Runs",
"cachedResultName": "Wazuh Health Runs"
}
},
"typeVersion": 1.1
},
{
"id": "a03c74f0-8bf9-4bfb-9cfb-ac1b0b36ae30",
"name": "Send Wazuh health report",
"type": "n8n-nodes-base.emailSend",
"onError": "continueRegularOutput",
"maxTries": 3,
"position": [
2640,
288
],
"parameters": {
"html": "={{ $json.htmlReport }}",
"options": {
"replyTo": "={{ $(\"Configure health check\").first().json.senderEmail }}",
"appendAttribution": false
},
"subject": "={{ $json.emailSubject }}",
"toEmail": "={{ $(\"Configure health check\").first().json.reportRecipient }}",
"fromEmail": "={{ $(\"Configure health check\").first().json.senderName + \" <\" + $(\"Configure health check\").first().json.senderEmail + \">\" }}"
},
"credentials": {
"smtp": {
"name": "<your credential>"
}
},
"executeOnce": true,
"retryOnFail": true,
"typeVersion": 2.1,
"waitBetweenTries": 5000
},
{
"id": "94fa6464-e068-48e1-ab9e-4efc6f619752",
"name": "Workflow overview",
"type": "n8n-nodes-base.stickyNote",
"position": [
-832,
-368
],
"parameters": {
"color": "#FFE770",
"width": 640,
"height": 1050,
"content": "## Assess Wazuh platform health\n\n**Wazuh** is an open-source SIEM and XDR platform for threat detection, security monitoring, compliance, and vulnerability detection. This workflow is designed primarily for **on-premises Wazuh environments** and can be adapted to other customer-managed deployment models supported by Wazuh, including virtual machines, Docker, and Kubernetes. It evaluates whether the platform can reliably collect, process, index, and report security telemetry.\n\n### How it works\n\n\u2192 Reads Wazuh Manager, daemon, and agent health through the Wazuh API.\n\n\u2192 Reads indexer cluster health, shard allocation, storage, and JVM pressure through the Wazuh Indexer API.\n\n\u2192 Samples Linux CPU, memory, disk, inode, and uptime through restricted SSH. Runtime-specific checks are applied according to the selected deployment type.\n\n\u2192 Applies configurable thresholds and produces a weighted 0-100 score plus a Green, Yellow, or Red overall status.\n\n\u2192 Compares the run with the previous baseline, creates explainable recommendations, stores the evidence, and sends an HTML report.\n\n### Setup\n\n\u2192 In **Configure health check**, set `deploymentType` to `on-premises`, `virtual-machine`, `docker`, or `kubernetes`; replace every example value and review the thresholds for your environment size and expected workload.\n\n\u2192 Select a read-only Wazuh API Basic Auth credential and a read-only Wazuh Indexer Basic Auth credential with cluster-monitoring access.\n\n\u2192 Select an SSH Private Key credential for a dedicated least-privilege account that can run only the documented read-only host commands.\n\n\u2192 Adapt **Collect host and runtime metrics** when the selected architecture requires a different infrastructure telemetry source.\n\n\u2192 Create a Data Table named **Wazuh Health Runs** with: `runId` (string), `runAt` (date), `assessmentLabel` (string), `overallStatus` (string), `healthScore` (number), `criticalCount` (number), `warningCount` (number), `metrics_object` (string), `recommendations_object` (string), `comparison_object` (string), and `reportSent` (boolean). Select it in both Data Table nodes.\n\n\u2192 Select an SMTP credential, verify the sender and recipient, then run the workflow manually.\n\n### Customization\n\n\u2192 Tune CPU, memory, disk, heap, disconnected-agent, and dropped-event thresholds. Adapt the infrastructure evidence source to the selected architecture while keeping the Wazuh API and Indexer checks. Keep the assessment deterministic; optional AI enrichment should summarize recommendations, not decide platform health.\n\n### Security\n\n\u2192 The template contains no secrets. Store credentials only in n8n Credentials and apply least privilege to every account."
},
"typeVersion": 1
},
{
"id": "4e1d9b48-88dc-4461-af17-2abcbc4cdc90",
"name": "Collection guidance",
"type": "n8n-nodes-base.stickyNote",
"position": [
128,
-160
],
"parameters": {
"color": 7,
"width": 1600,
"height": 712,
"content": "### 1 - Collect deployment-aware platform evidence\nUse dedicated read-only accounts for the Wazuh API and Indexer. Set `deploymentType` to `on-premises`, `virtual-machine`, `docker`, or `kubernetes`. The Wazuh API and Indexer checks remain consistent, while **Collect host and runtime metrics** can be adapted to the infrastructure used by each deployment. Unavailable evidence is explicitly marked as unverified."
},
"typeVersion": 1
},
{
"id": "596fde1b-61f9-4f6e-a5be-f35c47f8b984",
"name": "Assessment guidance",
"type": "n8n-nodes-base.stickyNote",
"position": [
1808,
-160
],
"parameters": {
"color": 7,
"width": 560,
"height": 712,
"content": "### 2 - Assess, compare, and recommend\nEach technical check is classified as Healthy, Warning, Critical, or Unknown. Overall health is Green when at least 80% of verified checks are healthy, fewer than 20% are warnings, no core check is critical, and no evidence is unknown. Yellow indicates degradation or incomplete evidence. Red indicates a critical core failure or at least 20% critical checks. The weighted score measures impact separately from the overall color."
},
"typeVersion": 1
},
{
"id": "6f707228-9f20-4706-b5c1-a3c9ffa7e46c",
"name": "Output guidance",
"type": "n8n-nodes-base.stickyNote",
"position": [
2448,
-160
],
"parameters": {
"color": 7,
"width": 520,
"height": 712,
"content": "### 3 - Report and preserve evidence\nThe final summary is stored in the selected Data Table to preserve an auditable baseline and is sent as an HTML email. Both nodes are intentionally terminal outputs. Review the execution log when either output is unavailable."
},
"typeVersion": 1
}
],
"active": false,
"settings": {
"binaryMode": "separate",
"availableInMCP": true,
"executionOrder": "v1"
},
"versionId": "8b407aec-e07c-45bb-bc7b-3f32b24fa53f",
"nodeGroups": [],
"connections": {
"Configure health check": {
"main": [
[
{
"node": "Authenticate to Wazuh API",
"type": "main",
"index": 0
}
]
]
},
"Authenticate to Wazuh API": {
"main": [
[
{
"node": "Get Wazuh manager information",
"type": "main",
"index": 0
}
]
]
},
"Run health check manually": {
"main": [
[
{
"node": "Configure health check",
"type": "main",
"index": 0
}
]
]
},
"Check Wazuh server cluster": {
"main": [
[
{
"node": "Check Wazuh indexer cluster health",
"type": "main",
"index": 0
}
]
]
},
"Load previous health check": {
"main": [
[
{
"node": "Evaluate Wazuh health and recommendations",
"type": "main",
"index": 0
}
]
]
},
"Summarize Wazuh agent status": {
"main": [
[
{
"node": "Check Wazuh server cluster",
"type": "main",
"index": 0
}
]
]
},
"Check Wazuh manager processes": {
"main": [
[
{
"node": "Collect Wazuh manager statistics",
"type": "main",
"index": 0
}
]
]
},
"Collect indexer node pressure": {
"main": [
[
{
"node": "Collect host and runtime metrics",
"type": "main",
"index": 0
}
]
]
},
"Get Wazuh manager information": {
"main": [
[
{
"node": "Check Wazuh manager processes",
"type": "main",
"index": 0
}
]
]
},
"Collect Wazuh daemon statistics": {
"main": [
[
{
"node": "Summarize Wazuh agent status",
"type": "main",
"index": 0
}
]
]
},
"Collect Wazuh manager statistics": {
"main": [
[
{
"node": "Collect Wazuh daemon statistics",
"type": "main",
"index": 0
}
]
]
},
"Collect host and runtime metrics": {
"main": [
[
{
"node": "Load previous health check",
"type": "main",
"index": 0
}
]
]
},
"Check Wazuh indexer cluster health": {
"main": [
[
{
"node": "Collect indexer storage allocation",
"type": "main",
"index": 0
}
]
]
},
"Collect indexer storage allocation": {
"main": [
[
{
"node": "Collect indexer node pressure",
"type": "main",
"index": 0
}
]
]
},
"Evaluate Wazuh health and recommendations": {
"main": [
[
{
"node": "Store health check run",
"type": "main",
"index": 0
},
{
"node": "Send Wazuh health report",
"type": "main",
"index": 0
}
]
]
}
}
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
smtp
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow runs a manual health assessment of a Wazuh deployment by querying the Wazuh Manager API, Wazuh Indexer API, and host metrics over SSH, then scores the results, generates prioritized recommendations, stores a baseline in an n8n Data Table, and emails an HTML report.…
Source: https://n8n.io/workflows/18029/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
This workflow runs daily (or manually) to pull vulnerability findings from Wazuh Indexer, enriches CVEs with FIRST EPSS and the CISA KEV catalog, calculates a priority score, tracks finding lifecycle
The Sora 2 API allows seamless generation of CGI ads, turning text prompts into stunning videos. This workflow automates the entire process from video generation to upload, notification, and file shar
This workflow automates the collection of public procurement data from TenderNed (the official Dutch tender platform). It: Fetches the latest tender publications from the TenderNed API Retrieves detai
Storyboard I2V Pipeline (17 shots) with Retry/Wait/State/Notify. Uses httpTrigger, httpRequest, emailSend. Event-driven trigger; 28 nodes.
N8N-Self-Updater. Uses ssh, emailSend, httpRequest. Scheduled trigger; 27 nodes.