Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions integrations/detect_new_agents/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# New agent detection

## Table of Contents
* [Introduction](#introduction)
* [Prerequisites](#prerequisites)
* [Integration Steps](#integration-steps)
* [Testing](#testing)
* [Sources](#sources)

## Introduction
This script runs one one of your Wazuh agents to query the list of agents from the manager’s API, and send a log to the manager when new agents are
discovered.

The agent will execute the script every 5 minutes. If an agent has been enrolled during the last 5 minutes, it will write a log in the monitored log file.
The log will trigger the custom rule 100110 which is configured to automatically send email notifications.

## Prerequisites
- The endpoint running the script should be enrolled to the Wazuh manager and active.
- The endpoint should have Python3 installed.

## Integration Steps
### Add the integration script
- Copy the `detect_new_agents.py` script to `/opt/scripts/` folder of your agent.
- Edit the script to set the correct credentials (`USERNAME` and `PASSWORD` variables) and Wazuh manager IP address (`MANAGER_IP`). You may need to create a new manager’s API user.
- Set the permissions and ownership:
```
chown root:wazuh /opt/scripts/detect_new_agents.py
chmod 750 /opt/scripts/detect_new_agents.py
```

### Wazuh agent configuration
- Add the following to the configuration of the agent where you copied the integration script:
```
<wodle name="command">
<disabled>no</disabled>
<tag>new-agent-detector</tag>
<command>/opt/scripts/detect_new_agents.py</command>
<interval>5m</interval>
<run_on_start>yes</run_on_start>
<timeout>60</timeout>
<ignore_output>yes</ignore_output>
</wodle>

<localfile>
<log_format>json</log_format>
<location>/var/ossec/logs/new_agents.json</location>
</localfile>
```
The script will run every 5 minutes and write logs to a local file (`/var/ossec/logs/new_agents.json`) when new agents are discovered.
The localfile section ensures the logs will be forwarded to the manager.

### Add custom rule
- In Wazuh Dashboard go to Server Management > Rules > Add new rules file. Name it `detect_new_agent-rules.xml`, add the content of detect_new_agent-rules.xml and save.
- Restart the Wazuh Manager to apply the changes.

## Testing
Once the configuration on the manager is done, the integration will run after 5 minutes and the alerts will appear on the dashboard.
<img width="1358" height="82" alt="image" src="https://github.com/user-attachments/assets/1c783cd1-91bb-4e50-a4f1-edb486883e1a" />

When running for the first time, no agent will be reported.


## Sources
- https://documentation.wazuh.com/current/user-manual/api/reference.html
- https://documentation.wazuh.com/current/user-manual/reference/ossec-conf/wodle-command.html
8 changes: 8 additions & 0 deletions integrations/detect_new_agents/detect_new_agent-rule.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<group name="agent_enrollment,">
<rule id="100110" level="6">
<decoded_as>json</decoded_as>
<field name="integration">new-agent-detector</field>
<description>New Wazuh agent enrolled: $(agent_info.name) (ID $(agent_info.id), IP $(agent_info.ip))</description>
<options>alert_by_email</options>
</rule>
</group>
195 changes: 195 additions & 0 deletions integrations/detect_new_agents/detect_new_agents.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
#!/usr/bin/python3
#
# detect_new_agents.py
# Detects newly enrolled Wazuh agents by comparing agent IDs returned by the
# Wazuh manager API against the highest agent ID seen so far (cache file).
# Each new agent is appended, as one JSON object per line, to a local log
# file that can be ingested back into Wazuh with a <localfile> block.

import json
import logging
import os
import sys

import requests
import urllib3

# === CONFIGURATION ===
MANAGER_IP = "MANAGER_IP"
API_URL = f"https://{MANAGER_IP}:55000"
API_USER = "USERNAME"
API_PASSWORD = "PASSWORD"
VERIFY_SSL = False # Set to True (or a CA bundle path) in production
PAGE_SIZE = 500 # Agents fetched per API request (max 100000)

CACHE_FILE = "/var/ossec/new_agents_cache.txt"
OUTPUT_LOG = "/var/ossec/logs/new_agents.json"
SCRIPT_LOG = "/var/ossec/logs/new_agents_integration.log"

# On the very first run (no cache file), report all existing agents (True)
# or just create a baseline with the current highest ID (False).
FIRST_RUN_REPORT_ALL = False

# === LOGGING ===
logging.basicConfig(filename=SCRIPT_LOG,
filemode='a',
format='%(asctime)s,%(msecs)d %(name)s %(levelname)s %(message)s',
datefmt='%Y-%m-%dT%H:%M:%S',
level=logging.INFO)

if not VERIFY_SSL:
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)


def get_token():
"""Authenticate against the Wazuh API and return a JWT token."""
url = f"{API_URL}/security/user/authenticate"
try:
resp = requests.post(url, auth=(API_USER, API_PASSWORD), verify=VERIFY_SSL, timeout=30)
except requests.exceptions.RequestException as err:
logging.error("Could not reach the Wazuh API: %s", err)
sys.exit(1)

if resp.status_code != 200:
logging.error("API authentication failed. Status: %s. Response: %s",
resp.status_code, resp.text[:200])
sys.exit(1)

return resp.json()["data"]["token"]


def fetch_all_agents(token):
"""Return the full list of agents (excluding the manager, ID 000)."""
headers = {"Authorization": f"Bearer {token}"}
agents = []
offset = 0

while True:
params = {
"limit": PAGE_SIZE,
"offset": offset,
"sort": "+id",
"select": "id,name,ip,registerIP,dateAdd,os.name,os.version,os.platform",
}
try:
resp = requests.get(f"{API_URL}/agents", headers=headers,
params=params, verify=VERIFY_SSL, timeout=30)
except requests.exceptions.RequestException as err:
logging.error("Error querying /agents: %s", err)
sys.exit(1)

if resp.status_code != 200:
logging.error("Error fetching agents. Status: %s. Response: %s",
resp.status_code, resp.text[:200])
sys.exit(1)

data = resp.json().get("data", {})
items = data.get("affected_items", [])
agents.extend(a for a in items if a.get("id") != "000")

offset += PAGE_SIZE
if offset >= data.get("total_affected_items", 0):
break

return agents


def read_cache():
"""Return the highest known agent ID as int, or None if no cache exists."""
if not os.path.isfile(CACHE_FILE):
return None
try:
with open(CACHE_FILE, "r") as f:
content = f.read().strip()
return int(content) if content else None
except (OSError, ValueError) as err:
logging.error("Could not read cache file '%s': %s", CACHE_FILE, err)
sys.exit(1)


def write_cache(highest_id):
"""Persist the highest agent ID (zero-padded, as Wazuh displays it)."""
try:
os.makedirs(os.path.dirname(CACHE_FILE), exist_ok=True)
with open(CACHE_FILE, "w") as f:
f.write(str(highest_id).zfill(3))
except OSError as err:
logging.error("Could not write cache file '%s': %s", CACHE_FILE, err)
sys.exit(1)


def build_record(agent):
"""Build the JSON record written to the output log for one new agent."""
os_info = agent.get("os", {})
return {
"integration": "new-agent-detector",
"registration_time": agent.get("dateAdd", "unknown"),
"agent_info": {
"id": agent.get("id"),
"name": agent.get("name"),
"ip": agent.get("ip", agent.get("registerIP", "unknown")),
"os": {
"name": os_info.get("name", "unknown"),
"version": os_info.get("version", "unknown"),
"platform": os_info.get("platform", "unknown"),
},
},
}


def append_records(records):
"""Append one JSON object per line to the output log file."""
try:
with open(OUTPUT_LOG, "a") as f:
for record in records:
f.write(json.dumps(record, ensure_ascii=False) + "\n")
except OSError as err:
logging.error("Could not write output log '%s': %s", OUTPUT_LOG, err)
sys.exit(1)


def main():
logging.info("Running the new-agent detection script.")

token = get_token()
agents = fetch_all_agents(token)

if not agents:
logging.info("No agents registered in the environment. Nothing to do.")
return

current_max = max(int(a["id"]) for a in agents)
cached_max = read_cache()

# First run: create the cache file
if cached_max is None:
if FIRST_RUN_REPORT_ALL:
cached_max = 0
logging.info("No cache file found. Reporting all existing agents.")
else:
write_cache(current_max)
logging.info("No cache file found. Baseline created with highest ID %s. "
"No agents reported on first run.", str(current_max).zfill(3))
return

new_agents = [a for a in agents if int(a["id"]) > cached_max]

if not new_agents:
logging.info("No new agents detected (cached highest ID: %s).",
str(cached_max).zfill(3))
return

records = [build_record(a) for a in new_agents]
append_records(records)

new_max = max(int(a["id"]) for a in new_agents)
write_cache(new_max)

names = ", ".join(f"{a['id']}:{a['name']}" for a in new_agents)
logging.info("Detected %d new agent(s): %s. Cache updated to %s.",
len(new_agents), names, str(new_max).zfill(3))
print(f"Detected {len(new_agents)} new agent(s). Logged to {OUTPUT_LOG}.")


if __name__ == "__main__":
main()