diff --git a/integrations/wazuh-troubleshooting-tool/.gitignore b/integrations/wazuh-troubleshooting-tool/.gitignore
new file mode 100644
index 00000000..c2b9daab
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/.gitignore
@@ -0,0 +1,6 @@
+.env
+backend/sessions/
+backend/wizard_history/
+backend/__pycache__/
+backend/**/__pycache__/
+*.pyc
diff --git a/integrations/wazuh-troubleshooting-tool/HOW_THE_TOOL_WORKS.txt b/integrations/wazuh-troubleshooting-tool/HOW_THE_TOOL_WORKS.txt
new file mode 100644
index 00000000..a92832a7
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/HOW_THE_TOOL_WORKS.txt
@@ -0,0 +1,125 @@
+Wazuh Troubleshooting Tool - How It Works (plain language)
+=============================================================
+
+WHAT THIS TOOL IS
+------------------
+It's a web dashboard you open in your browser. It talks to a Wazuh
+installation (the Manager, the Indexer, the Dashboard) and helps a
+person diagnose and fix problems with it, either by running checks
+automatically, walking you through step-by-step wizards, or letting
+you chat with an AI that understands Wazuh.
+
+There are 5 tabs on the left side of the screen. Here's what each one does.
+
+1. HOME (the main dashboard)
+-----------------------------
+This is the page you land on. It shows:
+- Big status boxes at the top: is everything OK, how many issues are
+ found, how many services are online, cluster status.
+- A "Run Health Check" button. When you click it, the backend checks:
+ are wazuh-indexer, wazuh-manager, and wazuh-dashboard actually
+ running, can it log into the Wazuh API, and is the Indexer cluster
+ healthy (green/yellow/red).
+- A small chat box right there on the dashboard where you can type a
+ problem in plain English (like "alerts are not showing up") and it
+ will walk you through a fix, step by step. This is a wizard, not a
+ free-form AI chat, it follows a fixed script for known problems. If
+ the wizard runs out of steps without actually confirming the issue
+ is fixed, it automatically looks up similar known issues (local
+ knowledge base plus a live search of public Wazuh GitHub
+ issues/discussions) and points you to the Wazuh community, instead
+ of just stopping. This lookup is a similarity match, not an AI
+ writing an answer, so it doesn't need Ollama or Claude. If a wizard
+ does confirm the fix worked, you just get a clean "resolved"
+ message with no extra suggestion.
+- A live services list, cluster stats, and memory usage.
+- Quick action buttons: restart a service, test filebeat, with one click.
+
+No AI/LLM is required for any of this. It's all direct checks against
+your Wazuh services (systemctl, the Wazuh API, the Indexer API).
+
+2. TROUBLESHOOTING LIBRARY
+----------------------------
+This is a list of common, named problems, for example:
+ Dashboard Error, Application Not Found, Alerts Not Showing,
+ Filebeat Error, Filebeat Mapping Issue, Alerts Not Indexing,
+ Cluster Health Issues, Indexer Problems.
+
+You pick one, and it opens the same kind of step-by-step wizard as the
+dashboard's chat box, but focused on that specific problem. It asks
+you questions, checks things on the system, and offers to run fixes
+(with your yes/no approval at each risky step). Just like the
+dashboard's chat box, if a wizard here runs out of steps without
+confirming the issue is actually fixed, it looks up similar known
+issues and points you to the Wazuh community instead of just
+stopping. Once a wizard finishes (fixed or not), it saves a small
+transcript so you can look back at what was done and download it
+later.
+
+Again, no AI/LLM needed here either, these are fixed logic scripts
+per known issue, not the AI chatting freely - the community lookup on
+an unresolved ending is a similarity search, not AI-generated text.
+
+3. OPERATIONS REPORTING CENTER
+--------------------------------
+This tab lets you generate reports: Agent Fleet Health, Dashboard,
+Data Flow, Cluster Health, Environment, Security Events. You pick a
+report type (or build a custom one by choosing which sections to
+include), click Generate, and it puts together a readable report you
+can view or export (PDF/HTML), based on live data pulled from your
+Wazuh install.
+
+4. WAZUH COPILOT / AGENT (this is where the AI actually is)
+--------------------------------------------------------------
+This tab is the real AI chat. There's a dropdown where you pick which
+"brain" answers you:
+ - Ollama (local): a small AI model that runs on the same machine,
+ no internet connection or API key needed. It's fast but limited,
+ it can answer questions and explain things using your live system
+ data and a local knowledge base of known Wazuh issues, but it
+ can't actually take actions on the system.
+ - Claude (API): a much more capable AI (Anthropic's Claude), used
+ only if you've added an API key in the config file. This one can
+ actually DO things: it can call tools to check statuses, read
+ logs, and even apply fixes, but only after you approve any change
+ it wants to make. It never touches your system without asking
+ first.
+
+Both brains use the same knowledge sources: your Wazuh system's live
+status, a local database of real Wazuh community issues/solutions
+(so it doesn't just guess), and (for Claude) real official Wazuh
+documentation pages when relevant.
+
+This is the only tab where you're talking to a genuinely open-ended
+AI rather than a fixed wizard script.
+
+5. SETTINGS
+------------
+Basic configuration display/controls for the tool itself.
+
+WHERE THE "AI" ACTUALLY LIVES IN THE CODE (for reference)
+------------------------------------------------------------
+- assistant_engine.py + use_cases/*.py: the fixed step-by-step wizards
+ used by the Home dashboard's chat box AND the Troubleshooting
+ Library. No LLM involved, just guided logic.
+- copilot_engine.py: quick one-shot AI answers, used to help build
+ context for the Agent tab.
+- agent_engine.py + agent_brain.py + agent_tools.py: the real AI
+ agent loop (plan, call a tool, look at the result, repeat), used
+ only in the Wazuh Copilot / Agent tab. This is where Ollama or
+ Claude actually think and decide what to check or fix.
+- backend/knowledge/: a local database of real, past Wazuh community
+ issues and solutions, which both the wizards and the AI use to give
+ grounded, accurate answers instead of making things up.
+- utils/unresolved_help.py: used by every wizard's ending. On a
+ confirmed fix it just finishes cleanly; otherwise it looks up
+ similar issues (local knowledge base + live public GitHub search)
+ and points to the Wazuh community. Lookup only, no AI generation.
+
+IN ONE SENTENCE
+-----------------
+Three tabs (Home, Library, Reports) are automated checks and guided
+scripts with no AI involved; one tab (Wazuh Copilot / Agent) is where
+you actually talk to an AI, which can either be a small local model
+or Claude, and Claude is the only one that can take real action on
+your system, always with your approval first.
diff --git a/integrations/wazuh-troubleshooting-tool/README.md b/integrations/wazuh-troubleshooting-tool/README.md
new file mode 100644
index 00000000..f774eb17
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/README.md
@@ -0,0 +1,210 @@
+# Wazuh Troubleshooting & Operations Portal
+
+A dedicated, unified web portal to assess, diagnostic-report, and troubleshoot Wazuh deployments. The system provides real-time health checks, interactive diagnostic guides, an AI troubleshooting agent, and native vector reporting.
+
+## Key Features
+
+1. **Operations Reporting Center**: Natively generated interactive diagnostic report modules (Agent Fleet Health, Indexer Pipeline, Cluster health, Environmental assessments, and Security Events) with offline simulation support and print-formatted PDF/HTML export capabilities.
+2. **Interactive Diagnostic Wizards**: Step-by-step troubleshooting wizards for common Wazuh issues like alerts not showing, indexing pipeline errors, and database cluster yellow/red states.
+3. **Wazuh Agent AI**: A tool-calling troubleshooting agent that can run read-only checks on its own and always pauses for your approval before any change to the system.
+4. **AI Assistant (chat)**: Context-aware AI chat guidance for platform operators, backed by a local knowledge base synced from Wazuh community issues/discussions.
+5. **Secure by default**: No credentials or connection URLs are hardcoded anywhere in source code, everything sensitive lives in one gitignored `config` file.
+
+## Directory Structure
+
+```text
+wazuh-troubleshooting-tool/
+├── config # Placeholder credentials template (tracked, see step 4)
+├── .gitignore
+├── README.md
+├── start.sh # Dev environment launcher (backend + frontend)
+├── backend/ # Python FastAPI backend server
+│ ├── config.py # Loads config into importable constants
+│ ├── main.py # HTTP routes: health checks, reports, assistant, agent
+│ ├── wazuh_api.py # Wazuh Manager API auth/token helper
+│ ├── copilot_engine.py # Ollama-backed "Copilot" quick-answer engine
+│ ├── assistant_engine.py # Routes chat input into use_cases/ wizards
+│ ├── agent_engine.py # The Wazuh Agent's plan/tool/observe loop
+│ ├── agent_brain.py # Dual-brain abstraction: Ollama vs Claude
+│ ├── agent_tools.py # Tool registry the agent can call
+│ ├── use_cases/ # One module per diagnostic wizard (see below)
+│ ├── flows/ # Shared multi-step state machines reused across wizards
+│ ├── utils/ # Service/cluster/index/log helpers, the actual diagnostic logic
+│ ├── knowledge/ # Scripts + shipped data for the local community-issue
+│ │ # knowledge base (backend/knowledge/lgtm.db, lgtm_issues.json)
+│ ├── sessions/ # Agent chat session state, created automatically at runtime
+│ └── wizard_history/ # Completed Troubleshooting Library run transcripts, created
+│ # automatically at runtime, see /assistant/history
+└── frontend/ # HTML5/CSS/JS frontend views
+ ├── index.html # App shell, all panel layouts
+ ├── app.js # Routing, health-check polling, report triggers
+ ├── assistant.js # Chat UI for the wizard-driven Assistant
+ ├── agent.js # Chat UI for the Wazuh Agent (brain picker, approvals)
+ ├── manual.js # Manual diagnostics panel
+ ├── reports.js # SVG charts & report export engine
+ └── styles.css
+```
+
+## Setup & Configuration
+
+### 1. Prerequisites
+
+- **Python 3.10+** and `pip`
+- **[Ollama](https://ollama.com)** installed and running locally (used for the chat Assistant, the Copilot, the Ollama agent brain, and the local knowledge-base embeddings)
+- A standard Linux host with `curl`, `systemctl`, and `sed` available. The diagnostic/fix tools shell out to these to inspect and manage `wazuh-indexer`, `wazuh-manager`, `wazuh-dashboard`, and `filebeat`
+- Network access to your Wazuh Manager API, Wazuh Indexer, and Kibana/Dashboard
+- `sudo` privileges for the user running the backend, if you want the fix tools (service restarts, cert/config edits) to actually work rather than just report what they'd do
+
+### 2. Install backend dependencies
+
+Run this from the project root (no need to `cd` into `backend/` first):
+
+```bash
+pip install -r backend/requirements.txt
+```
+
+This installs every third-party package the backend needs:
+
+| Package | Used for |
+|---|---|
+| `fastapi` | The backend's HTTP API framework |
+| `uvicorn` | ASGI server that actually runs the FastAPI app |
+| `requests` | All HTTP calls to the Wazuh API, Indexer, Kibana, GitHub, and Ollama |
+| `rapidfuzz` | Fuzzy phrase matching that routes chat input to the right diagnostic wizard |
+| `pyyaml` | Parses the `config` file's YAML in `start.sh` |
+| `anthropic` | Optional Claude brain for the Wazuh Agent (see step 5) |
+| `numpy` | Vector similarity search over the local knowledge base (`backend/knowledge/lgtm.db`) |
+
+Everything else imported (`sqlite3`, `subprocess`, `uuid`, `gzip`, etc.) is Python's standard library, no separate install needed.
+
+### 3. Pull the required Ollama models
+
+```bash
+ollama pull qwen3:1.7b # chat model: Copilot, Assistant, and the Ollama agent brain
+ollama pull nomic-embed-text # embedding model: powers the local knowledge-base search
+```
+
+### 4. Create your `config` file
+
+A `config` file already exists in the project root (next to `start.sh`) as a placeholder template. Edit it in place with your real values:
+
+```yaml
+wazuh_api:
+ host: "https://localhost:55000"
+ username: "wazuh"
+ password: "YOUR_WAZUH_PASSWORD"
+ verify_ssl: false
+
+indexer:
+ url: "https://localhost:9200"
+ username: "admin"
+ password: "YOUR_INDEXER_PASSWORD"
+
+kibana:
+ username: "kibanaserver"
+ password: "YOUR_KIBANA_PASSWORD"
+
+ollama:
+ url: "http://localhost:11434"
+ model: "qwen3:1.7b"
+
+anthropic:
+ api_key: "" # optional, see step 5
+ model: "claude-sonnet-5"
+
+server:
+ host: "localhost" # replace with your server/lab IP if you're
+ # browsing to this box from another machine
+ backend_port: "8000"
+ frontend_port: "3000"
+```
+
+`wazuh_api.password`, `indexer.password`, and `kibana.password` are required. The backend refuses to start without them (see `backend/config.py`). Everything else has a working default. `server.host` only needs to change from `localhost` if you'll access the UI from a different machine than the one running it, set it to that machine's real IP/hostname.
+
+You need to replace the placeholder credentials according to your own environment in the following file:
+
+- `config` (`wazuh_api.password`, `indexer.password`, `kibana.password`, and optionally `anthropic.api_key`, `server.host`)
+
+### 5. (Optional) Enable the Claude brain for the Wazuh Agent
+
+The Agent works out of the box on Ollama alone. To also offer Claude as a brain option (recommended, far more reliable at multi-step tool use than a small local CPU model):
+
+1. Create an API key at [console.anthropic.com](https://console.anthropic.com)
+2. Set `anthropic.api_key` in your `config` file
+3. Restart the backend. `GET /agent/brains` will now report `claude.available: true`
+
+Never commit a real key. If one is ever pasted somewhere it shouldn't be (chat, a public PR, a log), revoke it immediately from the Anthropic Console and generate a new one.
+
+### 6. Start the app
+
+```bash
+./start.sh
+```
+
+This starts the backend (`backend_port`, default `8000`) and frontend (`frontend_port`, default `3000`). Navigate to `http://:` to access the portal.
+
+---
+
+## Adding Your Own Diagnostic Wizard (Use Case)
+
+Each wizard is a self-contained module in `backend/use_cases/` that walks the operator through checking and fixing one specific problem, reusing the shared step-machines in `flows/` and helpers in `utils/` rather than re-implementing diagnostic logic.
+
+1. **Write the flow.** Create `backend/use_cases/my_issue.py` exposing a function with this contract:
+
+ ```python
+ def my_issue_flow(user_choice=None, context=None):
+ context = context or {}
+ # ... inspect context.get("stage") to know where you are in a
+ # multi-step conversation, call your utils/flows helpers, and
+ # return the next step:
+ return {
+ "display": "What you show the user this turn",
+ "context": {"stage": "next_stage_name", **context},
+ }
+ ```
+
+ Look at `use_cases/mapping_issue.py` for a short, self-contained example, or `use_cases/dashboard_error.py` for one that chains through several `flows/` state machines.
+
+2. **Register it** in `backend/use_cases/__init__.py`:
+ - Add an entry to the `USE_CASES` list with a `name`, a list of trigger `phrases` (matched with fuzzy string matching, threshold 65), and a `handler` key.
+ - Add your `handler` to both `elif` chains inside `run_use_cases()`: one for starting a fresh match, one for continuing an in-progress flow (`context["stage"]` already set).
+
+That's it. The Assistant chat and `/assistant` endpoint pick it up automatically; no frontend changes needed.
+
+---
+
+## How the Wazuh Agent AI Works
+
+The Agent (`agent_engine.py`) runs a **plan, call tool, observe, repeat** loop, capped at 8 iterations per turn, with one hard rule: **read-only tools chain automatically, but the moment the agent wants to call a tool marked `mutating` in `agent_tools.py`, the loop stops and waits for your explicit approval** via `/agent/approve` before anything on the system actually changes.
+
+**Two interchangeable brains** (`agent_brain.py`), picked per-session from the UI's brain dropdown:
+
+- **Ollama** (local, offline): the small model here is too slow to drive a real tool-calling loop, so instead of letting it choose tools, the backend fetches all relevant data itself (service status, cluster health, matching knowledge-base issues) and hands Ollama one single prompt for one single answer. Fast, but can't execute fixes.
+- **Claude** (API): runs the full tool-calling agentic loop and can actually execute approved fixes. Requires `anthropic.api_key` in `config` (see Setup step 5); if it's blank, the Agent silently falls back to Ollama-only.
+
+**The tool registry** (`agent_tools.py`) is what the agent can see and call. Each entry wraps an existing, already-tested function from `utils/*.py`, described by a name, a JSON schema for its arguments, and a `mutating` flag. To give the agent a new capability, add an entry to the `TOOLS` list there; don't re-implement diagnostic logic inline, call into `utils/` the same way every other tool does.
+
+---
+
+## Knowledge Base Data
+
+`backend/knowledge/lgtm.db` and `backend/knowledge/lgtm_issues.json` are shipped with the repo so the Assistant/Copilot/Agent RAG features work immediately after cloning, with no separate build step. Both are an indexed snapshot sourced entirely from public Wazuh GitHub issues and discussions.
+
+That snapshot goes stale over time. To refresh it with more recent or more relevant community issues, manually re-run the sync scripts and replace the shipped file:
+
+```bash
+cd backend/knowledge
+python3 sync_lgtm_issues.py # rebuilds lgtm_issues.json from source
+python3 migrate_json_to_sqlite.py # rebuilds lgtm.db from lgtm_issues.json
+```
+
+Commit the regenerated `lgtm.db`/`lgtm_issues.json` in place of the old ones when you want the shipped knowledge base updated.
+
+---
+
+## Security Notes
+
+- `config` is tracked as a placeholder-only template (`YOUR_*_PASSWORD` values, `host: "localhost"`). Never commit it with real credentials filled in.
+- `backend/sessions/` and `backend/wizard_history/` (runtime-generated session/history files, created automatically on first use) are gitignored. Don't force-add real session data into git.
+- `.env`, if you use one, is gitignored the same way.
+- If any real credential ever ends up pasted in a chat log, commit, or public PR, treat it as compromised and rotate it immediately, even if you're not sure it was actually exposed.
diff --git a/integrations/wazuh-troubleshooting-tool/backend/agent_brain.py b/integrations/wazuh-troubleshooting-tool/backend/agent_brain.py
new file mode 100644
index 00000000..37437be7
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/agent_brain.py
@@ -0,0 +1,175 @@
+"""
+agent_brain.py
+Dual-brain tool-calling abstraction for the Wazuh Agent.
+
+Two interchangeable "brains" can drive the same agent loop:
+ - "ollama": local, offline, uses Ollama's OpenAI-style /api/chat tools param.
+ - "claude": the Anthropic API (Claude), used the same way Claude Agent SDK
+ tool-use loops work — generally far more reliable at multi-step tool use
+ than a small local model, at the cost of needing an API key + egress.
+
+agent_engine.py talks to this module only through step(), and passes/receives
+a brain-neutral conversation shape so it never needs to know which brain is
+active:
+
+ turns: list of
+ {"role": "user", "text": str}
+ {"role": "assistant", "text": str, "tool_calls": [{"id","name","arguments"}]}
+ {"role": "tool", "tool_call_id": str, "name": str, "content": str}
+
+ step() returns: {"text": str, "tool_calls": [{"id","name","arguments"}]}
+"""
+
+import json
+import requests
+
+from config import OLLAMA_URL, OLLAMA_MODEL, ANTHROPIC_API_KEY, ANTHROPIC_MODEL
+from copilot_engine import check_ollama_health, list_ollama_models
+
+try:
+ import anthropic
+except ImportError:
+ anthropic = None
+
+_anthropic_client = None
+
+
+def available_brains():
+ """What the frontend should offer as brain choices, and whether each is actually usable."""
+ health = check_ollama_health(OLLAMA_URL)
+ return {
+ "ollama": {
+ "available": health.get("ok", False),
+ "model": OLLAMA_MODEL,
+ "models": health.get("models") or list_ollama_models(OLLAMA_URL),
+ },
+ "claude": {
+ "available": bool(ANTHROPIC_API_KEY) and anthropic is not None,
+ "model": ANTHROPIC_MODEL,
+ "reason": "" if ANTHROPIC_API_KEY else "no anthropic.api_key configured",
+ },
+ }
+
+
+def _get_anthropic_client():
+ global _anthropic_client
+ if _anthropic_client is None:
+ _anthropic_client = anthropic.Anthropic(api_key=ANTHROPIC_API_KEY)
+ return _anthropic_client
+
+
+def step(turns, system_prompt, tools_openai, tools_anthropic, brain="ollama", model=None):
+ if brain == "claude":
+ if not ANTHROPIC_API_KEY or anthropic is None:
+ raise RuntimeError("Claude brain is not configured (missing anthropic.api_key or the anthropic package).")
+ return _step_claude(turns, system_prompt, tools_anthropic, model)
+ return _step_ollama(turns, system_prompt, tools_openai, model)
+
+
+# ─────────────────────────────────────────────────────────────────────────────
+# OLLAMA
+# ─────────────────────────────────────────────────────────────────────────────
+
+def _step_ollama(turns, system_prompt, tools, model):
+ model = model or OLLAMA_MODEL
+ messages = [{"role": "system", "content": system_prompt}]
+
+ for t in turns:
+ if t["role"] == "user":
+ messages.append({"role": "user", "content": t["text"]})
+ elif t["role"] == "assistant":
+ msg = {"role": "assistant", "content": t.get("text") or ""}
+ if t.get("tool_calls"):
+ msg["tool_calls"] = [
+ {"function": {"name": tc["name"], "arguments": tc["arguments"]}}
+ for tc in t["tool_calls"]
+ ]
+ messages.append(msg)
+ elif t["role"] == "tool":
+ messages.append({"role": "tool", "name": t["name"], "content": t["content"]})
+
+ payload = {
+ "model": model,
+ "messages": messages,
+ "tools": tools,
+ "stream": False,
+ "think": False,
+ # qwen3:1.7b generates at ~7 tokens/sec on this CPU (no GPU) - 2048 would
+ # let it ramble for minutes. Capped to keep answers focused and the wait tolerable.
+ "options": {"temperature": 0.2, "num_predict": 300},
+ }
+
+ resp = requests.post(f"{OLLAMA_URL}/api/chat", json=payload, timeout=300)
+ if resp.status_code != 200:
+ raise RuntimeError(f"Ollama returned HTTP {resp.status_code}: {resp.text[:300]}")
+
+ message = resp.json().get("message", {})
+ raw_calls = message.get("tool_calls") or []
+
+ tool_calls = []
+ for i, tc in enumerate(raw_calls):
+ fn = tc.get("function", {})
+ args = fn.get("arguments", {})
+ if isinstance(args, str):
+ try:
+ args = json.loads(args)
+ except (ValueError, TypeError):
+ args = {}
+ tool_calls.append({"id": f"call_{i}", "name": fn.get("name", ""), "arguments": args or {}})
+
+ return {"text": (message.get("content") or "").strip(), "tool_calls": tool_calls}
+
+
+# ─────────────────────────────────────────────────────────────────────────────
+# CLAUDE
+# ─────────────────────────────────────────────────────────────────────────────
+
+def _step_claude(turns, system_prompt, tools, model):
+ model = model or ANTHROPIC_MODEL
+ client = _get_anthropic_client()
+
+ messages = []
+ i = 0
+ while i < len(turns):
+ t = turns[i]
+ if t["role"] == "user":
+ messages.append({"role": "user", "content": t["text"]})
+ i += 1
+ elif t["role"] == "assistant":
+ content = []
+ if t.get("text"):
+ content.append({"type": "text", "text": t["text"]})
+ for tc in t.get("tool_calls", []):
+ content.append({"type": "tool_use", "id": tc["id"], "name": tc["name"], "input": tc["arguments"]})
+ messages.append({"role": "assistant", "content": content})
+ i += 1
+ elif t["role"] == "tool":
+ group = []
+ while i < len(turns) and turns[i]["role"] == "tool":
+ group.append({
+ "type": "tool_result",
+ "tool_use_id": turns[i]["tool_call_id"],
+ "content": turns[i]["content"],
+ })
+ i += 1
+ messages.append({"role": "user", "content": group})
+ else:
+ i += 1
+
+ resp = client.messages.create(
+ model=model,
+ max_tokens=2048,
+ system=system_prompt,
+ messages=messages,
+ tools=tools,
+ )
+
+ text_parts = []
+ tool_calls = []
+ for block in resp.content:
+ if block.type == "text":
+ text_parts.append(block.text)
+ elif block.type == "tool_use":
+ tool_calls.append({"id": block.id, "name": block.name, "arguments": block.input or {}})
+
+ return {"text": "\n".join(text_parts).strip(), "tool_calls": tool_calls}
diff --git a/integrations/wazuh-troubleshooting-tool/backend/agent_engine.py b/integrations/wazuh-troubleshooting-tool/backend/agent_engine.py
new file mode 100644
index 00000000..6a20f0a7
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/agent_engine.py
@@ -0,0 +1,357 @@
+"""
+agent_engine.py
+The agent loop: plan -> call tool -> observe -> repeat, with a hard
+propose-then-confirm gate in front of every mutating tool.
+
+Read-only tools chain automatically — the agent can run several checks in a
+row on its own. The moment it wants to call a tool marked "mutating" in
+agent_tools.py, the loop stops and hands control back to the caller (the
+/agent/approve endpoint) with the exact tool + arguments it wants to run.
+Nothing mutating ever executes without that round-trip.
+
+Sessions are kept in-memory only (module-level dict), same lifetime
+convention as copilot_engine.py's SESSION_ENV_CACHE - fine for a single
+backend process; conversations don't need to survive a restart.
+"""
+
+import json
+import uuid
+
+import agent_brain
+from agent_tools import TOOLS, TOOLS_BY_NAME, to_openai_schema, to_anthropic_schema, list_tools_metadata
+from utils import session_store
+from utils.lgtm_utils import find_relevant_issues, format_lgtm_context
+from utils.wazuh_docs import format_doc_context
+from copilot_engine import collect_environment_context, format_environment_context
+from config import (
+ WAZUH_API_URL, API_USERNAME, API_PASSWORD,
+ INDEXER_URL, INDEXER_USERNAME, INDEXER_PASSWORD,
+)
+
+MAX_ITERATIONS = 8
+
+TOOLS_OPENAI = to_openai_schema()
+TOOLS_ANTHROPIC = to_anthropic_schema()
+
+# ─────────────────────────────────────────────────────────────────────────────
+# OLLAMA RAG PATH — qwen3:1.7b is CPU-only and too slow to drive the agentic
+# tool-calling loop (even a trimmed schema took 180s+ with no result on this
+# hardware). Instead of asking it to decide when to call tools, we fetch
+# everything relevant directly in Python (fast, no LLM involved) and hand it
+# one single prompt for one single answer - no multi-turn loop, no tool
+# schema overhead. This trades away Ollama's ability to execute fixes itself;
+# Claude keeps the full tool-calling loop since it's fast enough for it.
+#
+# find_relevant_issues() searches the unified local SQLite+embeddings store
+# (lgtm.db) covering wazuh/community issues, wazuh/community discussions, AND
+# public wazuh/wazuh issues together - all pre-synced, so this is a fast local
+# lookup with no live GitHub calls at chat time (unlike Claude's tool-calling
+# path, which can still call search_public_wazuh_issues live for freshness).
+# ─────────────────────────────────────────────────────────────────────────────
+
+RAG_SYSTEM_PROMPT = """You are the Wazuh Troubleshooting Assistant. Answer using the live \
+system data and known-issue context provided below when it's relevant to the question. \
+Be direct and specific. If the provided context doesn't cover the question, answer from \
+general Wazuh expertise instead of saying you don't know. Keep answers focused and short.
+
+Never invent a specific documentation URL, deep link, or exact file path unless it appears \
+verbatim in the context provided below - a plausible-looking but wrong URL is worse than no \
+URL at all. If you want to point someone to documentation and don't have a verified link, \
+say "check the official Wazuh documentation at documentation.wazuh.com" instead of \
+fabricating a specific page path. Likewise, flag install/package commands as something to \
+verify against the official docs for their exact OS/version rather than presenting them as \
+guaranteed-correct - package names, repo setup steps, and syntax vary and you may not have \
+the current, exact sequence memorized correctly."""
+
+
+def _build_rag_context(user_text):
+ parts = []
+
+ lgtm_context = format_lgtm_context(find_relevant_issues(user_text))
+ if lgtm_context:
+ parts.append(lgtm_context)
+
+ try:
+ doc_context = format_doc_context(user_text)
+ if doc_context:
+ parts.append(doc_context)
+ except Exception:
+ pass # verified-doc fetch is best-effort - never block an answer on it
+
+ try:
+ env_ctx = collect_environment_context(
+ WAZUH_API_URL, API_USERNAME, API_PASSWORD,
+ INDEXER_URL, INDEXER_USERNAME, INDEXER_PASSWORD,
+ )
+ env_str = format_environment_context(env_ctx)
+ if env_str:
+ parts.append(env_str)
+ except Exception:
+ pass # live env snapshot is best-effort - never block an answer on it
+
+ return "\n\n".join(parts)
+
+
+def _run_ollama_rag(session, user_text, model):
+ system_prompt = RAG_SYSTEM_PROMPT
+ context = _build_rag_context(user_text)
+ if context:
+ system_prompt += "\n\n" + context
+
+ # Only the last few turns go to Ollama, not the whole growing history -
+ # this is RAG-grounded (context is rebuilt fresh every message from the
+ # knowledge base + live env), not memory-dependent, and qwen3:1.7b's
+ # prompt-processing time on this CPU scales with input length. Sending
+ # the full history would make every later message in a conversation
+ # progressively slower for no real benefit.
+ recent_turns = session["turns"][-6:]
+
+ step = agent_brain.step(recent_turns, system_prompt, [], [], brain="ollama", model=model)
+ session["turns"].append({"role": "assistant", "text": step["text"]})
+ return {"status": "final", "message": step["text"], "trace": []}
+
+SYSTEM_PROMPT = """You are the Wazuh Troubleshooting Agent, an autonomous diagnostic assistant for a \
+live Wazuh SIEM deployment (manager, indexer, dashboard, filebeat, endpoint agents).
+
+You have tools to inspect and fix the deployment directly instead of just describing what to do. Use them.
+
+Rules:
+1. Investigate before acting. Call read-only tools to confirm a root cause before proposing a fix - \
+don't jump straight to a fix from the symptom alone if a tool can confirm it first. For symptoms that could \
+be a known issue, check search_lgtm_knowledge_base and search_public_wazuh_issues early - a previously-seen \
+resolution is worth more than reasoning from scratch.
+2. Call tools one at a time when a later step depends on an earlier result; only call several at once \
+when they are genuinely independent checks.
+3. Every tool that changes system state (restarts a service, edits a config file, deletes data, installs \
+a package, etc.) automatically pauses for the user's explicit approval before it actually runs - you don't \
+need to ask permission in words, just call the tool once you've decided it's the right next step. The user \
+sees exactly what you're about to run, with its arguments, before it executes.
+4. Never call a mutating tool speculatively "just to see what happens" - only once your diagnosis actually \
+points to it as the fix.
+5. Some actions are irreversible (deleting indices) or heavy (full certificate regeneration) - prefer the \
+smallest fix that addresses the confirmed root cause, and say why you picked it.
+6. When you're done, give a short plain-language summary: what was wrong, what you checked, what you fixed \
+(or recommend if you stopped short of fixing it), and whether the issue looks resolved.
+7. When you're not calling a tool, you're either asking the user one concise clarifying question or giving \
+your final answer - keep both short and to the point.
+"""
+
+SESSIONS = {}
+
+
+def _new_session():
+ return {
+ "turns": [],
+ "pending_batch": None,
+ "pending_batch_index": 0,
+ "pending_action": None,
+ "brain": "ollama",
+ "model": None,
+ "iterations": 0,
+ }
+
+
+def _get_session(session_id):
+ if session_id not in SESSIONS:
+ SESSIONS[session_id] = _new_session()
+ persisted = session_store.load_session(session_id)
+ if persisted:
+ SESSIONS[session_id]["turns"] = persisted
+ return SESSIONS[session_id]
+
+
+def _to_text(result):
+ if isinstance(result, str):
+ return result[:8000]
+ try:
+ return json.dumps(result, default=str)[:8000]
+ except Exception:
+ return str(result)[:8000]
+
+
+def _append_tool_result(session, tc, result):
+ session["turns"].append({
+ "role": "tool",
+ "tool_call_id": tc["id"],
+ "name": tc["name"],
+ "content": _to_text(result),
+ })
+
+
+def _execute_tool(tool, tc):
+ try:
+ return tool["fn"](**(tc["arguments"] or {}))
+ except TypeError as e:
+ return {"error": f"invalid arguments for {tc['name']}: {e}"}
+ except Exception as e:
+ return {"error": str(e)}
+
+
+def _process_batch(session, trace):
+ """Run session['pending_batch'] from session['pending_batch_index'] onward.
+ Returns (tool_call, tool) if it had to pause on a mutating call, else None
+ once the whole batch has executed."""
+ batch = session["pending_batch"]
+ idx = session["pending_batch_index"]
+
+ while idx < len(batch):
+ tc = batch[idx]
+ tool = TOOLS_BY_NAME.get(tc["name"])
+
+ if tool is None:
+ error = {"error": f"unknown tool '{tc['name']}'"}
+ _append_tool_result(session, tc, error)
+ trace.append({"type": "tool_result", "tool": tc["name"], "error": True, "result": error})
+ idx += 1
+ continue
+
+ if tool["mutating"]:
+ session["pending_batch_index"] = idx
+ return tc, tool
+
+ result = _execute_tool(tool, tc)
+ trace.append({"type": "tool_call", "tool": tc["name"], "arguments": tc["arguments"], "mutating": False})
+ trace.append({"type": "tool_result", "tool": tc["name"], "result": result})
+ _append_tool_result(session, tc, result)
+ idx += 1
+
+ session["pending_batch"] = None
+ session["pending_batch_index"] = 0
+ return None
+
+
+def _pending_action_payload(tc, tool):
+ return {
+ "tool_call_id": tc["id"],
+ "tool": tc["name"],
+ "arguments": tc["arguments"],
+ "description": tool["description"],
+ "risk": tool.get("risk", "medium"),
+ }
+
+
+def _run_loop(session):
+ trace = []
+
+ while session["iterations"] < MAX_ITERATIONS:
+ if session["pending_batch"]:
+ paused = _process_batch(session, trace)
+ if paused:
+ tc, tool = paused
+ session["pending_action"] = _pending_action_payload(tc, tool)
+ return {"status": "awaiting_approval", "pending_action": session["pending_action"], "trace": trace}
+
+ session["iterations"] += 1
+
+ step = agent_brain.step(
+ session["turns"], SYSTEM_PROMPT, TOOLS_OPENAI, TOOLS_ANTHROPIC,
+ brain=session["brain"], model=session.get("model"),
+ )
+
+ if not step["tool_calls"]:
+ session["turns"].append({"role": "assistant", "text": step["text"]})
+ return {"status": "final", "message": step["text"], "trace": trace}
+
+ session["turns"].append({"role": "assistant", "text": step["text"], "tool_calls": step["tool_calls"]})
+ session["pending_batch"] = step["tool_calls"]
+ session["pending_batch_index"] = 0
+
+ return {
+ "status": "final",
+ "message": "Stopped after too many investigation steps in a row — ask me to continue, or narrow the question.",
+ "trace": trace,
+ }
+
+
+def handle_message(session_id, user_text, brain="ollama", model=None):
+ session = _get_session(session_id)
+
+ if session.get("pending_action"):
+ return {
+ "status": "error",
+ "message": "There's an action awaiting your approval — approve or reject it before sending a new message.",
+ "pending_action": session["pending_action"],
+ }
+
+ session["brain"] = brain if brain in ("ollama", "claude") else "ollama"
+ session["model"] = model
+ session["iterations"] = 0
+ session["turns"].append({"role": "user", "text": user_text})
+
+ if session["brain"] == "ollama":
+ result = _run_ollama_rag(session, user_text, model)
+ else:
+ result = _run_loop(session)
+ result["session_id"] = session_id
+ session_store.save_session(session_id, session["turns"], brain=session.get("brain"))
+ return result
+
+
+def handle_approve(session_id, approve, edited_arguments=None):
+ session = SESSIONS.get(session_id)
+ if not session or not session.get("pending_action"):
+ return {"status": "error", "message": "No pending action for this session."}
+
+ trace = []
+ tc = session["pending_batch"][session["pending_batch_index"]]
+ tool = TOOLS_BY_NAME[tc["name"]]
+
+ if approve:
+ args = edited_arguments if edited_arguments is not None else tc["arguments"]
+ exec_tc = {"id": tc["id"], "name": tc["name"], "arguments": args}
+ result = _execute_tool(tool, exec_tc)
+ trace.append({"type": "tool_call", "tool": tc["name"], "arguments": args, "mutating": True})
+ trace.append({"type": "tool_result", "tool": tc["name"], "result": result})
+ _append_tool_result(session, tc, result)
+ else:
+ declined = {"error": "User declined this action. Choose a different approach, ask a clarifying question, or stop here."}
+ _append_tool_result(session, tc, declined)
+ trace.append({"type": "tool_result", "tool": tc["name"], "result": "declined by user"})
+
+ session["pending_batch_index"] += 1
+ session["pending_action"] = None
+ session["iterations"] = 0
+
+ result = _run_loop(session)
+ result["trace"] = trace + result["trace"]
+ result["session_id"] = session_id
+ session_store.save_session(session_id, session["turns"], brain=session.get("brain"))
+ return result
+
+
+def reset_session(session_id):
+ SESSIONS.pop(session_id, None)
+ return {"status": "reset"}
+
+
+def get_tools_metadata():
+ return list_tools_metadata()
+
+
+def list_session_history():
+ return session_store.list_sessions()
+
+
+def resume_session(chat_id):
+ """Load a persisted chat back into memory so sending a new message
+ continues it, and return its turns for the frontend to replay."""
+ turns = session_store.load_session(chat_id)
+ if turns is None:
+ return None
+ session = _new_session()
+ session["turns"] = turns
+ SESSIONS[chat_id] = session
+ return turns
+
+
+def delete_session_history(chat_id):
+ session_store.delete_session(chat_id)
+ SESSIONS.pop(chat_id, None)
+
+
+def rename_session_history(chat_id, new_title):
+ return session_store.rename_session(chat_id, new_title)
+
+
+def get_brains():
+ return agent_brain.available_brains()
diff --git a/integrations/wazuh-troubleshooting-tool/backend/agent_tools.py b/integrations/wazuh-troubleshooting-tool/backend/agent_tools.py
new file mode 100644
index 00000000..10163f89
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/agent_tools.py
@@ -0,0 +1,651 @@
+"""
+agent_tools.py
+Tool registry for the Wazuh Agent (agentic troubleshooting loop).
+
+Every tool wraps an already-existing, already-tested function from
+utils/*.py or use_cases/flows — nothing here re-implements diagnostic or
+fix logic. This module only describes each one (name, JSON schema, and
+whether it mutates the system) so an LLM tool-calling loop (agent_engine.py)
+can select and invoke them safely.
+
+Tools are split into two trust tiers:
+ - READ-ONLY tools execute immediately, no approval needed.
+ - MUTATING tools (restarts services, edits config, deletes data, etc.)
+ always pause the agent loop for explicit user approval first.
+"""
+
+from executor import run_command
+from utils.service_utils import get_service_status, restart_service_and_wait
+from utils.cluster_utils import get_cluster_health, get_write_blocks, clear_write_blocks
+from utils.index_utils import list_indices, check_most_recent_index, select_indices_by_age, delete_indices
+from utils.pipeline_utils import (
+ get_agent_status,
+ check_manager_config,
+ get_alerts_json_status,
+ check_cluster_shards,
+ check_alert_indices,
+)
+from utils.agent_utils import list_active_agents, restart_agent as _restart_agent, restart_all_agents as _restart_all_agents
+from utils.manager_config_utils import set_log_alert_level, enable_jsonout_output
+from utils.manager_log_utils import get_manager_log_errors, get_manager_disk_usage
+from utils.log_handler import LogHandler
+from utils.filebeat_utils import (
+ run_filebeat_output_test,
+ get_filebeat_log_errors,
+ fix_unsupported_filebeat_version as _fix_unsupported_filebeat_version,
+)
+from utils.replica_utils import set_replica_count
+from utils.shard_utils import get_unassigned_shards, explain_allocation
+from utils.fix_engine import FixEngine
+from utils.cert_utils import regenerate_and_redeploy_certs as _regenerate_and_redeploy_certs
+from utils.default_route_utils import set_default_route
+from utils.lgtm_utils import find_relevant_issues
+from utils.public_repo_search import search_public_issues, search_public_discussions
+from utils.wazuh_docs import find_matching_doc, fetch_doc_content
+
+KNOWN_SERVICES = ["wazuh-indexer", "wazuh-manager", "wazuh-dashboard", "filebeat"]
+
+
+# ─────────────────────────────────────────────────────────────────────────────
+# Combined helpers — a few mutating fixes are naturally "edit + restart" as a
+# single logical action in the existing wizards, so they stay that way here
+# too (one approval, not two).
+# ─────────────────────────────────────────────────────────────────────────────
+
+def _fix_manager_log_alert_level():
+ new_value = set_log_alert_level(3)
+ status = restart_service_and_wait("wazuh-manager")
+ return {"log_alert_level": new_value, "manager_status": status}
+
+
+def _fix_manager_jsonout_output():
+ enabled = enable_jsonout_output()
+ status = restart_service_and_wait("wazuh-manager")
+ return {"jsonout_output_enabled": enabled, "manager_status": status}
+
+
+def _fix_dashboard_default_route():
+ new_value = set_default_route()
+ status = restart_service_and_wait("wazuh-dashboard")
+ return {"default_route": new_value, "dashboard_status": status}
+
+
+def _check_all_services():
+ return {svc: get_service_status(svc) for svc in KNOWN_SERVICES}
+
+
+def _get_cluster_health():
+ parsed, raw = get_cluster_health()
+ return parsed if parsed is not None else {"error": "could not reach indexer", "raw": raw}
+
+
+def _search_lgtm_knowledge_base(query):
+ issues = find_relevant_issues(query)
+ if not issues:
+ return {"matches": [], "note": "no matching resolved issue found in the internal knowledge base"}
+ return {
+ "matches": [
+ {
+ "number": i["number"],
+ "title": i["title"],
+ "resolution": "\n".join(i.get("comments", []) + i.get("external_community", []))[:1500],
+ }
+ for i in issues
+ ]
+ }
+
+
+def _search_public_wazuh_repo(query):
+ issues = search_public_issues(query)
+ discussions = search_public_discussions(query)
+ return {
+ "issues": [
+ {
+ "number": i["number"],
+ "title": i["title"],
+ "url": i["url"],
+ "discussion": "\n".join(i.get("comments", []))[:1000],
+ }
+ for i in issues
+ ],
+ "discussions": [
+ {"number": d["number"], "title": d["title"], "url": d["url"], "answer": d.get("answer", "")}
+ for d in discussions
+ ],
+ }
+
+
+def _fetch_verified_wazuh_doc(query):
+ key, doc = find_matching_doc(query)
+ if not doc:
+ return {"found": False, "note": "No verified doc page matches this topic yet."}
+ content = fetch_doc_content(doc["url"])
+ if not content:
+ return {"found": False, "note": f"Matched topic '{key}' but the page fetch failed."}
+ return {"found": True, "url": doc["url"], "title": doc["title"], "content": content[:3000]}
+
+
+# ─────────────────────────────────────────────────────────────────────────────
+# TOOL REGISTRY
+# ─────────────────────────────────────────────────────────────────────────────
+# risk: "low" | "medium" | "high" — shown as a badge in the approval UI.
+
+TOOLS = [
+ # ── READ-ONLY: services & system ────────────────────────────────────
+ {
+ "name": "check_all_services",
+ "description": "Get systemd status (active/inactive/failed) for wazuh-indexer, wazuh-manager, wazuh-dashboard and filebeat in one call.",
+ "mutating": False,
+ "parameters": {"type": "object", "properties": {}},
+ "fn": lambda: _check_all_services(),
+ },
+ {
+ "name": "check_service_status",
+ "description": "Get the systemd status of a single named service.",
+ "mutating": False,
+ "parameters": {
+ "type": "object",
+ "properties": {"service": {"type": "string", "enum": KNOWN_SERVICES}},
+ "required": ["service"],
+ },
+ "fn": lambda service: get_service_status(service),
+ },
+ {
+ "name": "check_disk_usage",
+ "description": "Run `df -h` on the host. Use when investigating slow/failed services or unassigned shards, since a full disk is a common silent cause.",
+ "mutating": False,
+ "parameters": {"type": "object", "properties": {}},
+ "fn": lambda: FixEngine.check_disk(),
+ },
+
+ # ── READ-ONLY: IPs & certs ───────────────────────────────────────────
+ {
+ "name": "check_ip_configuration",
+ "description": "Compare the indexer IP configured at install time (config.yml) against what's actually configured in the indexer and dashboard configs. Mismatches are a common cause of the dashboard failing to reach the indexer.",
+ "mutating": False,
+ "parameters": {"type": "object", "properties": {}},
+ "fn": lambda: FixEngine.compare_ips(),
+ },
+ {
+ "name": "check_indexer_cert_paths",
+ "description": "Check whether the TLS cert/key/CA files referenced in opensearch.yml actually exist on disk.",
+ "mutating": False,
+ "parameters": {"type": "object", "properties": {}},
+ "fn": lambda: FixEngine.check_indexer_cert_paths(),
+ },
+ {
+ "name": "check_dashboard_cert_paths",
+ "description": "Check whether the TLS cert/key/CA files referenced in opensearch_dashboards.yml actually exist on disk.",
+ "mutating": False,
+ "parameters": {"type": "object", "properties": {}},
+ "fn": lambda: FixEngine.check_dashboard_cert_paths(),
+ },
+ {
+ "name": "check_cert_permissions",
+ "description": "Check file/directory permissions and ownership on the dashboard's certs directory.",
+ "mutating": False,
+ "parameters": {"type": "object", "properties": {}},
+ "fn": lambda: FixEngine.check_cert_permissions(),
+ },
+ {
+ "name": "check_jvm_heap",
+ "description": "Check the wazuh-indexer JVM heap size (jvm.options) against the recommended value (50% of host RAM). Undersized heap is a common cause of indexer crashes/slowness.",
+ "mutating": False,
+ "parameters": {"type": "object", "properties": {}},
+ "fn": lambda: FixEngine.check_jvm_heap(),
+ },
+
+ # ── READ-ONLY: manager / agents / pipeline ───────────────────────────
+ {
+ "name": "check_manager_config",
+ "description": "Check ossec.conf's log_alert_level and jsonout_output settings — misconfiguration here silently drops alerts before they're ever written to alerts.json.",
+ "mutating": False,
+ "parameters": {"type": "object", "properties": {}},
+ "fn": lambda: check_manager_config(),
+ },
+ {
+ "name": "get_manager_log_errors",
+ "description": "Tail the manager's ossec.log filtered to error/warn lines.",
+ "mutating": False,
+ "parameters": {"type": "object", "properties": {"lines": {"type": "integer", "description": "how many recent lines to scan, default 200"}}},
+ "fn": lambda lines=200: get_manager_log_errors(lines),
+ },
+ {
+ "name": "get_manager_disk_usage",
+ "description": "Disk usage for /var/ossec (the manager's data directory).",
+ "mutating": False,
+ "parameters": {"type": "object", "properties": {}},
+ "fn": lambda: get_manager_disk_usage(),
+ },
+ {
+ "name": "get_agent_status",
+ "description": "Run agent_control -l, optionally filtered to one agent by name or ID, to check if it's Active.",
+ "mutating": False,
+ "parameters": {"type": "object", "properties": {"identifier": {"type": "string", "description": "agent name or ID to look up; omit to just check whether ANY agent is active"}}},
+ "fn": lambda identifier=None: get_agent_status(identifier),
+ },
+ {
+ "name": "list_active_agents",
+ "description": "List every currently-active endpoint agent (id, name), excluding the manager's own local agent 000.",
+ "mutating": False,
+ "parameters": {"type": "object", "properties": {}},
+ "fn": lambda: list_active_agents(),
+ },
+ {
+ "name": "get_alerts_json_status",
+ "description": "Check whether the manager is actively writing new alerts to alerts.json (the file Filebeat reads). Staleness here means the pipeline is stalled at the manager, before Filebeat/indexer are even involved.",
+ "mutating": False,
+ "parameters": {"type": "object", "properties": {}},
+ "fn": lambda: get_alerts_json_status(),
+ },
+
+ # ── READ-ONLY: filebeat ──────────────────────────────────────────────
+ {
+ "name": "run_filebeat_output_test",
+ "description": "Run `filebeat test output` to check Filebeat's connectivity to the indexer.",
+ "mutating": False,
+ "parameters": {"type": "object", "properties": {}},
+ "fn": lambda: run_filebeat_output_test(),
+ },
+ {
+ "name": "get_filebeat_log_errors",
+ "description": "Tail Filebeat's log filtered to error/warn lines.",
+ "mutating": False,
+ "parameters": {"type": "object", "properties": {"lines": {"type": "integer", "description": "how many recent lines to scan, default 200"}}},
+ "fn": lambda lines=200: get_filebeat_log_errors(lines),
+ },
+
+ # ── READ-ONLY: indexer / cluster / indices ───────────────────────────
+ {
+ "name": "get_cluster_health",
+ "description": "GET /_cluster/health from the indexer (status green/yellow/red, node count, shard counts).",
+ "mutating": False,
+ "parameters": {"type": "object", "properties": {}},
+ "fn": lambda: _get_cluster_health(),
+ },
+ {
+ "name": "check_cluster_shards",
+ "description": "Cluster health plus, if not green, the detail of *why* shards are unassigned (disk watermark, no replica node, etc).",
+ "mutating": False,
+ "parameters": {"type": "object", "properties": {}},
+ "fn": lambda: check_cluster_shards(),
+ },
+ {
+ "name": "get_unassigned_shards",
+ "description": "List every currently-unassigned shard with its index, shard number and reason.",
+ "mutating": False,
+ "parameters": {"type": "object", "properties": {}},
+ "fn": lambda: get_unassigned_shards(),
+ },
+ {
+ "name": "explain_shard_allocation",
+ "description": "Get OpenSearch's own explanation for why one specific shard is unassigned.",
+ "mutating": False,
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "index": {"type": "string"},
+ "shard": {"type": "integer"},
+ "primary": {"type": "boolean", "description": "true for the primary copy, false for a replica"},
+ },
+ "required": ["index", "shard"],
+ },
+ "fn": lambda index, shard, primary=False: explain_allocation(index, shard, primary),
+ },
+ {
+ "name": "get_cluster_write_blocks",
+ "description": "Check for cluster-wide read_only/create_index blocks. These silently prevent ALL writes/new indices cluster-wide even when _cluster/health looks fine.",
+ "mutating": False,
+ "parameters": {"type": "object", "properties": {}},
+ "fn": lambda: get_write_blocks(),
+ },
+ {
+ "name": "list_alert_indices",
+ "description": "List wazuh-alerts-* indices with health/status/doc count/size.",
+ "mutating": False,
+ "parameters": {"type": "object", "properties": {"pattern": {"type": "string", "description": "index pattern, default 'wazuh-alerts-*'"}}},
+ "fn": lambda pattern="wazuh-alerts-*": list_indices(pattern),
+ },
+ {
+ "name": "check_most_recent_alert_index",
+ "description": "Find the newest wazuh-alerts-* index by date and report how many days old it is — the key check for 'no alerts are showing today'.",
+ "mutating": False,
+ "parameters": {"type": "object", "properties": {"pattern": {"type": "string", "description": "index pattern, default 'wazuh-alerts-*'"}}},
+ "fn": lambda pattern="wazuh-alerts-*": check_most_recent_index(pattern),
+ },
+ {
+ "name": "check_alert_indices_today",
+ "description": "Confirm wazuh-alerts-* indices exist and one matching TODAY's date is present.",
+ "mutating": False,
+ "parameters": {"type": "object", "properties": {}},
+ "fn": lambda: check_alert_indices(),
+ },
+ {
+ "name": "preview_indices_older_than",
+ "description": "Preview which indices would be affected by an age-based cleanup, WITHOUT deleting anything. Always call this before proposing delete_old_indices, and show the exact list to the user.",
+ "mutating": False,
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "pattern": {"type": "string", "description": "index pattern, default 'wazuh-alerts-*'"},
+ "older_than_days": {"type": "integer"},
+ },
+ "required": ["older_than_days"],
+ },
+ "fn": lambda older_than_days, pattern="wazuh-alerts-*": select_indices_by_age(pattern, older_than_days=older_than_days),
+ },
+
+ # ── READ-ONLY: knowledge base ────────────────────────────────────────
+ {
+ "name": "search_lgtm_knowledge_base",
+ "description": (
+ "Search internally-reviewed, previously-resolved Wazuh issues (marked LGTM by the community "
+ "team) for one matching the current symptom. Call this before proposing a fix for anything "
+ "that looks like it could be a known, previously-seen issue rather than guessing from scratch."
+ ),
+ "mutating": False,
+ "parameters": {
+ "type": "object",
+ "properties": {"query": {"type": "string", "description": "the symptom or error message to search for"}},
+ "required": ["query"],
+ },
+ "fn": lambda query: _search_lgtm_knowledge_base(query),
+ },
+ {
+ "name": "search_public_wazuh_issues",
+ "description": "Live-search the public wazuh/wazuh GitHub repo (issues + discussions) for similar reported problems and how they were resolved.",
+ "mutating": False,
+ "parameters": {
+ "type": "object",
+ "properties": {"query": {"type": "string", "description": "the symptom or error message to search for"}},
+ "required": ["query"],
+ },
+ "fn": lambda query: _search_public_wazuh_repo(query),
+ },
+ {
+ "name": "fetch_verified_wazuh_doc",
+ "description": (
+ "Fetch a real, human-verified official Wazuh documentation page matching the query "
+ "(e.g. agent installation, Wazuh Cloud trial sign-up). Always prefer this over reciting "
+ "a documentation URL from memory - a wrong-but-plausible-looking URL is worse than none, "
+ "and this only ever returns pages someone actually checked are real."
+ ),
+ "mutating": False,
+ "parameters": {
+ "type": "object",
+ "properties": {"query": {"type": "string", "description": "what the user is trying to do, e.g. 'install a linux agent'"}},
+ "required": ["query"],
+ },
+ "fn": lambda query: _fetch_verified_wazuh_doc(query),
+ },
+
+ # ── READ-ONLY: logs ───────────────────────────────────────────────────
+ {
+ "name": "get_indexer_logs",
+ "description": "Recent wazuh-indexer cluster log, filtered to error/warn.",
+ "mutating": False,
+ "parameters": {"type": "object", "properties": {"hours": {"type": "integer", "description": "how many hours back, default 2"}}},
+ "fn": lambda hours=2: LogHandler.clean_logs(LogHandler.get_indexer_logs(hours)),
+ },
+ {
+ "name": "get_dashboard_logs",
+ "description": "Recent wazuh-dashboard journal log, filtered to error/warn.",
+ "mutating": False,
+ "parameters": {"type": "object", "properties": {"hours": {"type": "integer", "description": "how many hours back, default 2"}}},
+ "fn": lambda hours=2: LogHandler.clean_logs(LogHandler.get_dashboard_logs(hours)),
+ },
+ # ── MUTATING: services ───────────────────────────────────────────────
+ {
+ "name": "restart_service",
+ "description": "Restart a systemd service and wait for it to come back active.",
+ "mutating": True,
+ "risk": "medium",
+ "parameters": {
+ "type": "object",
+ "properties": {"service": {"type": "string", "enum": KNOWN_SERVICES}},
+ "required": ["service"],
+ },
+ "fn": lambda service: restart_service_and_wait(service),
+ },
+
+ # ── MUTATING: IP / certs ─────────────────────────────────────────────
+ {
+ "name": "fix_indexer_ip",
+ "description": "Rewrite network.host in opensearch.yml to match the install-time control IP, then restart wazuh-indexer.",
+ "mutating": True,
+ "risk": "medium",
+ "parameters": {"type": "object", "properties": {"control_ip": {"type": "string"}}, "required": ["control_ip"]},
+ "fn": lambda control_ip: FixEngine.fix_indexer_ip(control_ip),
+ },
+ {
+ "name": "fix_dashboard_ip",
+ "description": "Rewrite the indexer host URL in opensearch_dashboards.yml to match the control IP, then restart wazuh-dashboard.",
+ "mutating": True,
+ "risk": "medium",
+ "parameters": {"type": "object", "properties": {"control_ip": {"type": "string"}}, "required": ["control_ip"]},
+ "fn": lambda control_ip: FixEngine.fix_dashboard_ip(control_ip),
+ },
+ {
+ "name": "fix_indexer_cert_paths",
+ "description": "Auto-detect the cert/key/CA files actually present in /etc/wazuh-indexer/certs and rewrite opensearch.yml to point at them, then restart wazuh-indexer.",
+ "mutating": True,
+ "risk": "medium",
+ "parameters": {"type": "object", "properties": {}},
+ "fn": lambda: FixEngine.fix_indexer_cert_paths(),
+ },
+ {
+ "name": "fix_dashboard_cert_paths",
+ "description": "Auto-detect the cert/key/CA files actually present in /etc/wazuh-dashboard/certs and rewrite opensearch_dashboards.yml to point at them, then restart wazuh-dashboard.",
+ "mutating": True,
+ "risk": "medium",
+ "parameters": {"type": "object", "properties": {}},
+ "fn": lambda: FixEngine.fix_dashboard_cert_paths(),
+ },
+ {
+ "name": "fix_cert_permissions",
+ "description": "chmod/chown the dashboard's certs directory back to the expected 500/400 wazuh-dashboard ownership.",
+ "mutating": True,
+ "risk": "low",
+ "parameters": {"type": "object", "properties": {}},
+ "fn": lambda: FixEngine.fix_cert_permissions(),
+ },
+ {
+ "name": "regenerate_and_redeploy_certs",
+ "description": (
+ "Full TLS cert regeneration: runs wazuh-certs-tool.sh and redeploys fresh certs to the indexer, "
+ "Filebeat and dashboard, then restarts all three services. Use only after simpler cert-path/permission "
+ "fixes have already been ruled out or failed — this is the heaviest, slowest cert fix available."
+ ),
+ "mutating": True,
+ "risk": "high",
+ "parameters": {"type": "object", "properties": {}},
+ "fn": lambda: _regenerate_and_redeploy_certs(),
+ },
+
+ # ── MUTATING: dashboard config ───────────────────────────────────────
+ {
+ "name": "fix_dashboard_default_route",
+ "description": "Set uiSettings.overrides.defaultRoute to /app/wz-home in opensearch_dashboards.yml (fixes 'Application Not Found' after an upgrade), then restart wazuh-dashboard.",
+ "mutating": True,
+ "risk": "low",
+ "parameters": {"type": "object", "properties": {}},
+ "fn": lambda: _fix_dashboard_default_route(),
+ },
+
+ # ── MUTATING: indexer heap ───────────────────────────────────────────
+ {
+ "name": "fix_jvm_heap",
+ "description": "Set -Xms/-Xmx in the indexer's jvm.options to the given size (GB), then restart wazuh-indexer.",
+ "mutating": True,
+ "risk": "medium",
+ "parameters": {"type": "object", "properties": {"heap_gb": {"type": "integer"}}, "required": ["heap_gb"]},
+ "fn": lambda heap_gb: FixEngine.fix_jvm_heap(heap_gb),
+ },
+
+ # ── MUTATING: manager config ─────────────────────────────────────────
+ {
+ "name": "fix_manager_log_alert_level",
+ "description": "Set ossec.conf's log_alert_level to 3 (so alerts stop being silently dropped) and restart wazuh-manager.",
+ "mutating": True,
+ "risk": "low",
+ "parameters": {"type": "object", "properties": {}},
+ "fn": lambda: _fix_manager_log_alert_level(),
+ },
+ {
+ "name": "fix_manager_jsonout_output",
+ "description": "Set ossec.conf's jsonout_output to yes (required for alerts.json to be written) and restart wazuh-manager.",
+ "mutating": True,
+ "risk": "low",
+ "parameters": {"type": "object", "properties": {}},
+ "fn": lambda: _fix_manager_jsonout_output(),
+ },
+
+ # ── MUTATING: agents ──────────────────────────────────────────────────
+ {
+ "name": "restart_agent",
+ "description": "Remotely restart one currently-Active endpoint agent by ID.",
+ "mutating": True,
+ "risk": "low",
+ "parameters": {"type": "object", "properties": {"agent_id": {"type": "string"}}, "required": ["agent_id"]},
+ "fn": lambda agent_id: _restart_agent(agent_id),
+ },
+ {
+ "name": "restart_all_agents",
+ "description": "Remotely restart EVERY currently-active endpoint agent.",
+ "mutating": True,
+ "risk": "medium",
+ "parameters": {"type": "object", "properties": {}},
+ "fn": lambda: _restart_all_agents(),
+ },
+
+ # ── MUTATING: filebeat ────────────────────────────────────────────────
+ {
+ "name": "fix_unsupported_filebeat_version",
+ "description": "Deploy the Wazuh Filebeat module + alerts template, and reinstall Filebeat-OSS 7.10.2 if the version is still wrong. Use when classify_filebeat_failure-style symptoms point at an unsupported version.",
+ "mutating": True,
+ "risk": "medium",
+ "parameters": {"type": "object", "properties": {}},
+ "fn": lambda: _fix_unsupported_filebeat_version(),
+ },
+
+ # ── MUTATING: cluster / indices ──────────────────────────────────────
+ {
+ "name": "clear_cluster_write_blocks",
+ "description": "Clear the given cluster.blocks.* settings that are silently preventing writes/new indices cluster-wide.",
+ "mutating": True,
+ "risk": "medium",
+ "parameters": {
+ "type": "object",
+ "properties": {"block_names": {"type": "array", "items": {"type": "string"}}},
+ "required": ["block_names"],
+ },
+ "fn": lambda block_names: clear_write_blocks(block_names),
+ },
+ {
+ "name": "set_index_replica_count",
+ "description": "Update number_of_replicas on an index/pattern. Applies immediately to existing indices, no reindex needed.",
+ "mutating": True,
+ "risk": "low",
+ "parameters": {
+ "type": "object",
+ "properties": {"index_pattern": {"type": "string"}, "replicas": {"type": "integer"}},
+ "required": ["index_pattern", "replicas"],
+ },
+ "fn": lambda index_pattern, replicas: set_replica_count(index_pattern, replicas),
+ },
+ {
+ "name": "delete_old_indices",
+ "description": (
+ "PERMANENTLY delete the given indices. IRREVERSIBLE — there is no undo. Always call "
+ "preview_indices_older_than first and pass exactly the index names it returned; never guess names."
+ ),
+ "mutating": True,
+ "risk": "high",
+ "parameters": {
+ "type": "object",
+ "properties": {"index_names": {"type": "array", "items": {"type": "string"}}},
+ "required": ["index_names"],
+ },
+ "fn": lambda index_names: delete_indices(index_names),
+ },
+
+ # ── MUTATING: ad-hoc escape hatch ─────────────────────────────────────
+ {
+ "name": "run_shell_command",
+ "description": (
+ "Run an arbitrary shell command for ad-hoc investigation when no other tool fits "
+ "(e.g. 'grep', 'cat', 'journalctl', 'ps aux'). Prefer a specific tool above whenever one "
+ "applies. This always requires approval — even for a command that only reads state — "
+ "because it cannot be verified as safe the way the named tools above can."
+ ),
+ "mutating": True,
+ "risk": "medium",
+ "parameters": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]},
+ "fn": lambda command: run_command(command),
+ },
+]
+
+TOOLS_BY_NAME = {t["name"]: t for t in TOOLS}
+
+
+# qwen3:1.7b runs CPU-only on modest hardware — passing it all 48 tool
+# schemas makes the prompt so long it can take minutes just to start
+# answering. This is the reduced set it gets instead: the most common
+# read-only diagnostics, the most common single-step fixes, and all three
+# knowledge-base tools (the whole point of that feature). Claude has no
+# such constraint and keeps the full TOOLS list via to_anthropic_schema().
+OLLAMA_TOOL_NAMES = {
+ # read-only diagnostics
+ "check_all_services", "check_service_status", "check_disk_usage",
+ "check_manager_config", "get_manager_log_errors", "get_manager_disk_usage",
+ "get_agent_status", "list_active_agents", "get_alerts_json_status",
+ "run_filebeat_output_test", "get_filebeat_log_errors",
+ "get_cluster_health", "check_cluster_shards", "get_unassigned_shards",
+ "check_most_recent_alert_index", "check_alert_indices_today",
+ "get_indexer_logs", "get_dashboard_logs",
+ # knowledge base — always available regardless of brain
+ "search_lgtm_knowledge_base", "search_public_wazuh_issues", "fetch_verified_wazuh_doc",
+ # the handful of most common single-step fixes
+ "restart_service", "fix_manager_log_alert_level", "fix_manager_jsonout_output", "restart_agent",
+}
+
+
+def to_openai_schema():
+ """Ollama's /api/chat 'tools' param — the reduced OLLAMA_TOOL_NAMES subset only."""
+ return [
+ {
+ "type": "function",
+ "function": {
+ "name": t["name"],
+ "description": t["description"],
+ "parameters": t["parameters"],
+ },
+ }
+ for t in TOOLS if t["name"] in OLLAMA_TOOL_NAMES
+ ]
+
+
+def to_anthropic_schema():
+ """Claude's 'tools' param — the full TOOLS list, no reduction needed."""
+ return [
+ {
+ "name": t["name"],
+ "description": t["description"],
+ "input_schema": t["parameters"],
+ }
+ for t in TOOLS
+ ]
+
+
+def list_tools_metadata():
+ """For the frontend's tool-transparency panel — no fn/lambdas."""
+ return [
+ {
+ "name": t["name"],
+ "description": t["description"],
+ "mutating": t["mutating"],
+ "risk": t.get("risk", "read-only"),
+ "ollama_available": t["name"] in OLLAMA_TOOL_NAMES,
+ }
+ for t in TOOLS
+ ]
diff --git a/integrations/wazuh-troubleshooting-tool/backend/analyzer.py b/integrations/wazuh-troubleshooting-tool/backend/analyzer.py
new file mode 100644
index 00000000..d96d1ddb
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/analyzer.py
@@ -0,0 +1,87 @@
+def analyze(data):
+
+ # ----------------------------
+ # API AUTH FAILURE
+ # ----------------------------
+ if "API AUTH FAILED" in data["api"]:
+ return {
+ "problem": "Wazuh API authentication failed",
+ "fix": "Check API credentials in config.py",
+ "command": None
+ }
+
+ # ----------------------------
+ # API CONNECTION FAILURE
+ # ----------------------------
+ if "API CONNECTION FAILED" in data["api"]:
+ return {
+ "problem": "Wazuh API not reachable",
+ "fix": "Manager service might be down",
+ "command": "sudo systemctl restart wazuh-manager"
+ }
+
+ # ----------------------------
+ # INDEXER DOWN
+ # ----------------------------
+ if "inactive" in data["indexer"] or "failed" in data["indexer"]:
+ return {
+ "problem": "Wazuh Indexer is DOWN",
+ "fix": "Restart Wazuh Indexer",
+ "command": "sudo systemctl restart wazuh-indexer"
+ }
+
+ # ----------------------------
+ # MANAGER DOWN
+ # ----------------------------
+ if "inactive" in data["manager"] or "failed" in data["manager"]:
+ return {
+ "problem": "Wazuh Manager is DOWN",
+ "fix": "Restart Wazuh Manager",
+ "command": "sudo systemctl restart wazuh-manager"
+ }
+
+ # ----------------------------
+ # DASHBOARD DOWN
+ # ----------------------------
+ if "inactive" in data["dashboard"] or "failed" in data["dashboard"]:
+ return {
+ "problem": "Wazuh Dashboard is DOWN",
+ "fix": "Restart Wazuh Dashboard",
+ "command": "sudo systemctl restart wazuh-dashboard"
+ }
+
+ # ----------------------------
+ # ERROR3099
+ # ----------------------------
+ if "ERROR3099" in data["logs"]:
+ return {
+ "problem": "Wazuh modules failure (ERROR3099)",
+ "fix": "Restart Wazuh Manager",
+ "command": "sudo systemctl restart wazuh-manager"
+ }
+
+ # ----------------------------
+ # DISK FULL
+ # ----------------------------
+ if "100%" in data["disk"] or "95%" in data["disk"]:
+ return {
+ "problem": "Disk usage too high",
+ "fix": "Clean disk or increase storage",
+ "command": None
+ }
+
+ # ----------------------------
+ # MEMORY LOW
+ # ----------------------------
+ if "available" in data["memory"] and "Mi" in data["memory"]:
+ return {
+ "problem": "Memory might be low",
+ "fix": "Consider increasing RAM",
+ "command": None
+ }
+
+ return {
+ "problem": "System OK",
+ "fix": "No action needed",
+ "command": None
+ }
diff --git a/integrations/wazuh-troubleshooting-tool/backend/assistant_engine.py b/integrations/wazuh-troubleshooting-tool/backend/assistant_engine.py
new file mode 100644
index 00000000..39c45366
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/assistant_engine.py
@@ -0,0 +1,66 @@
+from use_cases import run_use_cases
+from copilot_engine import run_copilot
+from config import (
+ WAZUH_API_URL,
+ API_USERNAME,
+ API_PASSWORD,
+ INDEXER_USERNAME,
+ INDEXER_PASSWORD,
+ INDEXER_URL,
+ OLLAMA_URL,
+ OLLAMA_MODEL,
+)
+
+def process_assistant(user_input, context=None):
+ if context is None:
+ context = {}
+
+ # 1. Try to run guided diagnostic use cases
+ try:
+ result = run_use_cases(user_input, context)
+ except Exception as e:
+ print(f"Error in guided use case: {e}")
+ result = None
+
+ if result:
+ # Predefined use cases take precedence
+ return {
+ "type": "use_case",
+ "display": result.get("display", ""),
+ "ask": result.get("ask", []),
+ "done": result.get("done", False),
+ "context": result.get("context", {})
+ }
+
+ # 2. Fall back to Ollama conversational AI
+ history = context.get("ollama_history", [])
+ history.append({"role": "user", "content": user_input})
+
+ try:
+ reply = run_copilot(
+ messages=history,
+ ollama_url=OLLAMA_URL,
+ ollama_model=OLLAMA_MODEL,
+ include_env=True,
+ wazuh_api_url=WAZUH_API_URL,
+ api_username=API_USERNAME,
+ api_password=API_PASSWORD,
+ indexer_url=INDEXER_URL,
+ indexer_username=INDEXER_USERNAME,
+ indexer_password=INDEXER_PASSWORD,
+ )
+ history.append({"role": "assistant", "content": reply})
+ context["ollama_history"] = history
+
+ return {
+ "type": "use_case",
+ "display": reply,
+ "ask": [],
+ "done": False,
+ "context": context
+ }
+ except Exception as e:
+ return {
+ "type": "info",
+ "message": f"Error from Ollama: {str(e)}"
+ }
diff --git a/integrations/wazuh-troubleshooting-tool/backend/config.py b/integrations/wazuh-troubleshooting-tool/backend/config.py
new file mode 100644
index 00000000..1bd88778
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/config.py
@@ -0,0 +1,62 @@
+import os
+
+# Config path is located in the root directory (parent of the backend directory)
+CONFIG_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "config"))
+
+def parse_simple_yaml(filepath):
+ config = {}
+ current_key = None
+ if not os.path.exists(filepath):
+ return config
+ with open(filepath, 'r') as f:
+ for line in f:
+ line = line.strip()
+ if not line or line.startswith('#'):
+ continue
+ if line.endswith(':'):
+ current_key = line[:-1].strip()
+ config[current_key] = {}
+ elif ':' in line:
+ parts = line.split(':', 1)
+ k = parts[0].strip()
+ v = parts[1].strip().strip('"').strip("'")
+ if v.lower() == 'true':
+ v = True
+ elif v.lower() == 'false':
+ v = False
+ if current_key:
+ config[current_key][k] = v
+ else:
+ config[k] = v
+ return config
+
+_cfg = parse_simple_yaml(CONFIG_PATH)
+
+WAZUH_API_URL = _cfg.get("wazuh_api", {}).get("host")
+API_USERNAME = _cfg.get("wazuh_api", {}).get("username")
+API_PASSWORD = _cfg.get("wazuh_api", {}).get("password")
+
+INDEXER_USERNAME = _cfg.get("indexer", {}).get("username")
+INDEXER_PASSWORD = _cfg.get("indexer", {}).get("password")
+INDEXER_URL = _cfg.get("indexer", {}).get("url")
+
+KIBANA_USERNAME = _cfg.get("kibana", {}).get("username")
+KIBANA_PASSWORD = _cfg.get("kibana", {}).get("password")
+
+# ── Ollama (Wazuh Copilot) ────────────────────────────────────────────────────
+OLLAMA_URL = _cfg.get("ollama", {}).get("url", "http://localhost:11434")
+OLLAMA_MODEL = _cfg.get("ollama", {}).get("model", "qwen3:1.7b")
+
+# ── Anthropic / Claude (Wazuh Agent — optional second brain) ─────────────────
+# Not required: if api_key is left blank, the agent simply runs on Ollama only.
+ANTHROPIC_API_KEY = _cfg.get("anthropic", {}).get("api_key", "")
+ANTHROPIC_MODEL = _cfg.get("anthropic", {}).get("model", "claude-sonnet-5")
+
+# ── Server (used to build the CORS allowlist — see main.py) ─────────────────
+SERVER_HOST = _cfg.get("server", {}).get("host", "localhost")
+FRONTEND_PORT = _cfg.get("server", {}).get("frontend_port", "3000")
+
+if not API_PASSWORD or not INDEXER_PASSWORD or not KIBANA_PASSWORD:
+ import sys
+ print(f"CRITICAL ERROR: Required credentials missing in configuration file at {CONFIG_PATH}", file=sys.stderr)
+ sys.exit(1)
diff --git a/integrations/wazuh-troubleshooting-tool/backend/copilot_engine.py b/integrations/wazuh-troubleshooting-tool/backend/copilot_engine.py
new file mode 100644
index 00000000..1addc4a1
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/copilot_engine.py
@@ -0,0 +1,314 @@
+"""
+copilot_engine.py
+Wazuh Copilot — AI-powered Wazuh expert assistant.
+Sends conversations to a local Ollama instance with a deep Wazuh system prompt.
+Optionally enriches context with live environment data from the Wazuh API / Indexer.
+"""
+
+import json
+import requests
+import urllib3
+
+from utils.lgtm_utils import find_relevant_issues, format_lgtm_context
+from utils.public_repo_search import search_public_issues, search_public_discussions, format_public_context
+
+urllib3.disable_warnings()
+
+# ─────────────────────────────────────────────────────────────────────────────
+# SYSTEM PROMPT — the brain of the copilot
+# ─────────────────────────────────────────────────────────────────────────────
+
+WAZUH_SYSTEM_PROMPT = """You are WazuhCopilot, a world-class Wazuh SIEM expert assistant.
+Provide direct, precise, and technical answers.
+Always generate complete, ready-to-use XML rules/decoders or YAML configs.
+For explanations, keep them brief and structured. Use current Wazuh v4.x syntax."""
+
+
+# ─────────────────────────────────────────────────────────────────────────────
+# ENVIRONMENT CONTEXT COLLECTOR
+# ─────────────────────────────────────────────────────────────────────────────
+
+def _safe_run(cmd: str) -> str:
+ """Run a shell command, return output or empty string on failure."""
+ import subprocess
+ try:
+ out = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT, timeout=5)
+ return out.decode(errors="replace").strip()
+ except Exception:
+ return ""
+
+
+def collect_environment_context(
+ wazuh_api_url: str,
+ api_username: str,
+ api_password: str,
+ indexer_url: str,
+ indexer_username: str,
+ indexer_password: str,
+) -> dict:
+ """
+ Collect live environment data to give the AI context about the user's
+ actual Wazuh deployment. All failures are silently ignored.
+ """
+ ctx = {}
+
+ # ── Service status ────────────────────────────────────────────────────
+ ctx["manager_status"] = _safe_run("systemctl is-active wazuh-manager")
+ ctx["indexer_status"] = _safe_run("systemctl is-active wazuh-indexer")
+ ctx["dashboard_status"] = _safe_run("systemctl is-active wazuh-dashboard")
+ ctx["filebeat_status"] = _safe_run("systemctl is-active filebeat")
+
+ # ── System resources ─────────────────────────────────────────────────
+ ctx["disk"] = _safe_run("df -h / /var/ossec /var/lib/wazuh-indexer 2>/dev/null | head -5")
+ ctx["memory"] = _safe_run("free -h")
+
+ # ── Wazuh API ─────────────────────────────────────────────────────────
+ try:
+ token_res = requests.post(
+ f"{wazuh_api_url}/security/user/authenticate?raw=true",
+ auth=(api_username, api_password),
+ verify=False,
+ timeout=5,
+ )
+ token = token_res.text.strip() if token_res.status_code == 200 else None
+
+ if token:
+ headers = {"Authorization": f"Bearer {token}"}
+
+ # Manager info
+ info = requests.get(f"{wazuh_api_url}/", headers=headers, verify=False, timeout=5)
+ if info.status_code == 200:
+ d = info.json().get("data", {})
+ ctx["manager_version"] = d.get("api_version", "unknown")
+ ctx["wazuh_version"] = d.get("api_version", "unknown")
+
+ # Agent summary
+ agents_res = requests.get(
+ f"{wazuh_api_url}/agents?limit=500",
+ headers=headers,
+ verify=False,
+ timeout=5,
+ )
+ if agents_res.status_code == 200:
+ agents = agents_res.json().get("data", {}).get("affected_items", [])
+ ctx["agents_total"] = len(agents)
+ ctx["agents_active"] = sum(1 for a in agents if a.get("status") == "active")
+ ctx["agents_disconnected"] = sum(1 for a in agents if a.get("status") == "disconnected")
+
+ # Cluster
+ cluster_res = requests.get(
+ f"{wazuh_api_url}/cluster/status",
+ headers=headers,
+ verify=False,
+ timeout=5,
+ )
+ if cluster_res.status_code == 200:
+ cdata = cluster_res.json().get("data", {})
+ ctx["cluster_enabled"] = cdata.get("enabled", "unknown")
+ ctx["cluster_running"] = cdata.get("running", "unknown")
+
+ except Exception:
+ pass
+
+ # ── Indexer cluster health ─────────────────────────────────────────────
+ try:
+ health = requests.get(
+ f"{indexer_url}/_cluster/health",
+ auth=(indexer_username, indexer_password),
+ verify=False,
+ timeout=5,
+ )
+ if health.status_code == 200:
+ h = health.json()
+ ctx["indexer_cluster_status"] = h.get("status", "unknown")
+ ctx["indexer_nodes"] = h.get("number_of_nodes", 0)
+ ctx["indexer_active_shards"] = h.get("active_shards", 0)
+ ctx["indexer_unassigned_shards"] = h.get("unassigned_shards", 0)
+ except Exception:
+ pass
+
+ # ── Recent ossec.log errors ────────────────────────────────────────────
+ ctx["recent_manager_errors"] = _safe_run(
+ "tail -n 30 /var/ossec/logs/ossec.log 2>/dev/null | grep -i -E 'error|warn' | tail -10"
+ )
+
+ return ctx
+
+
+def format_environment_context(ctx: dict) -> str:
+ """Concise environment summary for CPU-friendly inference."""
+ if not ctx:
+ return ""
+
+ parts = []
+ # Services
+ svcs = []
+ for key, name in [("manager_status", "manager"), ("indexer_status", "indexer"), ("dashboard_status", "dashboard"), ("filebeat_status", "filebeat")]:
+ val = ctx.get(key)
+ if val:
+ svcs.append(f"{name}:{val}")
+ if svcs:
+ parts.append("Services: " + ", ".join(svcs))
+
+ # Version
+ if ctx.get("wazuh_version"):
+ parts.append(f"Version: {ctx['wazuh_version']}")
+
+ # Agents
+ if ctx.get("agents_total") is not None:
+ parts.append(f"Agents: {ctx['agents_total']} total ({ctx.get('agents_active', 0)} active)")
+
+ # Indexer status
+ if ctx.get("indexer_cluster_status"):
+ parts.append(f"Indexer: {ctx['indexer_cluster_status'].upper()}")
+
+ if not parts:
+ return ""
+
+ return "=== Environment: " + " | ".join(parts) + " ==="
+
+
+# ─────────────────────────────────────────────────────────────────────────────
+# MAIN COPILOT FUNCTION
+# ─────────────────────────────────────────────────────────────────────────────
+
+def fetch_wazuh_cloud_trial_doc() -> str:
+ """Fetch the official Wazuh Cloud trial documentation and extract the main content."""
+ try:
+ url = "https://documentation.wazuh.com/current/cloud-service/getting-started/sign-up-trial.html"
+ r = requests.get(url, timeout=10)
+ if r.status_code == 200:
+ html = r.text
+ start_idx = html.find('')
+ if start_idx != -1:
+ chunk = html[start_idx:start_idx + 15000]
+ import re
+ clean_text = re.sub(r'<[^>]+>', ' ', chunk)
+ clean_text = re.sub(r'\s+', ' ', clean_text).strip()
+ return clean_text[:6000]
+ except Exception as e:
+ print(f"Error fetching documentation: {e}")
+ return ""
+
+# Session-based cache for environment context strings to enable Ollama KV cache reuse
+SESSION_ENV_CACHE = {}
+
+def run_copilot(
+ messages: list,
+ ollama_url: str,
+ ollama_model: str,
+ include_env: bool,
+ wazuh_api_url: str,
+ api_username: str,
+ api_password: str,
+ indexer_url: str,
+ indexer_username: str,
+ indexer_password: str,
+ stream: bool = False,
+ session_id: str = None,
+ system_prompt: str = None,
+) -> str:
+ # Check if this query is about Wazuh Cloud trial or credentials
+ last_user_msg = ""
+ for msg in reversed(messages):
+ if msg.get("role") == "user":
+ last_user_msg = msg.get("content", "").lower()
+ break
+
+ doc_context = ""
+ if "cloud" in last_user_msg and any(x in last_user_msg for x in ["trial", "trail", "credential", "password", "username", "login"]):
+ doc_context = fetch_wazuh_cloud_trial_doc()
+
+ # Build the system message list
+ base_prompt = system_prompt if system_prompt is not None else WAZUH_SYSTEM_PROMPT
+ if doc_context:
+ system_content = (
+ base_prompt +
+ f"\n\n=== Official Wazuh Cloud Service Documentation ===\n{doc_context}\n=================================================\n\n"
+ "Instructions: Use the above official documentation to answer their question. "
+ "Explain that Wazuh Cloud trial credentials (username/password) are sent in a welcome email once provisioned, "
+ "or can be retrieved in the Environments console. Provide links if relevant."
+ )
+ else:
+ system_content = base_prompt
+
+ lgtm_issues = find_relevant_issues(last_user_msg)
+ lgtm_context = format_lgtm_context(lgtm_issues)
+ if lgtm_context:
+ system_content += "\n\n" + lgtm_context
+
+ public_issues = search_public_issues(last_user_msg)
+ public_discussions = search_public_discussions(last_user_msg)
+ public_context = format_public_context(public_issues, public_discussions)
+ if public_context:
+ system_content += "\n\n" + public_context
+
+ if include_env:
+ try:
+ env_str = None
+ if session_id and session_id in SESSION_ENV_CACHE:
+ env_str = SESSION_ENV_CACHE[session_id]
+
+ if not env_str:
+ env_ctx = collect_environment_context(
+ wazuh_api_url, api_username, api_password,
+ indexer_url, indexer_username, indexer_password,
+ )
+ env_str = format_environment_context(env_ctx)
+ if session_id and env_str:
+ SESSION_ENV_CACHE[session_id] = env_str
+
+ if env_str:
+ system_content += "\n\n" + env_str
+ except Exception:
+ pass
+
+ ollama_messages = [{"role": "system", "content": system_content}] + messages
+
+ payload = {
+ "model": ollama_model,
+ "messages": ollama_messages,
+ "stream": False,
+ "think": False,
+ "options": {
+ "temperature": 0.3, # more factual for technical answers
+ "num_predict": 4096,
+ },
+ }
+
+ resp = requests.post(
+ f"{ollama_url}/api/chat",
+ json=payload,
+ timeout=300,
+ )
+
+ if resp.status_code != 200:
+ raise RuntimeError(
+ f"Ollama returned HTTP {resp.status_code}: {resp.text[:300]}"
+ )
+
+ data = resp.json()
+ return data.get("message", {}).get("content", "").strip()
+
+
+def list_ollama_models(ollama_url: str) -> list:
+ """Return list of available Ollama model names."""
+ try:
+ resp = requests.get(f"{ollama_url}/api/tags", timeout=10)
+ if resp.status_code == 200:
+ return [m["name"] for m in resp.json().get("models", [])]
+ except Exception:
+ pass
+ return []
+
+
+def check_ollama_health(ollama_url: str) -> dict:
+ """Quick health check — is Ollama reachable and is the model available?"""
+ try:
+ resp = requests.get(f"{ollama_url}/api/tags", timeout=5)
+ if resp.status_code == 200:
+ models = [m["name"] for m in resp.json().get("models", [])]
+ return {"ok": True, "models": models}
+ except Exception as e:
+ return {"ok": False, "error": str(e), "models": []}
+ return {"ok": False, "models": []}
diff --git a/integrations/wazuh-troubleshooting-tool/backend/executor.py b/integrations/wazuh-troubleshooting-tool/backend/executor.py
new file mode 100644
index 00000000..3cd02ff8
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/executor.py
@@ -0,0 +1,63 @@
+import re
+import subprocess
+
+
+def run_command(cmd):
+ try:
+ result = subprocess.check_output(
+ cmd,
+ shell=True,
+ stderr=subprocess.STDOUT,
+ text=True
+ )
+ return result.strip()
+ except subprocess.CalledProcessError as e:
+ return e.output.strip()
+
+
+def run_command_argv(argv, input=None, timeout=30):
+ """
+ Run a command as an argv list with no shell involved.
+
+ Use this instead of run_command() whenever any part of the command is
+ built from a variable (password, filename, IP, ...), so that value can
+ never be reinterpreted as shell syntax. `input`, if given, is written to
+ the process's stdin — the safe replacement for shell pipes like
+ `echo secret | some-tool --stdin`, which also leaves the secret out of
+ the argv (and therefore out of `ps`).
+ """
+ try:
+ result = subprocess.run(
+ argv,
+ input=input,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ text=True,
+ timeout=timeout,
+ )
+ return result.stdout.strip()
+ except subprocess.TimeoutExpired as e:
+ output = e.output
+ return output.strip() if isinstance(output, str) else str(e)
+ except OSError as e:
+ return str(e)
+
+
+def replace_in_file(path, pattern, replacement, count=0, flags=0):
+ """
+ Apply a regex substitution to a file in place — the shell-free
+ replacement for `sed -i 's/pattern/replacement/' path`. Values used to
+ build `pattern`/`replacement` (filenames, IPs, ...) are never handed to
+ a shell or to sed, so they can't be reinterpreted as sed/shell syntax.
+
+ `replacement` is applied via a callable so its content is used
+ literally — a plain string passed to re.sub would let backslash
+ sequences (e.g. "\\1") in a filename be reinterpreted as a group
+ reference.
+ """
+ with open(path, "r") as f:
+ content = f.read()
+ new_content = re.sub(pattern, lambda _m: replacement, content, count=count, flags=flags)
+ with open(path, "w") as f:
+ f.write(new_content)
+ return new_content
diff --git a/integrations/wazuh-troubleshooting-tool/backend/flows/dashboard_ip_cert_flow.py b/integrations/wazuh-troubleshooting-tool/backend/flows/dashboard_ip_cert_flow.py
new file mode 100644
index 00000000..805bbaeb
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/flows/dashboard_ip_cert_flow.py
@@ -0,0 +1,149 @@
+"""
+Dashboard IP / certificate paths flow.
+
+Order: dashboard IP -> dashboard certificate paths -> (log analysis if still
+ongoing after the last step).
+
+Same pattern as flows/ip_cert_flow.py, just for the dashboard side. This
+module only DEFINES what's specific to these checks; all flow-control lives
+in utils/step_flow.py.
+"""
+
+from utils.fix_engine import FixEngine
+from utils.service_utils import restart_service_and_wait
+from utils.step_flow import stage_names, start_flow, run_step_flow
+
+PREFIX = "dash_ip_cert"
+ENTRY_STAGE = "dash_ip_check" # legacy-compatible entry point
+NEXT_STAGE_AFTER_ONGOING = "fetch_logs"
+
+
+# ---------------------------------------------------------------------------
+# STEP: dashboard IP
+# ---------------------------------------------------------------------------
+def _check_dash_ip(context):
+ data = FixEngine.check_dashboard_ip()
+ context["d_ip"] = data["d_ip"]
+ context["c_ip"] = data["c_ip"]
+ details = (
+ f" opensearch_dashboards.yml opensearch.hosts: {data['d_ip']}\n"
+ f" Expected (verified indexer IP): {data['c_ip']}"
+ )
+ return data["match"], details
+
+
+def _manual_check_dash_ip(context):
+ c_ip = context.get("c_ip", "")
+ return (
+ "To check this yourself:\n\n"
+ "1. See the dashboard's configured indexer host:\n"
+ " grep opensearch.hosts /etc/wazuh-dashboard/opensearch_dashboards.yml\n\n"
+ f"2. It should point to the indexer IP: {c_ip}"
+ )
+
+
+def _auto_fix_dash_ip(context):
+ c_ip = context.get("c_ip", "")
+ status = FixEngine.fix_dashboard_ip(c_ip)
+ details = (
+ f"Updated opensearch.hosts to https://{c_ip}:9200 in opensearch_dashboards.yml.\n"
+ f"Restarted wazuh-dashboard (status: {status.upper()})."
+ )
+ return status, details
+
+
+def _manual_fix_dash_ip(context):
+ c_ip = context.get("c_ip", "")
+ return (
+ "Edit /etc/wazuh-dashboard/opensearch_dashboards.yml and set:\n\n"
+ f" opensearch.hosts: [\"https://{c_ip}:9200\"]\n\n"
+ "Save the file."
+ )
+
+
+# ---------------------------------------------------------------------------
+# STEP: dashboard certificate paths
+# ---------------------------------------------------------------------------
+def _check_dash_cert(context):
+ data = FixEngine.check_dashboard_cert_paths()
+ context["dash_cert_missing"] = data["missing"]
+ details = (
+ "Configured cert paths (opensearch_dashboards.yml):\n"
+ f"{data['paths_raw']}\n\n"
+ "Available cert files (/etc/wazuh-dashboard/certs/):\n"
+ f"{data['files_raw']}"
+ )
+ return (not data["missing"]), details
+
+
+def _manual_check_dash_cert(context):
+ return (
+ "To check this yourself:\n\n"
+ "1. See the cert paths configured in opensearch_dashboards.yml:\n"
+ " grep -E 'ssl.certificate|ssl.key|certificateAuthorities' "
+ "/etc/wazuh-dashboard/opensearch_dashboards.yml\n\n"
+ "2. See the cert files that actually exist:\n"
+ " ls /etc/wazuh-dashboard/certs/\n\n"
+ "3. Every path from step 1 should exist in step 2's listing."
+ )
+
+
+def _auto_fix_dash_cert(context):
+ result = FixEngine.fix_dashboard_cert_paths()
+ if result.get("success"):
+ status = result["status"]
+ details = (
+ "Updated cert paths:\n"
+ f" cert: {result['cert']}\n key: {result['key']}\n CA: {result['ca']}\n"
+ f"Restarted wazuh-dashboard (status: {status.upper()})."
+ )
+ else:
+ status = "unknown"
+ details = "Could not auto-identify dashboard cert files. Please fix this one manually."
+ return status, details
+
+
+def _manual_fix_dash_cert(context):
+ return FixEngine.dashboard_cert_path_steps()
+
+
+def _restart_dashboard(context):
+ return restart_service_and_wait("wazuh-dashboard")
+
+
+STEPS = [
+ {
+ "key": "ip",
+ "title": "dashboard IP configuration",
+ "check_fn": _check_dash_ip,
+ "manual_check_instructions_fn": _manual_check_dash_ip,
+ "auto_fix_fn": _auto_fix_dash_ip,
+ "manual_fix_instructions_fn": _manual_fix_dash_ip,
+ "restart_fn": _restart_dashboard,
+ },
+ {
+ "key": "cert",
+ "title": "dashboard certificate paths",
+ "check_fn": _check_dash_cert,
+ "manual_check_instructions_fn": _manual_check_dash_cert,
+ "auto_fix_fn": _auto_fix_dash_cert,
+ "manual_fix_instructions_fn": _manual_fix_dash_cert,
+ "restart_fn": _restart_dashboard,
+ },
+]
+
+# All stages this module owns, plus the legacy entry stage.
+STAGES = stage_names(PREFIX, STEPS) | {ENTRY_STAGE}
+
+
+def dashboard_ip_cert_flow(user_choice=None, context=None):
+ if context is None:
+ context = {}
+
+ if context.get("stage") == ENTRY_STAGE:
+ return start_flow(PREFIX, STEPS, context)
+
+ return run_step_flow(
+ PREFIX, STEPS, NEXT_STAGE_AFTER_ONGOING,
+ user_choice=user_choice, context=context,
+ )
diff --git a/integrations/wazuh-troubleshooting-tool/backend/flows/default_route_flow.py b/integrations/wazuh-troubleshooting-tool/backend/flows/default_route_flow.py
new file mode 100644
index 00000000..7648eb53
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/flows/default_route_flow.py
@@ -0,0 +1,103 @@
+"""
+Dashboard default route flow — dedicated to the "Application Not Found" card.
+
+New, self-contained flow: it only reuses the generic, use-case-agnostic
+step engine (utils/step_flow.py). It does not touch flows/ip_cert_flow.py
+or flows/dashboard_ip_cert_flow.py.
+
+Checks a single thing: whether opensearch_dashboards.yml has
+uiSettings.overrides.defaultRoute set to /app/wz-home. This is the most
+common cause of "Application Not Found" - it happens when the dashboard
+config is left over from before an upgrade and is missing (or has a stale)
+default route setting for the new version.
+"""
+
+from utils.default_route_utils import (
+ DASHBOARD_CONFIG_PATH,
+ EXPECTED_DEFAULT_ROUTE,
+ get_default_route,
+ is_default_route_ok,
+ set_default_route,
+)
+from utils.service_utils import restart_service_and_wait
+from utils.step_flow import stage_names, start_flow, run_step_flow
+
+PREFIX = "default_route"
+ENTRY_STAGE = "default_route_check"
+NEXT_STAGE_AFTER_ONGOING = "app_not_found_broader_diagnostics"
+
+
+# ---------------------------------------------------------------------------
+# STEP: dashboard default route
+# ---------------------------------------------------------------------------
+def _check_default_route(context):
+ raw = get_default_route()
+ details = (
+ f" Configured: {raw.strip() or '(not set)'}\n"
+ f" Expected: uiSettings.overrides.defaultRoute: {EXPECTED_DEFAULT_ROUTE}"
+ )
+ return is_default_route_ok(raw), details
+
+
+def _manual_check_default_route(context):
+ return (
+ "To check this yourself:\n\n"
+ "1. Look for the default route setting:\n"
+ f" grep uiSettings.overrides.defaultRoute {DASHBOARD_CONFIG_PATH}\n\n"
+ "2. It should be set to:\n"
+ f" uiSettings.overrides.defaultRoute: {EXPECTED_DEFAULT_ROUTE}"
+ )
+
+
+def _auto_fix_default_route(context):
+ updated = set_default_route()
+ status = restart_service_and_wait("wazuh-dashboard")
+ details = (
+ f"Set uiSettings.overrides.defaultRoute: {EXPECTED_DEFAULT_ROUTE} in "
+ "opensearch_dashboards.yml.\n"
+ f" {updated.strip()}\n\n"
+ f"Restarted wazuh-dashboard (status: {status.upper()}).\n\n"
+ "Please open your browser and verify the dashboard is accessible."
+ )
+ return status, details
+
+
+def _manual_fix_default_route(context):
+ return (
+ f"Edit {DASHBOARD_CONFIG_PATH} and set:\n\n"
+ f" uiSettings.overrides.defaultRoute: {EXPECTED_DEFAULT_ROUTE}\n\n"
+ "Save the file, then restart the dashboard:\n"
+ " systemctl restart wazuh-dashboard"
+ )
+
+
+def _restart_dashboard(context):
+ return restart_service_and_wait("wazuh-dashboard")
+
+
+STEPS = [
+ {
+ "key": "route",
+ "title": "dashboard default route configuration",
+ "check_fn": _check_default_route,
+ "manual_check_instructions_fn": _manual_check_default_route,
+ "auto_fix_fn": _auto_fix_default_route,
+ "manual_fix_instructions_fn": _manual_fix_default_route,
+ "restart_fn": _restart_dashboard,
+ },
+]
+
+STAGES = stage_names(PREFIX, STEPS) | {ENTRY_STAGE}
+
+
+def default_route_flow(user_choice=None, context=None):
+ if context is None:
+ context = {}
+
+ if context.get("stage") == ENTRY_STAGE:
+ return start_flow(PREFIX, STEPS, context)
+
+ return run_step_flow(
+ PREFIX, STEPS, NEXT_STAGE_AFTER_ONGOING,
+ user_choice=user_choice, context=context,
+ )
diff --git a/integrations/wazuh-troubleshooting-tool/backend/flows/filebeat_flow.py b/integrations/wazuh-troubleshooting-tool/backend/flows/filebeat_flow.py
new file mode 100644
index 00000000..acd2994e
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/flows/filebeat_flow.py
@@ -0,0 +1,264 @@
+"""
+Step 3 of the alerts-not-showing pipeline: Filebeat.
+
+Order: ask how to check -> (start Filebeat if it's down) -> run the output
+test -> if it fails, classify WHY and offer an auto/manual fix for the
+known causes -> hand off to Step 4 (Wazuh Indexer) once Filebeat can talk
+to the indexer, or immediately if the failure itself says the indexer is
+unreachable (no point iterating on Filebeat if the indexer is the problem).
+
+This module owns everything specific to Filebeat: what to check, why we're
+checking it, what the manual instructions are, how to fix each known
+failure category. It hands off to the caller (use_cases/no_alerts_are_showing.py)
+by setting context["stage"] = STEP4_ENTRY_STAGE and returning handoff=True,
+the same pattern used by flows/ip_cert_flow.py and flows/dashboard_ip_cert_flow.py.
+
+Also reused standalone by use_cases/filebeat_error.py, a Filebeat-only
+troubleshooting card that stops on handoff instead of continuing into the
+indexer/cluster steps.
+"""
+
+from utils.response_utils import make_response
+from utils.service_utils import get_service_status, start_service_and_wait
+from utils.filebeat_utils import (
+ run_filebeat_output_test, get_filebeat_log_errors, classify_filebeat_failure,
+ fix_unsupported_filebeat_version, manual_unsupported_version_instructions,
+)
+from utils.cert_utils import regenerate_and_redeploy_certs, manual_cert_redeploy_instructions
+from utils.ai_utils import ai_explain
+
+ENTRY_STAGE = "step3_method"
+STEP4_ENTRY_STAGE = "step4_entry"
+
+STAGES = {
+ ENTRY_STAGE,
+ "step3_manual_wait",
+ "fix_filebeat_start",
+ "step3_fix_version_choice",
+ "step3_fix_version_manual_wait",
+ "step3_fix_tls_choice",
+ "step3_fix_tls_manual_wait",
+}
+
+WHY_TEXT = (
+ "The Wazuh Manager is generating alerts correctly. The next step is to "
+ "verify whether Filebeat is reading those alerts and forwarding them to "
+ "the Wazuh Indexer. If Filebeat is not working, alerts will never reach "
+ "the indexer or the dashboard."
+)
+
+MANUAL_CHECK_TEXT = (
+ "First, check whether the Filebeat service is running:\n\n"
+ " systemctl status filebeat\n\n"
+ "If Filebeat is active, test its connection to the Wazuh Indexer:\n\n"
+ " filebeat test output\n\n"
+ "Did the output test succeed?"
+)
+
+UNKNOWN_FAILURE_SYSTEM_PROMPT = (
+ "You are a Wazuh Filebeat troubleshooting expert. You'll be given the "
+ "output of 'filebeat test output' plus recent filebeat log error/warning "
+ "lines. In 3-4 short sentences: state the most likely root cause and the "
+ "single most useful next command or config fix. Be specific to what's "
+ "actually in the output - don't give generic advice."
+)
+
+
+def start_filebeat_flow(context):
+ context["stage"] = ENTRY_STAGE
+ return make_response(
+ display=(
+ "Step 3 - Check Filebeat:\n\n"
+ f"{WHY_TEXT}\n\n"
+ "How would you like to check this?\n\n"
+ " Auto - We perform all checks automatically.\n"
+ " Manual - We provide the commands, and you run them and share the output."
+ ),
+ ask=["Auto", "Manual"],
+ context=context,
+ )
+
+
+def filebeat_flow(user_choice=None, context=None):
+ if context is None:
+ context = {}
+ choice = (user_choice or "").strip().lower()
+ stage = context.get("stage")
+
+ if stage == ENTRY_STAGE:
+ if "manual" in choice:
+ context["stage"] = "step3_manual_wait"
+ return make_response(
+ display=MANUAL_CHECK_TEXT,
+ ask=["Yes, it succeeded", "No, it failed"],
+ context=context,
+ )
+ return _auto_check(context)
+
+ if stage == "step3_manual_wait":
+ if "yes" in choice or "succeeded" in choice:
+ return make_response(
+ display="[OK] Good - Filebeat can reach the indexer.",
+ context=_handoff_to_step4(context),
+ handoff=True,
+ )
+ # Self-reported failure - we still need real data to know why, so
+ # run the same diagnosis the auto path uses.
+ return _auto_check(context)
+
+ if stage == "fix_filebeat_start":
+ if "auto" in choice:
+ status = start_service_and_wait("filebeat")
+ elif choice == "done":
+ status = get_service_status("filebeat")
+ elif "manual" in choice:
+ return make_response(
+ display="Run: systemctl start filebeat",
+ ask=["Done"],
+ context=context,
+ )
+ else:
+ return make_response(display="How would you like to start it?", ask=["Auto", "Manual"], context=context)
+
+ if status != "active":
+ return make_response(
+ display=(
+ "[ROOT CAUSE FOUND] Filebeat failed to start\n\n"
+ "Filebeat did not come up, so alerts.json can never be shipped to the indexer.\n\n"
+ "Manual fix:\nCheck `journalctl -u filebeat` and /var/log/filebeat/filebeat for startup errors."
+ ),
+ done=True,
+ context=context,
+ )
+ return _run_test_and_branch(context, prefix="[OK] Filebeat is running.\n\n")
+
+ if stage == "step3_fix_version_choice":
+ return _apply_fix(
+ context, choice,
+ auto_fn=fix_unsupported_filebeat_version,
+ manual_instructions=manual_unsupported_version_instructions(),
+ manual_wait_stage="step3_fix_version_manual_wait",
+ issue_label="unsupported Filebeat version",
+ )
+
+ if stage == "step3_fix_version_manual_wait":
+ return _run_test_and_branch(context, prefix="")
+
+ if stage == "step3_fix_tls_choice":
+ return _apply_fix(
+ context, choice,
+ auto_fn=lambda: regenerate_and_redeploy_certs(),
+ manual_instructions=manual_cert_redeploy_instructions(),
+ manual_wait_stage="step3_fix_tls_manual_wait",
+ issue_label="TLS/certificate error",
+ )
+
+ if stage == "step3_fix_tls_manual_wait":
+ return _run_test_and_branch(context, prefix="")
+
+ return make_response(display="Unexpected Filebeat step.", done=True, context=context)
+
+
+# ---------------------------------------------------------------------------
+# internal helpers
+# ---------------------------------------------------------------------------
+def _handoff_to_step4(context):
+ context["stage"] = STEP4_ENTRY_STAGE
+ return context
+
+
+def _auto_check(context):
+ status = get_service_status("filebeat")
+ if status != "active":
+ context["stage"] = "fix_filebeat_start"
+ return make_response(
+ display=(
+ "Automatically checking Filebeat...\n\n"
+ "[WARNING] Filebeat is not running.\n\n"
+ "How would you like to start it?"
+ ),
+ ask=["Auto", "Manual"],
+ context=context,
+ )
+ return _run_test_and_branch(context, prefix="Automatically checking Filebeat...\n\n[OK] Filebeat is running.\n\n")
+
+
+def _run_test_and_branch(context, prefix=""):
+ test = run_filebeat_output_test()
+ display = f"{prefix}Running output test...\n{test['raw']}\n"
+
+ if test["ok"]:
+ display += (
+ "\n[OK] Filebeat is running correctly and can successfully communicate with "
+ "the Wazuh Indexer. We will now verify that the Wazuh Indexer is healthy."
+ )
+ return make_response(display=display, context=_handoff_to_step4(context), handoff=True)
+
+ errors = get_filebeat_log_errors()
+ category = classify_filebeat_failure(test["raw"], errors)
+
+ if category == "indexer_unreachable":
+ display += (
+ "\n[WARNING] Filebeat cannot reach the Wazuh Indexer. This isn't a Filebeat "
+ "problem by itself, so we're moving straight to checking the Wazuh Indexer "
+ "instead of continuing to troubleshoot Filebeat."
+ )
+ return make_response(display=display, context=_handoff_to_step4(context), handoff=True)
+
+ if category == "unsupported_version":
+ context["stage"] = "step3_fix_version_choice"
+ display += (
+ "\n[ISSUE] This looks like an unsupported Filebeat version. Wazuh is only "
+ "compatible with Filebeat-OSS 7.10.2 - a newer version will fail with errors "
+ "like 'invalid_index_name_exception' on the _license index.\n\n"
+ "Would you like us to fix this automatically, or fix it yourself?"
+ )
+ return make_response(display=display, ask=["Auto", "Manual"], context=context)
+
+ if category == "tls_cert_error":
+ context["stage"] = "step3_fix_tls_choice"
+ display += (
+ "\n[ISSUE] This looks like a TLS/certificate error. The fix is to regenerate "
+ "the certificates and redeploy them to the Wazuh Indexer, Filebeat, and Wazuh "
+ "Dashboard.\n\n"
+ "Would you like us to fix this automatically, or fix it yourself?"
+ )
+ return make_response(display=display, ask=["Auto", "Manual"], context=context)
+
+ # auth_failure / unknown - no scripted fix, surface what we know and stop.
+ explanation = ai_explain(UNKNOWN_FAILURE_SYSTEM_PROMPT, f"{test['raw']}\n{errors}") if errors.strip() else \
+ "No additional error/warning lines found in the Filebeat log."
+ label = "authentication failure" if category == "auth_failure" else "an unrecognized error"
+ display += (
+ f"\n[WARNING] The output test failed with what looks like {label}.\n\n"
+ f"Recent Filebeat log errors:\n{errors if errors else '(none found)'}\n\n"
+ f"AI analysis:\n{explanation}"
+ )
+ return make_response(display=display, done=True, context=context)
+
+
+def _apply_fix(context, choice, auto_fn, manual_instructions, manual_wait_stage, issue_label):
+ if "manual" in choice:
+ context["stage"] = manual_wait_stage
+ return make_response(
+ display=manual_instructions + "\n\nLet us know once you've made the change.",
+ ask=["Done"],
+ context=context,
+ )
+
+ result = auto_fn()
+ if result.get("ok"):
+ return _run_test_and_branch(
+ context,
+ prefix=f"Fixed the {issue_label} automatically.\n{result.get('log', '')}\n\n",
+ )
+
+ return make_response(
+ display=(
+ f"[ROOT CAUSE FOUND] Could not auto-fix the {issue_label}\n\n"
+ f"{result.get('log', '')}\n\n"
+ "Manual fix:\n" + manual_instructions
+ ),
+ done=True,
+ context=context,
+ )
diff --git a/integrations/wazuh-troubleshooting-tool/backend/flows/ip_cert_flow.py b/integrations/wazuh-troubleshooting-tool/backend/flows/ip_cert_flow.py
new file mode 100644
index 00000000..9c8ba805
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/flows/ip_cert_flow.py
@@ -0,0 +1,226 @@
+"""
+Indexer IP / certificate paths / heap memory flow.
+
+Order: IP address -> certificate paths -> heap memory -> (dashboard checks
+if still ongoing after the last step).
+
+This module only DEFINES what's specific to these checks - what to check,
+how to fix them, what the manual instructions are. All flow-control (asking
+permission, handling yes/manual, fixed/ongoing, moving between steps,
+handoff) lives in the generic, reusable engine at utils/step_flow.py.
+
+Per step (IP -> cert -> heap), the interaction pattern is always:
+
+ 1. ASK: "Should I check the ? (yes / manual)"
+ yes -> run the real check and report the real result.
+ manual -> give exact commands to check it, then ask "good to go /
+ incorrect".
+
+ 2. If there's an issue:
+ ASK: "Do you want me to fix this? (yes / manually)"
+ yes -> apply the fix, restart the indexer, ask "fixed/ongoing".
+ manually -> give fix steps, wait for confirmation, THEN restart the
+ indexer ourselves, then ask "fixed/ongoing".
+
+ 3. "fixed" -> stop.
+ "ongoing" -> move to the next step. After the last step (heap), still
+ ongoing hands off to the dashboard IP/cert flow.
+"""
+
+import re
+from utils.fix_engine import FixEngine
+from utils.service_utils import restart_service_and_wait
+from utils.step_flow import stage_names, start_flow, run_step_flow
+
+PREFIX = "ip_cert"
+ENTRY_STAGE = "ip_check" # legacy-compatible entry point
+NEXT_STAGE_AFTER_ONGOING = "dash_ip_check" # hands off to dashboard_ip_cert_flow
+
+
+# ---------------------------------------------------------------------------
+# STEP: IP address
+# ---------------------------------------------------------------------------
+def _check_ip(context):
+ data = FixEngine.check_indexer_ip()
+ context["c_ip"] = data["c_ip"]
+ context["i_ip"] = data["i_ip"]
+ details = (
+ f" config.yml IP: {data['c_ip']}\n"
+ f" opensearch.yml network.host: {data['i_ip']}"
+ )
+ return data["match"], details
+
+
+def _manual_check_ip(context):
+ return (
+ "To check this yourself:\n\n"
+ "1. Get the IP from the original install config:\n"
+ " tar -axf /home/vagrant/wazuh-install-files.tar wazuh-install-files/config.yml -O\n"
+ " (look under the 'indexer:' section for 'ip:')\n\n"
+ "2. Get the IP the indexer is actually using:\n"
+ " grep network.host /etc/wazuh-indexer/opensearch.yml\n\n"
+ "3. Compare the two — they should match."
+ )
+
+
+def _auto_fix_ip(context):
+ c_ip = context.get("c_ip", "")
+ status = FixEngine.fix_indexer_ip(c_ip)
+ details = (
+ f"Updated network.host to {c_ip} in opensearch.yml.\n"
+ f"Restarted wazuh-indexer (status: {status.upper()})."
+ )
+ return status, details
+
+
+def _manual_fix_ip(context):
+ c_ip = context.get("c_ip", "")
+ return (
+ "Edit /etc/wazuh-indexer/opensearch.yml and set:\n\n"
+ f" network.host: {c_ip}\n\n"
+ "Save the file."
+ )
+
+
+# ---------------------------------------------------------------------------
+# STEP: certificate paths
+# ---------------------------------------------------------------------------
+def _check_cert(context):
+ data = FixEngine.check_indexer_cert_paths()
+ context["cert_missing"] = data["missing"]
+ details = (
+ "Configured cert paths (opensearch.yml):\n"
+ f"{data['paths_raw']}\n\n"
+ "Available cert files (/etc/wazuh-indexer/certs/):\n"
+ f"{data['files_raw']}"
+ )
+ return (not data["missing"]), details
+
+
+def _manual_check_cert(context):
+ return (
+ "To check this yourself:\n\n"
+ "1. See the cert paths configured in opensearch.yml:\n"
+ " grep -E 'pemcert_filepath|pemkey_filepath|pemtrustedcas_filepath' "
+ "/etc/wazuh-indexer/opensearch.yml\n\n"
+ "2. See the cert files that actually exist:\n"
+ " ls /etc/wazuh-indexer/certs/\n\n"
+ "3. Every path from step 1 should exist in step 2's listing."
+ )
+
+
+def _auto_fix_cert(context):
+ result = FixEngine.fix_indexer_cert_paths()
+ if result.get("success"):
+ status = result["status"]
+ details = (
+ "Updated cert paths:\n"
+ f" cert: {result['cert']}\n key: {result['key']}\n CA: {result['ca']}\n"
+ f"Restarted wazuh-indexer (status: {status.upper()})."
+ )
+ else:
+ status = "unknown"
+ details = "Could not auto-identify cert files. Please fix this one manually."
+ return status, details
+
+
+def _manual_fix_cert(context):
+ return (
+ "Update the cert paths in /etc/wazuh-indexer/opensearch.yml so each one "
+ "points to a file that actually exists in /etc/wazuh-indexer/certs/."
+ )
+
+
+# ---------------------------------------------------------------------------
+# STEP: heap memory
+# ---------------------------------------------------------------------------
+def _check_heap(context):
+ data = FixEngine.check_jvm_heap()
+ context["recommended_heap"] = data["recommended_heap"]
+ details = (
+ f"Current: {data['current']}\n"
+ f"Total RAM: {data['total_gb']} GB\n"
+ f"Recommended: -Xms{data['recommended_heap']}g / -Xmx{data['recommended_heap']}g"
+ )
+ m = re.search(r"-Xmx(\d+)g", data["current"] or "")
+ current_gb = int(m.group(1)) if m else None
+ ok = (current_gb == data["recommended_heap"])
+ return ok, details
+
+
+def _manual_check_heap(context):
+ return (
+ "To check this yourself:\n\n"
+ "1. See the current heap settings:\n"
+ " grep -E '^-Xms|^-Xmx' /etc/wazuh-indexer/jvm.options\n\n"
+ "2. See total RAM:\n"
+ " free -h\n\n"
+ "3. Heap (-Xms/-Xmx) should be about 50% of total RAM, not more."
+ )
+
+
+def _auto_fix_heap(context):
+ heap_gb = context.get("recommended_heap", 2)
+ result = FixEngine.fix_jvm_heap(heap_gb)
+ status = result["status"]
+ details = (
+ "Edited jvm.options.\n"
+ f"Current heap settings:\n{result['updated']}\n"
+ f"Restarted wazuh-indexer (status: {status.upper()})."
+ )
+ return status, details
+
+
+def _manual_fix_heap(context):
+ return FixEngine.heap_steps()
+
+
+def _restart_indexer(context):
+ return restart_service_and_wait("wazuh-indexer")
+
+
+STEPS = [
+ {
+ "key": "ip",
+ "title": "indexer IP address",
+ "check_fn": _check_ip,
+ "manual_check_instructions_fn": _manual_check_ip,
+ "auto_fix_fn": _auto_fix_ip,
+ "manual_fix_instructions_fn": _manual_fix_ip,
+ "restart_fn": _restart_indexer,
+ },
+ {
+ "key": "cert",
+ "title": "certificate paths",
+ "check_fn": _check_cert,
+ "manual_check_instructions_fn": _manual_check_cert,
+ "auto_fix_fn": _auto_fix_cert,
+ "manual_fix_instructions_fn": _manual_fix_cert,
+ "restart_fn": _restart_indexer,
+ },
+ {
+ "key": "heap",
+ "title": "heap memory configuration",
+ "check_fn": _check_heap,
+ "manual_check_instructions_fn": _manual_check_heap,
+ "auto_fix_fn": _auto_fix_heap,
+ "manual_fix_instructions_fn": _manual_fix_heap,
+ "restart_fn": _restart_indexer,
+ },
+]
+
+# All stages this module owns, plus the legacy entry stage.
+STAGES = stage_names(PREFIX, STEPS) | {ENTRY_STAGE}
+
+
+def ip_cert_flow(user_choice=None, context=None):
+ if context is None:
+ context = {}
+
+ if context.get("stage") == ENTRY_STAGE:
+ return start_flow(PREFIX, STEPS, context)
+
+ return run_step_flow(
+ PREFIX, STEPS, NEXT_STAGE_AFTER_ONGOING,
+ user_choice=user_choice, context=context,
+ )
diff --git a/integrations/wazuh-troubleshooting-tool/backend/knowledge/lgtm.db b/integrations/wazuh-troubleshooting-tool/backend/knowledge/lgtm.db
new file mode 100644
index 00000000..5e4b2970
Binary files /dev/null and b/integrations/wazuh-troubleshooting-tool/backend/knowledge/lgtm.db differ
diff --git a/integrations/wazuh-troubleshooting-tool/backend/knowledge/lgtm_issues.json b/integrations/wazuh-troubleshooting-tool/backend/knowledge/lgtm_issues.json
new file mode 100644
index 00000000..1cac2b2e
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/knowledge/lgtm_issues.json
@@ -0,0 +1,3013 @@
+[
+ {
+ "number": 65967,
+ "title": "hi guys i am having an issue with agent enrollment where im using certificate based authentication b",
+ "body": "## Discord Message\n\n**Channel:** 1049711340316541004\n**User:** @alen6868\n**Message ID:** 1524302965131706368\n**Permalink:** https://discord.com/channels/1049711339578331186/1049711340316541004/1524302965131706368\n\n### Message Content:\n\nhi guys i am having an issue with agent enrollment where im using certificate based authentication but its asking for a password in the logs. its only some agents that have been deployed at a later date since we are deploying with GPOs\n\n```\n2026/07/08 08:23:37 wazuh-agent: ERROR: SSL write error (unable to send message.)\n2026/07/08 08:23:37 wazuh-agent: ERROR: If Agent verification is enabled, agent key and certificates are required!\n2026/07/08 08:24:37 wazuh-agent: INFO: Requesting a key from server: \n2026/07/08 08:24:37 wazuh-agent: INFO: No authentication password provided\n```\nmanager config\n```\n \n no\n 1515\n no\n yes\n no\n HIGH:!ADH:!EXP:!MD5:!RC4:!3DES:!CAMELLIA:@STRENGTH\n \n /var/ossec/etc/rootCA.pem\n no\n etc/sslmanager.cert\n etc/sslmanager.key\n \n no\n \n```\n\n",
+ "comments": [
+ "### Thread Reply from @nikhilgurjar_79625\n\n**User:** @nikhilgurjar_79625\n**Message ID:** 1524311602331910164\n**Permalink:** https://discord.com/channels/1049711339578331186/1524302965131706368/1524311602331910164\n\nHi Alen,\n\nUpon reviewing the shared details, the message: `INFO: No authentication password provided` is information message and is not the cause of the enrollment failure. Since your manager is configured with:\n```no```\n\nInstead, the relevant error is: `Error: If Agent verification is enabled, agent key and certificates are required!`. This indicates that the agent is attempting to perform certificate-based enrollment but is unable to locate or use the required client certificate and key.\n\nPlease ensure that the signed SSL certificate and key files (**sslagent.cert** and **sslagent.key**) have been copied to the affected Windows endpoint(reference document:https://documentation.wazuh.com/current/user-manual/agent/agent-enrollment/security-options/agent-identity-verification.html<#1260899358430462053>-verification-options) .\n\nThen, using an administrator account, verify that the Wazuh agent configuration file is located at: `C:\\Program Files (x86)\\ossec-agent\\ossec.conf` and that it contains the correct manager address and references the certificate and key in the section, for example:\n```\n\n \n \n \n\n \n \\sslagent.cert\n \\sslagent.key\n \n\n```\nAfter verifying the configuration, restart the Wazuh agent to apply the changes(PowerShell run as Administrator):\n```Restart-Service -Name wazuh```\n\nIf the issue persists, could you also provide the following?\n- Ensure the correct location of `sslagent.cert` and `sslagent.key` are present on the affected endpoint and accessible by the Wazuh agent.\n- The `ossec.conf` from both a working and a failing agent.\n\nHope this information is helpful for you. Feel free to let us know if you have further concerns or queries here.\n\nBest regards,\nNikhil\n\n",
+ "LGTM"
+ ],
+ "external_community": [
+ "[Linked Discord conversation \u2014 not fetchable without a bot token in that server.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/65967",
+ "labels": [
+ "Discord",
+ "reviewer/team/community/Stuti3097",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 65844,
+ "title": "Wazuh 5 - Sample Data Hi, I have deployed Wazuh 5 via Docker (lab). Where can I find the sample dat",
+ "body": "## Discord Message\n\n**Channel:** 1260900476505948262\n**User:** @_frieder\n**Message ID:** 1522085300741079172\n**Permalink:** https://discord.com/channels/1049711339578331186/1260900476505948262/1522085300741079172\n\n### Message Content:\n\nWazuh 5 - Sample Data\n\nHi, I have deployed Wazuh 5 via Docker (lab). Where can I find the sample date (alerts and so on) for testing purposes?\n\nI noticed that there are no longer any wazuh-alerts-* / wazuh-archives-*...\n\nAre these the new index patterns?\n wazuh-events-v5* / wazuh-events-raw-v5*\n\nWhere can I find the API docimentation for Wazuh 5 ?\n\nCheers!\n\n",
+ "comments": [
+ "### Thread Reply from @bonyjohn_45221\n\n**User:** @bonyjohn_45221\n**Message ID:** 1522122385602510960\n**Permalink:** https://discord.com/channels/1049711339578331186/1522085300741079172/1522122385602510960\n\nCurrently, the **Sample Data** option available in Wazuh 4.x is not included in the Wazuh 5.0 Beta dashboard. Since Wazuh 5.0 is still in beta, this feature is still under consideration. You can check the related [GitHub issue](https://github.com/wazuh/wazuh-dashboard-plugins/issues/7839) for more details.\n\nFor now, to ingest sample data for testing, I recommend adding sample logs manually through a Wazuh agent.\n\nIn Wazuh 5.0, the manager and agent are separate components. You can install or configure an agent on the manager server to monitor specific log files. When sample logs are written to those files, the agent will send them to the Wazuh manager for analysis. If the logs match the available decoders and rules, alerts will be generated and displayed on the dashboard.\n\nFor your use case, I created a sample Python script that writes test logs for different modules into separate log files. I tested this setup by configuring the agent on the manager server to monitor those files, and the logs were normalized successfully and generated alerts on the dashboard.\n\n",
+ "### Thread Reply from @bonyjohn_45221\n\n**User:** @bonyjohn_45221\n**Message ID:** 1522122461234200757\n**Permalink:** https://discord.com/channels/1049711339578331186/1522085300741079172/1522122461234200757\n\nFirst, create the sample log directory on the Wazuh manager:\n\n```bash\nmkdir -p /var/log/wazuh-samples\n```\n\nThen create the Python script:\n\n```bash\nvi /root/wazuh-sample-logs.py\n```\n\nCopy the content from the attached script into this file and save it.\n\nNext, add the following configuration to the Wazuh agent\u2019s `/var/ossec/etc/ossec.conf` file:\n\n```xml\n\n\n syslog\n /var/log/wazuh-samples/ssh-auth.log\n\n\n\n syslog\n /var/log/wazuh-samples/apache-access.log\n\n\n\n audit\n /var/log/wazuh-samples/audit.log\n\n\n\n json\n /var/log/wazuh-samples/office365.json\n\n\n\n json\n /var/log/wazuh-samples/github.json\n\n\n\n json\n /var/log/wazuh-samples/aws-cloudtrail.json\n\n\n\n json\n /var/log/wazuh-samples/docker.json\n\n\n\n syslog\n /var/log/wazuh-samples/windows-events.log\n\n\n\n json\n /var/log/wazuh-samples/azure.json\n\n```\n\nRestart the Wazuh agent:\n\n```bash\nsystemctl restart wazuh-agent\n```\n\nThen run the script:\n\n```bash\npython3 /root/wazuh-sample-logs.py -d /var/log/wazuh-samples -i 3\n```\n\nThe script will write sample logs into the configured files. The agent will collect them and send them to the manager, where the default decoders and rules will process them and generate alerts.\n\nThe current script includes sample logs for all configured modules except GCP.\n\n",
+ "### Thread Reply from @bonyjohn_45221\n\n**User:** @bonyjohn_45221\n**Message ID:** 1522122864302493847\n**Permalink:** https://discord.com/channels/1049711339578331186/1522085300741079172/1522122864302493847\n\nRegarding the index patterns, Wazuh 5.0 introduces:\n\n```text\nwazuh-events-v5*\nwazuh-events-raw-v5*\n```\n\nThese replace the Wazuh 4.x index patterns:\n\n```text\nwazuh-alerts-*\nwazuh-archives-*\n```\n\nIn Wazuh 5.0, events are stored in different `wazuh-events-v5*` indexes based on their category, such as application, network security, or cloud service events.\n\nThe `wazuh-events-raw-v5*` index replaces `wazuh-archives-*` and stores raw events. To enable raw event indexing, go to:\n\n**Wazuh Dashboard > Indexer Management > Settings > Enable Raw Events**\n\nYou can view the available index patterns under:\n\n**Dashboard Management > Index Patterns**\n\nRegarding the Wazuh 5.0 API documentation, complete API documentation is not yet available because the version is still in beta. For now, you can refer to the currently available [Wazuh 5.0 Beta documentation](https://documentation.wazuh.com/5.0-beta/index.html).\n\n",
+ "```bash\n#!/usr/bin/env python3\n\"\"\"\nwazuh-sample-logs.py -- continuous multi-source sample-log generator for Wazuh 5.0 testing.\n\nv2: JSON cloud events are wrapped to match the loaded Engine decoders\n (ruleset cmsync_standard_*). Each cloud decoder gates on an \"integration\"\n wrapper; Azure is matched by content fields (operationName/category).\n\nFile extension follows format: JSON -> *.json (log_format json)\n syslog/text -> *.log (log_format syslog)\n auditd -> audit.log (log_format audit)\n\nSources: ssh, malware(clamav*), apache, fim(auditd), office365, gcp, github,\n aws, msgraph, docker, azure, windows\n (*malware/clamav has NO decoder in the standard 5.0 ruleset -> won't alert)\n\nUsage:\n python3 wazuh-sample-logs.py -d /var/log/wazuh-samples -i 3\n python3 wazuh-sample-logs.py --once\n python3 wazuh-sample-logs.py --only aws,office365,github,gcp,azure,msgraph\n python3 wazuh-sample-logs.py --list # show sources/files\n\"\"\"\nimport argparse, json, os, random, signal, sys, time, uuid\nfrom datetime import datetime, timezone\n\nHOSTS = [\"web01\", \"app02\", \"db01\", \"dc01\", \"gw-edge\"]\nUSERS = [\"root\", \"admin\", \"jsmith\", \"mgarcia\", \"svc_backup\", \"ubuntu\", \"ec2-user\", \"developer\", \"test\", \"oadmin\"]\n\n\ndef pub_ip():\n return \"%d.%d.%d.%d\" % (random.randint(11, 223), random.randint(0, 255),\n random.randint(0, 255), random.randint(1, 254))\n\n\ndef rid(n=16):\n return uuid.uuid4().hex[:n]\n\n\ndef now_syslog():\n return datetime.now().strftime(\"%b %e %H:%M:%S\")\n\n\ndef now_iso_z(ms=False):\n d = datetime.now(timezone.utc)\n return d.strftime(\"%Y-%m-%dT%H:%M:%S.\") + \"%03dZ\" % (d.microsecond // 1000) if ms \\\n else d.strftime(\"%Y-%m-%dT%H:%M:%SZ\")\n\n\n# --------------------------------------------------------------------------- #\n# Native syslog / audit sources (decoded directly by the loaded ruleset)\n# --------------------------------------------------------------------------- #\ndef gen_ssh():\n host, pid, ip, port = random.choice(HOSTS), random.randint(1000, 65000), pub_ip(), random.randint(1024, 65000)\n user = random.choice(USERS)\n if random.random() < 0.35:\n method = random.choice([\"password\", \"publickey\"])\n msg = \"Accepted %s for %s from %s port %d ssh2\" % (method, user, ip, port)\n if method == \"publickey\":\n msg += \": RSA SHA256:\" + \"\".join(random.choice(\n \"ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz0123456789+/\") for _ in range(43))\n msgs = [msg, \"pam_unix(sshd:session): session opened for user %s by (uid=0)\" % user]\n else:\n if random.random() < 0.5:\n msgs = [\"Invalid user %s from %s port %d\" % (user, ip, port),\n \"Failed password for invalid user %s from %s port %d ssh2\" % (user, ip, port)]\n else:\n msgs = [\"Failed password for %s from %s port %d ssh2\" % (user, ip, port)]\n if random.random() < 0.5:\n msgs.append(\"pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 \"\n \"tty=ssh ruser= rhost=%s user=%s\" % (ip, user))\n ts = now_syslog()\n return [\"%s %s sshd[%d]: %s\" % (ts, host, pid, m) for m in msgs]\n\n\ndef gen_malware():\n # NOTE: no ClamAV decoder exists in the standard 5.0 ruleset -> these will\n # NOT produce a malware alert. Kept for raw-log / archive testing only.\n host, pid, ts = random.choice(HOSTS), random.randint(500, 9000), now_syslog()\n if random.random() < 0.15:\n return [\"%s %s freshclam[%d]: Database updated (%d signatures)\" %\n (ts, host, pid, random.randint(8000000, 8600000))]\n sigs = [\"Eicar-Test-Signature\", \"Win.Trojan.Agent-1234567\", \"Unix.Malware.Agent-9988776\",\n \"Js.Trojan.Cryxos-9876543\", \"Php.Malware.Webshell-5555555\", \"Win.Ransomware.WannaCry-1\"]\n paths = [\"/home/%s/Downloads/invoice.exe\", \"/tmp/.hidden/payload.bin\", \"/var/www/html/upload/shell.php\",\n \"/home/%s/eicar.com\", \"/opt/data/setup.js\"]\n p = random.choice(paths)\n if \"%s\" in p:\n p = p % random.choice(USERS)\n return [\"%s %s clamd[%d]: %s: %s FOUND\" % (ts, host, pid, p, random.choice(sigs))]\n\n\ndef gen_apache():\n reqs = [(\"GET\", \"/index.html\", 200), (\"GET\", \"/about\", 200), (\"POST\", \"/login\", 302),\n (\"GET\", \"/wp-admin/\", 403), (\"GET\", \"/../../etc/passwd\", 404),\n (\"GET\", \"/admin.php?id=1'%20OR%20'1'='1\", 403), (\"GET\", \"/api/users\", 401),\n (\"POST\", \"/upload.php\", 500), (\"GET\", \"/robots.txt\", 200), (\"HEAD\", \"/\", 200)]\n m, path, status = random.choice(reqs)\n ua = random.choice([\"Mozilla/5.0 (Windows NT 10.0; Win64; x64)\", \"curl/8.4.0\",\n \"sqlmap/1.7\", \"Nikto/2.5\", \"python-requests/2.31\"])\n ts = datetime.now().strftime(\"%d/%b/%Y:%H:%M:%S +0000\")\n return ['%s - - [%s] \"%s %s HTTP/1.1\" %d %d \"-\" \"%s\"' %\n (pub_ip(), ts, m, path, status, random.randint(120, 20000), ua)]\n\n\ndef gen_fim():\n aid = \"%.3f:%d\" % (time.time(), random.randint(1000, 999999))\n f = random.choice([\"/etc/passwd\", \"/etc/shadow\", \"/etc/ssh/sshd_config\",\n \"/etc/hosts\", \"/etc/sudoers\", \"/boot/grub/grub.cfg\"])\n user = random.choice(USERS)\n uid = 0 if user == \"root\" else random.randint(1000, 1010)\n comm, exe = random.choice([(\"vim\", \"/usr/bin/vim\"), (\"nano\", \"/usr/bin/nano\"),\n (\"sed\", \"/usr/bin/sed\"), (\"cp\", \"/usr/bin/cp\")])\n syscall = random.choice([\"257\", \"2\", \"82\"])\n return [\n ('type=SYSCALL msg=audit(%s): arch=c000003e syscall=%s success=yes exit=3 a0=7ffd a1=241 a2=1b6 '\n 'items=2 ppid=%d pid=%d auid=%d uid=%d gid=%d euid=%d suid=%d fsuid=%d egid=%d sgid=%d fsgid=%d '\n 'tty=pts0 ses=3 comm=\"%s\" exe=\"%s\" subj=unconfined key=\"wazuh_fim\"' %\n (aid, syscall, random.randint(1000, 3000), random.randint(3001, 60000),\n uid, uid, uid, uid, uid, uid, uid, uid, uid, comm, exe)),\n 'type=CWD msg=audit(%s): cwd=\"/root\"' % aid,\n ('type=PATH msg=audit(%s): item=0 name=\"%s\" inode=%d dev=fd:01 mode=0100644 ouid=0 ogid=0 '\n 'rdev=00:00 nametype=NORMAL' % (aid, f, random.randint(100000, 999999))),\n 'type=PROCTITLE msg=audit(%s): proctitle=%s' % (aid, (comm + \" \" + f).encode().hex()),\n ]\n\n\n# --------------------------------------------------------------------------- #\n# Cloud sources -- WRAPPED to satisfy the Engine decoder \"check\" conditions.\n# decoder//0 check: $_tmp_json.integration == '' AND exists($_tmp_json.)\n# --------------------------------------------------------------------------- #\ndef gen_office365():\n ops = [(\"UserLoggedIn\", \"AzureActiveDirectory\", 15, \"Succeeded\"),\n (\"UserLoginFailed\", \"AzureActiveDirectory\", 15, \"Failed\"),\n (\"FileAccessed\", \"SharePoint\", 6, \"Succeeded\"),\n (\"FileDownloaded\", \"OneDrive\", 6, \"Succeeded\"),\n (\"MailboxLogin\", \"Exchange\", 2, \"Succeeded\"),\n (\"Add-MailboxPermission\", \"Exchange\", 1, \"Succeeded\"),\n (\"Set-Mailbox\", \"Exchange\", 1, \"Succeeded\"),\n (\"Add member to role.\", \"AzureActiveDirectory\", 8, \"Succeeded\")]\n op, wl, rt, status = random.choice(ops)\n user = random.choice(USERS) + \"@contoso.com\"\n rec = {\"CreationTime\": datetime.now(timezone.utc).strftime(\"%Y-%m-%dT%H:%M:%S\"), \"Id\": str(uuid.uuid4()),\n \"Operation\": op, \"OrganizationId\": str(uuid.uuid4()), \"RecordType\": rt, \"ResultStatus\": status,\n \"UserKey\": rid(24), \"UserType\": 0, \"Version\": 1, \"Workload\": wl, \"ClientIP\": pub_ip(), \"UserId\": user,\n \"ObjectId\": \"https://contoso.sharepoint.com/Documents/report.xlsx\" if wl in (\"SharePoint\", \"OneDrive\") else user}\n if status == \"Failed\":\n rec[\"LogonError\"] = random.choice([\"InvalidUserNameOrPassword\", \"UserAccountLocked\", \"BlockedByConditionalAccess\"])\n return [json.dumps({\"integration\": \"office365\", \"office365\": rec})]\n\n\ndef gen_gcp():\n # security-relevant actions (public IAM grant, firewall open, SA key, sink delete)\n methods = [(\"SetIamPolicy\", \"cloudresourcemanager.googleapis.com\", \"project\", True),\n (\"storage.setIamPermissions\", \"storage.googleapis.com\", \"gcs_bucket\", True),\n (\"google.iam.admin.v1.CreateServiceAccountKey\", \"iam.googleapis.com\", \"service_account\", False),\n (\"v1.compute.firewalls.insert\", \"compute.googleapis.com\", \"gce_firewall_rule\", False),\n (\"v1.compute.firewalls.delete\", \"compute.googleapis.com\", \"gce_firewall_rule\", False),\n (\"google.logging.v2.ConfigServiceV2.DeleteSink\", \"logging.googleapis.com\", \"logging_sink\", False),\n (\"v1.compute.instances.delete\", \"compute.googleapis.com\", \"gce_instance\", False)]\n m, svc, rtype, public = random.choice(methods)\n proj = \"wazuh-lab-%d\" % random.randint(1, 9)\n principal = random.choice(USERS) + \"@\" + proj + \".iam.gserviceaccount.com\"\n proto = {\"@type\": \"type.googleapis.com/google.cloud.audit.AuditLog\",\n \"authenticationInfo\": {\"principalEmail\": principal},\n \"requestMetadata\": {\"callerIp\": pub_ip(), \"callerSuppliedUserAgent\": \"google-cloud-sdk\"},\n \"serviceName\": svc, \"methodName\": m, \"resourceName\": \"projects/%s/%s\" % (proj, rtype),\n \"authorizationInfo\": [{\"permission\": m, \"granted\": True}]}\n if public:\n proto[\"request\"] = {\"policy\": {\"bindings\": [{\"role\": \"roles/storage.objectViewer\",\n \"members\": [\"allUsers\", \"allAuthenticatedUsers\"]}]}}\n rec = {\"insertId\": rid(12), \"logName\": \"projects/%s/logs/cloudaudit.googleapis.com%%2Factivity\" % proj,\n \"resource\": {\"type\": rtype, \"labels\": {\"project_id\": proj, \"zone\": \"us-central1-a\"}},\n \"timestamp\": now_iso_z(ms=True), \"severity\": random.choice([\"NOTICE\", \"WARNING\", \"ERROR\"]),\n \"protoPayload\": proto}\n return [json.dumps({\"integration\": \"gcp\", \"gcp\": rec})]\n\n\ndef gen_github():\n actions = [\"repo.create\", \"repo.destroy\", \"repo.access\", \"org.add_member\", \"org.remove_member\",\n \"team.add_member\", \"protected_branch.policy_override\", \"git.clone\", \"oauth_access.create\",\n \"user.login\", \"user.failed_login\", \"hook.create\", \"public_key.create\"]\n action = random.choice(actions)\n actor, org = random.choice(USERS), \"acme-corp\"\n repo = org + \"/\" + random.choice([\"webapp\", \"api\", \"infra\", \"payments\"])\n ts = int(time.time() * 1000)\n rec = {\"@timestamp\": ts, \"action\": action, \"actor\": actor, \"actor_id\": random.randint(1000, 99999),\n \"actor_ip\": pub_ip(), \"actor_location\": {\"country_code\": \"US\"}, \"org\": org,\n \"org_id\": random.randint(100, 999), \"created_at\": ts, \"user\": actor, \"_document_id\": rid(20)}\n if action.startswith(\"repo\") or action == \"git.clone\":\n rec[\"repo\"] = rec[\"repository\"] = repo\n return [json.dumps({\"integration\": \"github\", \"github\": rec})]\n\n\ndef gen_aws():\n events = [(\"ConsoleLogin\", \"signin.amazonaws.com\", \"AwsConsoleSignIn\"),\n (\"RunInstances\", \"ec2.amazonaws.com\", \"AwsApiCall\"),\n (\"TerminateInstances\", \"ec2.amazonaws.com\", \"AwsApiCall\"),\n (\"CreateBucket\", \"s3.amazonaws.com\", \"AwsApiCall\"),\n (\"DeleteBucket\", \"s3.amazonaws.com\", \"AwsApiCall\"),\n (\"PutBucketPolicy\", \"s3.amazonaws.com\", \"AwsApiCall\"),\n (\"AuthorizeSecurityGroupIngress\", \"ec2.amazonaws.com\", \"AwsApiCall\"),\n (\"CreateUser\", \"iam.amazonaws.com\", \"AwsApiCall\"),\n (\"AttachUserPolicy\", \"iam.amazonaws.com\", \"AwsApiCall\")]\n name, src, etype = random.choice(events)\n acct, user = \"123456789012\", random.choice(USERS)\n rec = {\"eventVersion\": \"1.08\",\n \"userIdentity\": {\"type\": \"IAMUser\", \"principalId\": \"AIDA\" + rid(16).upper(),\n \"arn\": \"arn:aws:iam::%s:user/%s\" % (acct, user), \"accountId\": acct, \"userName\": user},\n \"eventTime\": now_iso_z(), \"eventSource\": src, \"eventName\": name,\n \"awsRegion\": random.choice([\"us-east-1\", \"eu-west-1\", \"ap-south-1\"]), \"sourceIPAddress\": pub_ip(),\n \"userAgent\": random.choice([\"aws-cli/2.13.0\", \"Mozilla/5.0\", \"console.amazonaws.com\"]),\n \"eventID\": str(uuid.uuid4()), \"eventType\": etype, \"recipientAccountId\": acct}\n if name == \"ConsoleLogin\":\n fail = random.random() < 0.5\n rec[\"responseElements\"] = {\"ConsoleLogin\": \"Failure\" if fail else \"Success\"}\n rec[\"additionalEventData\"] = {\"MFAUsed\": \"No\" if fail else \"Yes\"}\n if fail:\n rec[\"errorMessage\"] = \"Failed authentication\"\n return [json.dumps({\"integration\": \"aws\", \"aws\": rec})]\n\n\ndef gen_docker():\n combos = [(\"container\", \"start\"), (\"container\", \"create\"), (\"container\", \"die\"), (\"container\", \"stop\"),\n (\"container\", \"kill\"), (\"container\", \"destroy\"), (\"image\", \"pull\"),\n (\"network\", \"connect\"), (\"volume\", \"create\")]\n typ, action = random.choice(combos)\n img = random.choice([\"nginx:latest\", \"alpine:3.19\", \"mysql:8.0\", \"redis:7\", \"ubuntu:22.04\"])\n cid = rid(64)\n rec = {\"integration\": \"docker\",\n \"docker\": {\"Type\": typ, \"Action\": action, \"scope\": \"local\", \"time\": int(time.time()),\n \"timeNano\": int(time.time() * 1e9), \"status\": action, \"id\": cid, \"from\": img,\n \"Actor\": {\"ID\": cid, \"Attributes\": {\"image\": img, \"name\": \"cntr_\" + rid(6)}}}}\n return [json.dumps(rec)]\n\n\ndef gen_azure():\n # Azure logs are NOT integration-wrapped; matched by content fields:\n # sign-in -> operationName == 'Sign-in activity'\n # audit -> category == 'AuditLogs'\n # risky -> category in (RiskyUsers | UserRiskEvents)\n # graph -> category == 'MicrosoftGraphActivityLogs' (absorbs MS Graph;\n # there is no standalone ms-graph integration in the ruleset)\n kind = random.choice([\"signin\", \"audit\", \"risky\", \"graph\"])\n ip, user, corr = pub_ip(), random.choice(USERS) + \"@contoso.com\", str(uuid.uuid4())\n tenant = str(uuid.uuid4())\n if kind == \"signin\":\n fail = random.random() < 0.5\n return [json.dumps({\n \"time\": now_iso_z(ms=True), \"resourceId\": \"/tenants/%s/providers/Microsoft.aadiam\" % tenant,\n \"operationName\": \"Sign-in activity\", \"operationVersion\": \"1.0\", \"category\": \"SignInLogs\",\n \"tenantId\": tenant, \"resultType\": \"0\" if not fail else \"50126\",\n \"resultDescription\": \"\" if not fail else \"Invalid username or password.\",\n \"durationMs\": random.randint(50, 900), \"callerIpAddress\": ip, \"identity\": user,\n \"properties\": {\"id\": rid(24), \"correlationId\": corr,\n \"appDisplayName\": random.choice([\"Office 365\", \"Azure Portal\", \"Microsoft Teams\"]),\n \"userPrincipalName\": user, \"userDisplayName\": user.split(\"@\")[0], \"ipAddress\": ip,\n \"clientAppUsed\": \"Browser\", \"authenticationProtocol\": random.choice([\"ropc\", \"oauth2\", \"saml\"]),\n \"deviceDetail\": {\"deviceId\": rid(16), \"displayName\": \"LAPTOP-\" + rid(4).upper(),\n \"operatingSystem\": \"Windows 10\"},\n \"status\": {\"errorCode\": 0 if not fail else 50126,\n \"failureReason\": \"\" if not fail else \"Invalid username or password.\"},\n \"location\": {\"city\": \"Seattle\", \"countryOrRegion\": \"US\"},\n \"riskLevelAggregated\": random.choice([\"none\", \"low\", \"medium\", \"high\"]),\n \"riskState\": random.choice([\"none\", \"atRisk\"])}})]\n if kind == \"audit\":\n op = random.choice([\"Add user\", \"Delete user\", \"Update user\", \"Add member to role\", \"Reset user password\"])\n return [json.dumps({\n \"time\": now_iso_z(ms=True), \"category\": \"AuditLogs\", \"operationName\": op, \"tenantId\": tenant,\n \"resultType\": \"success\", \"callerIpAddress\": ip,\n \"properties\": {\"activityDisplayName\": op, \"category\": \"UserManagement\", \"result\": \"success\",\n \"initiatedBy\": {\"user\": {\"userPrincipalName\": user, \"ipAddress\": ip}},\n \"targetResources\": [{\"userPrincipalName\": \"target_\" + rid(3) + \"@contoso.com\", \"type\": \"User\"}],\n \"correlationId\": corr, \"id\": rid(20)}})]\n if kind == \"graph\":\n method = random.choice([\"GET\", \"POST\", \"PATCH\", \"DELETE\"])\n uri = random.choice([\"/v1.0/users\", \"/v1.0/me/messages\", \"/v1.0/groups\",\n \"/v1.0/security/alerts_v2\", \"/v1.0/directoryRoles/members/$ref\"])\n return [json.dumps({\n \"time\": now_iso_z(ms=True), \"category\": \"MicrosoftGraphActivityLogs\",\n \"operationName\": \"Microsoft Graph Activity\", \"tenantId\": tenant, \"Level\": 4,\n \"callerIpAddress\": ip, \"durationMs\": random.randint(10, 500), \"location\": \"US\",\n \"properties\": {\"appId\": str(uuid.uuid4()), \"userId\": rid(16), \"requestId\": str(uuid.uuid4()),\n \"clientRequestId\": str(uuid.uuid4()), \"requestMethod\": method, \"requestUri\": uri,\n \"responseStatusCode\": random.choice([200, 201, 401, 403, 404]),\n \"userAgent\": \"python-requests/2.31\", \"signInActivityId\": rid(20),\n \"roles\": [\"User.Read.All\", \"Directory.Read.All\"]}})]\n cat = random.choice([\"RiskyUsers\", \"UserRiskEvents\"])\n return [json.dumps({\n \"time\": now_iso_z(ms=True), \"category\": cat, \"operationName\": cat, \"tenantId\": tenant,\n \"properties\": {\"userPrincipalName\": user, \"riskLevel\": random.choice([\"low\", \"medium\", \"high\"]),\n \"riskState\": \"atRisk\", \"riskDetail\": \"none\", \"ipAddress\": ip,\n \"riskEventTypes\": [\"unfamiliarFeatures\", \"anonymizedIPAddress\"],\n \"correlationId\": corr, \"id\": rid(20)}})]\n\n\ndef gen_windows():\n # Single-line Windows EventLog XML -> read with log_format syslog.\n # decoder/windows-event gate: starts_with(event.original,'%s00\"\n \"000x8020000000000000\"\n \"%d\"\n \"SecurityWIN-DC01.contoso.local\"\n % (prov, guid, eid, ts, rec_id))\n data = \"\" + \"\".join(\"%s\" % (k, v) for k, v in pairs) + \"\"\n return [\"%s%s\" % (ns, system, data)]\n\n\nSOURCES = {\n \"ssh\": (\"ssh-auth.log\", \"syslog\", gen_ssh),\n \"malware\": (\"clamav.log\", \"syslog\", gen_malware),\n \"apache\": (\"apache-access.log\", \"syslog\", gen_apache),\n \"fim\": (\"audit.log\", \"audit\", gen_fim),\n \"office365\": (\"office365.json\", \"json\", gen_office365),\n \"gcp\": (\"gcp.json\", \"json\", gen_gcp),\n \"github\": (\"github.json\", \"json\", gen_github),\n \"aws\": (\"aws-cloudtrail.json\", \"json\", gen_aws),\n \"docker\": (\"docker.json\", \"json\", gen_docker),\n \"azure\": (\"azure.json\", \"json\", gen_azure),\n \"windows\": (\"windows-events.log\", \"syslog\", gen_windows),\n}\n\n\ndef main():\n ap = argparse.ArgumentParser(description=\"Continuous Wazuh 5.0 sample-log generator.\")\n ap.add_argument(\"-d\", \"--dir\", default=\"./wazuh-samples\")\n ap.add_argument(\"-i\", \"--interval\", type=float, default=5.0)\n ap.add_argument(\"-r\", \"--rate\", type=int, default=2, help=\"max events per source per batch\")\n ap.add_argument(\"--only\", default=\"\", help=\"comma-separated subset, e.g. aws,office365,azure\")\n ap.add_argument(\"--once\", action=\"store_true\")\n ap.add_argument(\"--list\", action=\"store_true\")\n args = ap.parse_args()\n\n if args.list:\n print(\"%-10s %-24s %s\" % (\"SOURCE\", \"FILE\", \"LOG_FORMAT\"))\n for n, (fn, fmt, _) in SOURCES.items():\n print(\"%-10s %-24s %s\" % (n, fn, fmt))\n return\n\n selected = list(SOURCES) if not args.only else \\\n [s.strip() for s in args.only.split(\",\") if s.strip() in SOURCES]\n if not selected:\n sys.exit(\"No valid sources. Choose from: \" + \", \".join(SOURCES))\n\n os.makedirs(args.dir, exist_ok=True)\n handles = {s: open(os.path.join(args.dir, SOURCES[s][0]), \"a\", buffering=1) for s in selected}\n\n stop = {\"v\": False}\n signal.signal(signal.SIGINT, lambda *_: stop.update(v=True))\n signal.signal(signal.SIGTERM, lambda *_: stop.update(v=True))\n\n print(\"Writing sample logs to %s\" % os.path.abspath(args.dir))\n print(\"Sources: %s\" % \", \".join(selected))\n print(\"Interval: %ss Rate: up to %s events/source/batch %s\" %\n (args.interval, args.rate, \"(single batch)\" if args.once else \"(Ctrl-C to stop)\"))\n\n total = 0\n while not stop[\"v\"]:\n for s in selected:\n fh, gen = handles[s], SOURCES[s][2]\n for _ in range(random.randint(1, max(1, args.rate))):\n for line in gen():\n fh.write(line + \"\\n\")\n total += 1\n fh.flush()\n if args.once:\n break\n t0 = time.time()\n while not stop[\"v\"] and time.time() - t0 < args.interval:\n time.sleep(0.2)\n\n for fh in handles.values():\n fh.close()\n print(\"\\nStopped. Wrote %d lines to %s\" % (total, os.path.abspath(args.dir)))\n\n\nif __name__ == \"__main__\":\n main()\n\n```",
+ "Good work, Bony, as always!",
+ "### Thread Reply from @_frieder\n\n**User:** @_frieder\n**Message ID:** 1522255817859010651\n**Permalink:** https://discord.com/channels/1049711339578331186/1522085300741079172/1522255817859010651\n\nGreat! Many thanks!\n\n"
+ ],
+ "external_community": [
+ "[Linked Discord conversation \u2014 not fetchable without a bot token in that server.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/65844",
+ "labels": [
+ "Discord",
+ "Resolved",
+ "reviewer/team/community/sakib789",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 65839,
+ "title": "Wazuh 4.14.6 - \"Error Checking Updates\"",
+ "body": "## Reddit Post\n\n**Subreddit:** r/Wazuh\n**Author:** u/IndyPilot80\n**Permalink:** https://www.reddit.com/r/Wazuh/comments/1uksdkn/wazuh_4146_error_checking_updates/\n\n### Content\n\nAnyone else seeing this after upgrading to 4.14.6?\n\nAPI Connections > Check updates > Error checking updates\nError in CTI service request: v4.14.6\n\n",
+ "comments": [
+ "A Community thread was assigned to me 20 minutes ago. I have no time to respond to both on my working hours.",
+ "Currently engaged in customizations for a enterprise level lead, after that meetings. Will not be able to address this today.",
+ "**Reddit comment from u/SirStephanikus**\n\n4.14.6 is still in rc2 status. \nDo you have perhaps some logs with more details?\n\nMaybe you open a ticket at GitHub \n\n[View on Reddit](https://www.reddit.com/r/Wazuh/comments/1uksdkn/comment/ouyndyg/)\n\n",
+ "**Reddit comment from u/IndyPilot80**\n\n4.14.6 came through via apt update so I assume it was promoted to stable.\n\n[View on Reddit](https://www.reddit.com/r/Wazuh/comments/1uksdkn/comment/ouyo8gj/)\n\n",
+ "**Reddit comment from u/Intelligent-Ear-866**\n\nHi! Thanks for reaching out to Wazuh Community.\n\nAs SirStephanikus mentioned, 4.14.6 hasn't reached stable yet \u2014 the latest tag is \\`v4.14.6-rc2\\`. The Check updates action queries the CTI service (\\`cti.wazuh.com\\`), and it only serves published/stable versions, so the \\`Error in CTI service request: v4.14.6\\` is expected while the version is still a release candidate.\n\nA couple of things worth checking:\n\n\\- If \\`apt update\\` pulled 4.14.6, you likely have the pre-release/staging repo enabled rather than the production one. Could you share \\`cat /etc/apt/sources.list.d/wazuh.list\\`?\n\n\\- To confirm whether this is the CTI-version mismatch or a network/SSL issue, the dashboard logs would help: \\`grep -i cti /var/log/wazuh-dashboard/\\*.log\\`\n\n\\- A quick reachability test from the server: \\`curl -v [https://cti.wazuh.com/api/v1/catalog/contexts/vd\\_1.0.0/consumers/vd\\_4.8.0\\`](https://cti.wazuh.com/api/v1/catalog/contexts/vd_1.0.0/consumers/vd_4.8.0`)\n\nIf the repo turns out to be pre-release, this should resolve itself once 4.14.6 goes GA.\n\nLet me know how it goes. \n\n[View on Reddit](https://www.reddit.com/r/Wazuh/comments/1uksdkn/comment/ouz261w/)\n\n",
+ "**Reddit comment from u/gdiazlo**\n\nThe release is in progress right now, the message will dissapear when all the artifacts are published (deb, rpm, docker, etc.). The version you have now contains all the updated data, so no need to do anything. \n\n[View on Reddit](https://www.reddit.com/r/Wazuh/comments/1uksdkn/comment/ouz5p0c/)\n\n",
+ "**Reddit comment from u/IndyPilot80**\n\nGreat. Thank you for the quick update!\n\n[View on Reddit](https://www.reddit.com/r/Wazuh/comments/1uksdkn/comment/ouz61cq/)\n\n",
+ "**Reddit comment from u/IndyPilot80**\n\nJust wanted to update saying everything is working normally now. Thanks again.\n\n[View on Reddit](https://www.reddit.com/r/Wazuh/comments/1uksdkn/comment/ov08xp8/)\n\n",
+ "Seems resolved by now, after Gabriel's response. Community member confirmed is all good now:\n\n```\n[SirStephanikus](https://www.reddit.com/user/SirStephanikus/)\n\u2022\n[17h ago](https://www.reddit.com/r/Wazuh/comments/1uksdkn/comment/ouyndyg/)\n4.14.6 is still in rc2 status.\nDo you have perhaps some logs with more details?\n\nMaybe you open a ticket at GitHub\n\n\nu/[IndyPilot80](https://www.reddit.com/user/IndyPilot80/) avatar\nIndyPilot80\n\u2022\n[17h ago](https://www.reddit.com/r/Wazuh/comments/1uksdkn/comment/ouyo8gj/)\n4.14.6 came through via apt update so I assume it was promoted to stable. I also tried doing a fresh install on a test VM. 4.14.6 installed and had the same CTI issue.\n\n\n[gdiazlo](https://www.reddit.com/user/gdiazlo/)\n\u2022\n[16h ago](https://www.reddit.com/r/Wazuh/comments/1uksdkn/comment/ouz5p0c/)\nThe release is in progress right now, the message will dissapear when all the artifacts are published (deb, rpm, docker, etc.). The version you have now contains all the updated data, so no need to do anything.\n\n\nu/[IndyPilot80](https://www.reddit.com/user/IndyPilot80/) avatar\nIndyPilot80\n\u2022\n[13h ago](https://www.reddit.com/r/Wazuh/comments/1uksdkn/comment/ov08xp8/)\nJust wanted to update saying everything is working normally now. Thanks again.\n\n\nu/[IndyPilot80](https://www.reddit.com/user/IndyPilot80/) avatar\nIndyPilot80\n\u2022\n[15h ago](https://www.reddit.com/r/Wazuh/comments/1uksdkn/comment/ouz61cq/)\nGreat. Thank you for the quick update!\n```",
+ "This issue is stale because it has been open for 15 days with no activity."
+ ],
+ "external_community": [],
+ "url": "https://github.com/wazuh/community/issues/65839",
+ "labels": [
+ "Reddit",
+ "stale",
+ "reviewer/team/community/sakib789",
+ "ambassador",
+ "review/quality",
+ "LGTM",
+ "reddit/subreddit/Wazuh"
+ ]
+ },
+ {
+ "number": 65835,
+ "title": "hi everyone im trying to create a sort of labeling system with wazuh labels and have a question if a",
+ "body": "## Discord Message\n\n**Channel:** 1049711340316541004\n**User:** @alen6868\n**Message ID:** 1521876460489670788\n**Permalink:** https://discord.com/channels/1049711339578331186/1049711340316541004/1521876460489670788\n\n### Message Content:\n\nhi everyone im trying to create a sort of labeling system with wazuh labels and have a question if anyone else has done the same,i currently have an agent which has 2 groups . I have the windows group in order to monitor sysmon logs and other windows log channels, and i want to centrally deploy the labels with the groups this is the agent.conf of the Cybersecurity group. But whenever inspecting any logs form this machine i see no labels. Any ideas\n\n```\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n```\n\n",
+ "comments": [
+ "### Thread Reply from @jorgesanchez_62411\n\n**User:** @jorgesanchez_62411\n**Message ID:** 1521884984598593826\n**Permalink:** https://discord.com/channels/1049711339578331186/1521876460489670788/1521884984598593826\n\nHi @alen6868 \n\nIn Wazuh's centralized `agent.conf`, the `name=\"...\"` attribute doesn't filter by the group name, it filters by the agent name. Because your agent's actual registered name isn't literally `\"Cybersecurity,\"` the agent evaluates this block, sees that its own name doesn't match, and skips the whole block, this is why the labels are not included.\n\nSince you're managing this centrally, the group targeting is already handled automatically by the folder structure on the manager (e.g., `/var/ossec/etc/shared/Cybersecurity/agent.conf`).\nSo you just need to drop the name attribute, so the block applies to every agent in the group:\n\n```xml\n \n \n \n \n \n \n \n \n \n \n \n \n \n\n```\n\n\nHaving an agent in 2 groups is a normal procedure, here is how it works: when an agent is in multiple groups, Wazuh manager reads the `agent.conf` from each group folder, merges them, and pushes a single compiled file down to the agent.\n\n\nRemoving the `name=\"Cybersecurity\"` attribute is almost certainly the fix you need. Let me know if the labels still aren't showing up after making this change\n\n",
+ "### Thread Reply from @alen6868\n\n**User:** @alen6868\n**Message ID:** 1521892736649789662\n**Permalink:** https://discord.com/channels/1049711339578331186/1521876460489670788/1521892736649789662\n\nAhhh okay i missunerstood the documentation or well more miss read for the name part. Appreciated\n\n",
+ "Hi @Jorgesnchz, \n\nThe initial response is good. Please ask the user if they need more help or if the labels are working now. If they don't need further help, we can close the issue. \n\nThank you!",
+ "### Thread Reply from @jorgesanchez_62411\n\n**User:** @jorgesanchez_62411\n**Message ID:** 1522175379547099237\n**Permalink:** https://discord.com/channels/1049711339578331186/1521876460489670788/1522175379547099237\n\nHi @alen6868 \n\nI'm glad to hear that you're not having any problems with it now. If you need more help during the setup process, feel free to open another ticket so we can help you resolve the issue.\n\n",
+ "### Thread Reply from @alen6868\n\n**User:** @alen6868\n**Message ID:** 1522235190846427206\n**Permalink:** https://discord.com/channels/1049711339578331186/1521876460489670788/1522235190846427206\n\nThank you, it works as intended now and solved a bunch of work with inventory management\n\n"
+ ],
+ "external_community": [
+ "[Linked Discord conversation \u2014 not fetchable without a bot token in that server.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/65835",
+ "labels": [
+ "Discord",
+ "level/task",
+ "type/troubleshooting",
+ "reviewer/team/community/Stuti3097",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 65791,
+ "title": "I have the following situation. I\u2019m monitoring specific files and folders using the parameters `chec",
+ "body": "## Slack Message\n\n**Channel:** #wazuh-server\n**User:** @michal.bednarczyk\n**Timestamp:** 1782803214.689649\n**Permalink:** https://wazuh.slack.com/archives/C07CCCCGHHP/p1782803214689649\n\n### Message Content:\n\nI have the following situation. I\u2019m monitoring specific files and folders using the parameters `check_all=\u201cyes\u201d` and `whodata=\u201cyes\u201d`. I\u2019ve noticed that in many cases, the `whodata` parameter doesn\u2019t work. Specifically, the Dashboard shows an entry indicating that a file, for example, was modified, but the syscheck mode is listed as \u201creal-time,\u201d and there\u2019s no record of the process responsible for opening and modifying the file, nor is the user who performed the operation listed. It\u2019s as if the `whodata` parameter didn\u2019t work. Is this normal, or when I\u2019m monitoring a file and have whodata set to \u201cyes,\u201d shouldn\u2019t it automatically switch to \u201crealtime\u201d?\n\nI also encountered another situation. I tested creating files in a monitored directory using various tools. I created a file using PowerShell, Notepad++, and, as usual, Windows File Explorer, and in each of these cases, the newly created file was identified by FIM, and an entry reporting this activity appeared.\n\nHowever, when I created an image file in Paint, there was no indication that the file had been created. Every time a file was created using Paint, there was no record of this activity.\nWhat could have caused this? It seems to me that if a file was created in Paint and saved in a monitored location, an alert or entry for that activity should have appeared in the Dashboard, under the FIM section.\n\n",
+ "comments": [
+ "@Stuti3097, please update the ticket status once you start working.",
+ "### Thread Reply from @stuti.gupta\n\n**User:** @stuti.gupta\n**Timestamp:** 1782810379.691979\n**Permalink:** https://wazuh.slack.com/archives/C07CCCCGHHP/p1782810379691979\n\nHi,\nLet me first clarify the difference between **`realtime`** and **`whodata:`**\n\n`realtime` is used to detect file changes immediately. It is configured on **directories** (not individual files), and Wazuh monitors the files within those directories. https://documentation.wazuh.com/current/user-manual/capabilities/file-integrity/basic-settings.html#real-time-monitoring\n`whodata` is an extension of real-time monitoring. It is also configured on **directories and files**, it collects information about **who** performed the action (user, process name, PID, etc.). On Linux, this requires the Audit subsystem (or eBPF, depending on the configuration). On Windows, it relies on the appropriate Windows auditing policies. https://documentation.wazuh.com/current/user-manual/capabilities/file-integrity/advanced-settings.html#who-data-monitoring\n\nIf you enable `whodata=\"yes\"`, you do not need to also enable `realtime=\"yes\"` for the same directory, as `whodata` already includes real-time monitoring.\n\nRegarding your first question, I tested this behavior on my end, and I wasn't able to reproduce the issue you're describing.\nWhen I configured a monitored directory with `whodata=\"yes\"` (along with `check_all=\"yes\"`), the generated FIM alerts were reported with `mode: \"whodata\"` and included the `audit` information (user and process). I did not observe the alerts falling back to `mode: \"realtime\"` for the same monitored path.\n\nIf you're seeing `mode: \"realtime\"` without the `audit` section, it usually indicates that Wazuh detected the file change but could not correlate it with the corresponding audit event.\n\n\nI couldn't reproduce this behavior, could you please share your `` configuration, your Wazuh version, and an example alert where this occurs? That would help us better understand what is happening in your environment.\n\nRegarding your second question, I was able to generate FIM alerts when creating files using File Explorer, PowerShell, and Notepad++. However, I observed the same behaviour as you when creating and saving an image with Paint; the FIM alert was not generated.\n\nI'm currently discussing this internally to determine whether this is expected behaviour or a limitation in how Paint performs file operations.\n\nIf it turns out to be expected behaviour and you still want to generate alerts for those Windows Security Events, you can create custom Wazuh rules to detect the relevant Windows Event IDs as a workaround.\nhttps://documentation.wazuh.com/current/user-manual/capabilities/file-integrity/creating-custom-fim-rules.html\nhttps://documentation.wazuh.com/current/user-manual/ruleset/ruleset-xml-syntax/rules.html\n\n",
+ "### Thread Reply from @stuti.gupta\n\n**User:** @stuti.gupta\n**Timestamp:** 1782820172.787279\n**Permalink:** https://wazuh.slack.com/archives/C07CCCCGHHP/p1782820172787279\n\nRefer to: https://documentation.wazuh.com/current/user-manual/reference/ossec-conf/syscheck.html#ignore\n\n",
+ "### Thread Reply from @michal.bednarczyk\n\n**User:** @michal.bednarczyk\n**Timestamp:** 1783061660.463029\n**Permalink:** https://wazuh.slack.com/archives/C07CCCCGHHP/p1783061660463029\n\nThank you very much for your help and explanation @stuti.gupta. In my Lab, I created an image file with a .png extension in the mspaint.exe application, and an entry appeared in the Dashboard when I opened the file. I used the whodata and check_all parameters on the monitored directory where the file was created. The whodata parameter worked correctly the first time.\n\nHowever, in the client\u2019s environment, it did not work correctly. Even though the `whodata` and `check_all` parameters were set, it kept switching to \u201crealtime\u201d mode every time. I should note that I had not set monitoring to \u201crealtime.\u201d\n\n",
+ "closing this due to inactivity"
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/65791",
+ "labels": [
+ "Slack",
+ "reviewer/team/community/sakib789",
+ "closed/inactivity",
+ "review/quality/community_team/internal",
+ "LGTM",
+ "slack/channel/wazuh-server"
+ ]
+ },
+ {
+ "number": 65782,
+ "title": "Hi, i changed the limit from 10.000 to 100.000 logs in a report and saved, yet the reports stays at ",
+ "body": "## Discord Message\n\n**Channel:** 1260900476505948262\n**User:** @.maxaumax\n**Message ID:** 1521135831618687177\n**Permalink:** https://discord.com/channels/1049711339578331186/1260900476505948262/1521135831618687177\n\n### Message Content:\n\nHi, i changed the limit from 10.000 to 100.000 logs in a report and saved, yet the reports stays at 10K lines. Any ideas ?\n\n",
+ "comments": [
+ "### Thread Reply from @stutigupta\n\n**User:** @stutigupta\n**Message ID:** 1521341187477213405\n**Permalink:** https://discord.com/channels/1049711339578331186/1521135831618687177/1521341187477213405\n\nHi @.maxaumax \n\nPlease allow me sometime. I'm testing this. \n\nThank you!\n\n",
+ "### Thread Reply from @stutigupta\n\n**User:** @stutigupta\n**Message ID:** 1521378155783389236\n**Permalink:** https://discord.com/channels/1049711339578331186/1521135831618687177/1521378155783389236\n\nI have tested this and am getting the same results as you. We are discussing this internally with the team.\n\nAs a workaround, you need to create a report definition with a record limit of more than 10000, \n\nFirstly, save, then search. For example, in Discover, I have applied a 1-year time-range filter. Then clicked on **Save** and gave the name **report1**\n\nGo to **Reporting**\nThen click on **Create ** under **Report definitions**\nIn Report source, select **Saved search**, now select the serach that we saved, like **report1**\nSet the **record limit**according to your requirement. \n*Note: Generating reports with a large number of records can cause memory issues* \nSet the rest of the options accordingly. Once done, click on the **Create**.\n\n",
+ "### Thread Reply from @stutigupta\n\n**User:** @stutigupta\n**Message ID:** 1521383138951954432\n**Permalink:** https://discord.com/channels/1049711339578331186/1521135831618687177/1521383138951954432\n\nAnother workaround is ;\nOpen this file `/usr/share/wazuh-dashboard/plugins/reportsDashboards/server/routes/utils/constants.js`\n\n\u00a0Search for this line\n`const DEFAULT_MAX_SIZE = exports.DEFAULT_MAX_SIZE = 10000;`\n\nNow update this value.\nEx:\n`const DEFAULT_MAX_SIZE = exports.DEFAULT_MAX_SIZE = 80000;`\n\nRestart the Wazuh dashboard\n`systemctl restart wazuh-dashboard`\n\n",
+ "Hi @Ayeeshar \n\nPlease make sure you respond to the user promptly if you have set the status to `In progress`. In case you are unable to do that, please mention the reason here. For now, to move things forward, I have answered this on your behalf. \n\nThank you!",
+ "### Thread Reply from @.maxaumax\n\n**User:** @.maxaumax\n**Message ID:** 1521427034625278054\n**Permalink:** https://discord.com/channels/1049711339578331186/1521135831618687177/1521427034625278054\n\nOk thanks for all that, i'll be testing it out @stutigupta\n\n",
+ "### Thread Reply from @stutigupta\n\n**User:** @stutigupta\n**Message ID:** 1522137906372939846\n**Permalink:** https://discord.com/channels/1049711339578331186/1521135831618687177/1522137906372939846\n\nHi @.maxaumax \n\nPlease let me know if the issue is resolved or if you need more help on this. \n\nThank you!\n\n",
+ "Opened issue here: https://github.com/wazuh/wazuh/issues/37294",
+ "### Thread Reply from @stutigupta\n\n**User:** @stutigupta\n**Message ID:** 1524624588934217758\n**Permalink:** https://discord.com/channels/1049711339578331186/1521135831618687177/1524624588934217758\n\nHi @.maxaumax\n\n",
+ "### Thread Reply from @stutigupta\n\n**User:** @stutigupta\n**Message ID:** 1524624632907304971\n**Permalink:** https://discord.com/channels/1049711339578331186/1521135831618687177/1524624632907304971\n\nPlease check the update here: https://github.com/wazuh/wazuh/issues/37294#issuecomment-4879044455\n\n"
+ ],
+ "external_community": [
+ "[Linked Discord conversation \u2014 not fetchable without a bot token in that server.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/65782",
+ "labels": [
+ "Discord",
+ "review/moderation",
+ "reviewer/team/community/sakib789",
+ "reviewer/team/community/Stuti3097",
+ "review/quality/community_team/internal",
+ "escalated",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 65764,
+ "title": "Several cis_debian13.yml SCA checks appear to fail on seemingly correctly configured systems \u2014 could someone confirm?",
+ "body": "**GitHub Discussion**\n\n**Repository:** wazuh/wazuh \n**Discussion #:** 37243 \n**Category:** Q&A \n**Author:** @Toutunique \n**URL:** https://github.com/wazuh/wazuh/discussions/37243 \n**Created:** 2026-06-28T11:48:03Z \n\n---\n\nHi all,\r\n\r\nWhile reviewing SCA results on a few Debian 13 (Trixie) hosts running the Wazuh agent (apt 4.x/stable channel), we ran into a number of checks in ruleset/sca/debian/cis_debian13.yml that report \"Failed\" even though the underlying system configuration looks correct to us. Before assuming our systems are wrong, we'd like to ask whether these are known issues or whether we're missing something.\r\n\r\nWe're listing everything we found in one place since they're all in the same ruleset file. Happy to split them if they appear issues \r\n\r\n\r\n1. ID 33044 \u2014 \"Ensure access to bootloader config is configured\"\r\n\r\nRule:\r\n\r\nc:stat -L \"%n %a %u %U %g %G\" /boot/grub/grub.cfg -> r:0 root 0 root && r:600\r\n\r\nRunning this stat command literally (without -c) does not produce the format string output \u2014 stat treats \"%n %a %u %U %g %G\" as a second filename argument, errors with \"No such file or directory\", and falls back to its default verbose output instead. Adding -c produces the expected 0 root 0 root / 600 substrings on a host where the file is genuinely 600 root:root.\r\n\r\nIs -c simply missing from the command?\r\n\r\n\r\n2. ID 33188 \u2014 \"Ensure pam_pwquality module is enabled\"\r\n\r\nRule:\r\n\r\nf:/etc/pam.d/common-password -> r:password\\s*\\t*requisite\\s*\\t*pam_pwquality.so\\s*\\t*retry=(d+)\r\n\r\n(d+) matches one or more literal letters \"d\" \u2014 it looks like the backslash for \\d was dropped. Our common-password has password requisite pam_pwquality.so retry=3, which never contains the literal letter \"d\" after retry=, so the check can't pass regardless of configuration.\r\n\r\n\r\n3. ID 33211 \u2014 \"Ensure strong password hashing algorithm is configured\"\r\n\r\nRule:\r\n\r\nf:/etc/login.defs -> r:ENCRYPT_METHOD\\s*\\t*SHA512 | r:ENCRYPT_METHOD\\s*\\t*YESCRYPT\r\n\r\nThis is a single regex with a | alternation inside it, not two separate r: clauses. The right-hand side of the | still contains the literal text r: as part of the pattern (r:ENCRYPT_METHOD\\s*\\t*YESCRYPT), so it's actually searching for the literal substring r:ENCRYPT_METHOD...YESCRYPT, which never appears in login.defs. The SHA512 branch (left of |) has no stray r: and would work correctly. So this only manifests for hosts using ENCRYPT_METHOD YESCRYPT (which the check's own remediation text recommends as an acceptable alternative to SHA512) \u2014 which may be why it hasn't been caught: a SHA512-configured host passes fine, masking the bug.\r\n\r\n\r\n4. ID 33216 \u2014 \"Ensure group root is the only GID 0 group\"\r\n\r\nRule:\r\n\r\nf:/etc/group -> r:^root:x:0:0\r\n\r\nStandard /etc/group format is name:password:GID:member-list. A normal, correct root entry is root:x:0: (empty member list) \u2014 there's no reason for a second 0 after the third colon unless a member literally named 0 exists. This looks like it may have been copied from a different check pattern.\r\n\r\n\r\n5. ID 33217 \u2014 \"Ensure root account access is controlled\"\r\n\r\nRule:\r\n\r\nc:passwd -S root -> r:Password is status: P | r:Password is status: L\r\n\r\nOn Debian (shadow-utils), passwd -S root outputs short fields, e.g.:\r\n\r\n\n*(truncated \u2014 see original discussion for full content)*\n\n",
+ "comments": [
+ "Hi @Nikhil201Gurjar Please take care of this",
+ "Hi Team,\n\nI investigated this issue in my local lab by reproducing the same environment and was able to confirm the behaviour. I've already prepared the correct syntax to address the issue.\n\nCurrently, I have two additional SCA checks remaining to validate. Once I complete the testing and confirm the results, I'll provide the complete solution and respond to the user's query.\n\n[Debian CIS Benchmark Failed.pdf](https://github.com/user-attachments/files/29463113/Debian.CIS.Benchmark.Failed.pdf)\n",
+ "Related PR: https://github.com/wazuh/wazuh/pull/37320",
+ "Hi @Nikhil201Gurjar \n\nGreat work on this issue. If it is possible, please escalate to the concern team when you spot any bugs or possible fixes in the future.\n\nThank you!",
+ "**Discussion comment**\n\n**Author:** @Nikhil201Gurjar \n**URL:** https://github.com/wazuh/wazuh/discussions/37243#discussioncomment-17466590 \n**Created:** 2026-06-29T04:13:43Z \n\n---\n\nHi @Toutunique \r\n\r\nI appreciate your efforts in investigating the issue in depth. Let me review the details you've shared and attempt to replicate the behaviour in my test environment. I'll share my findings with you once I complete my investigation.\r\n\r\nBest regards,\r\nNikhil\n\n",
+ "**Discussion comment**\n\n**Author:** @Nikhil201Gurjar \n**URL:** https://github.com/wazuh/wazuh/discussions/37243#discussioncomment-17479187 \n**Created:** 2026-06-30T04:28:42Z \n\n---\n\nHi @Toutunique \r\n\r\nI replicated the reported scenario in my local lab using an identical environment and can confirm that the majority of the `SCA` checks you flagged \u2014 specifically those related to regular expression issues \u2014 are valid. I verified each check individually using the corresponding CLI commands, and after applying the appropriate remediation steps, all affected checks passed successfully and were reflected as Passed on the SCA dashboard for the agent.\r\n\r\nThe one exception is SCA Check `ID: 33271`. During my validation, I confirmed that the **create_module** and **query_module** syscalls remain available by default in the test environment, which means the underlying logic of this check is sound. That said, I do agree that some of the regular expressions within the policy could be refined to improve both compatibility and accuracy.\r\nPlease refer to the attached document below, which includes full validation details, CLI verification steps, and the recommended remediation syntax updates for each affected SCA check:\r\n\r\n[CIS_Debian13_SCA_Validation.pdf](https://github.com/user-attachments/files/29489988/CIS_Debian13_SCA_Validation.pdf)\r\n\r\nAdditionally, I'd like to make a general recommendation for issue reports: whenever you identify a potential problem with an SCA check, please include both the `reproduction steps` and the `proposed corrected syntax` alongside your report. This allows us to reproduce and validate the behaviour more efficiently on our end, and it also serves as a strong foundation should you choose to submit a Pull Request (PR) to improve the SCA policy.\r\n\r\nHope this information is helpful for you. Please feel free to let us know if you have further queries or questions here. \r\n\r\nBest regards,\r\nNikhil\n\n",
+ "**Discussion comment**\n\n**Author:** @Toutunique \n**URL:** https://github.com/wazuh/wazuh/discussions/37243#discussioncomment-17479526 \n**Created:** 2026-06-30T05:17:02Z \n\n---\n\nHi Nikhil,\r\n\r\nThanks for the thorough reproduction and for sharing the validation PDF \u2014 the corrected rules and reproduction steps for all 9 checks are exactly what's needed to move this into a PR.\r\n\r\nOn 33271: no real disagreement. The `create_module`/`query_module` absence I flagged isn't an x86 Debian 13 issue \u2014 it's specific to an ARM64 host in my environment, where those syscalls genuinely don't exist in the kernel. Your test on a standard x86_64 VM correctly shows them present, so the check logic itself is sound there. Might be worth a documentation note that this check can't pass on ARM64 platforms, but that's a platform caveat rather than a regex bug, so I'll leave it out of the PR.\r\n\r\nFor the other 8, I'd like to open a PR using the corrected syntax from your document. Let me know if that works for you, and if there's a preferred target branch or PR template for ruleset changes.\r\n\r\nThanks again for taking the time to dig into this.\r\n\r\nKind regards, Tout\n\n",
+ "**Discussion comment**\n\n**Author:** @Nikhil201Gurjar \n**URL:** https://github.com/wazuh/wazuh/discussions/37243#discussioncomment-17479893 \n**Created:** 2026-06-30T06:06:10Z \n\n---\n\nHi Tout,\r\n\r\nSure, you can use the attached PDF as a reference when raising a Pull Request (PR) or creating an issue here:\r\n\r\nCreate Issue: https://github.com/wazuh/wazuh/issues/new?assignees=&labels=&projects=&template=default.md&title=\r\n\r\nWhen creating the issue, I recommend including the following information to help the team reproduce and validate the behaviour efficiently:\r\n- Environment (e.g., OS version, Wazuh version, SCA policy version)\r\n- Current behaviour (existing condition and observed result)\r\n- Expected behaviour (the expected SCA validation result)\r\n- Steps to reproduce the issue\r\n- Proposed corrected syntax or configuration, where applicable\r\n\r\nIncluding these details will help the team reproduce the scenario more quickly and streamline the validation process for the proposed changes.\r\n\r\nRegards\n\n",
+ "**Discussion comment**\n\n**Author:** @Toutunique \n**URL:** https://github.com/wazuh/wazuh/discussions/37243#discussioncomment-17488344 \n**Created:** 2026-06-30T18:45:41Z \n\n---\n\nThanks again for the validation. I've opened a PR with the corrected syntax for the 7 confirmed checks:\r\n\r\nhttps://github.com/wazuh/wazuh/pull/37320\r\n\r\nKind regards,\r\n\r\nTout\n\n",
+ "A [PR](https://github.com/wazuh/wazuh/pull/37385) has been created that addresses these issues.",
+ "**Discussion comment**\n\n**Author:** @Johnng007 \n**URL:** https://github.com/wazuh/wazuh/discussions/37243#discussioncomment-17516541 \n**Created:** 2026-07-02T22:11:19Z \n\n---\n\n@Toutunique \r\nThanks for taking time out to point out the issues.\r\n\r\nAlbeit some of the bugs you noted are correct, the bugs you mentioned affecting some IDs (IDs 33257, 33270, 33271) are false positives.\r\n\r\nI believe the confusion comes from misinterpreting `.+` this does not mean in literal dots `. dot` that would be `\\.+`. \r\n\r\nThis is correct `echo \"hardening.rules\" | grep -E '.+\\.rules$'` hence the file match pattern is correct.\r\n\r\nFixes will be available in the next release.\n\n",
+ "Closing the community (Resolved)",
+ "**Discussion reply**\n\n**Author:** @Toutunique \n**URL:** https://github.com/wazuh/wazuh/discussions/37243#discussioncomment-17519485 \n**Created:** 2026-07-03T06:05:03Z \n\n---\n\n@Johnng007 \r\n\r\nThanks for the feedback \u2014 I think the grep -E test doesn't reflect how SCA actually evaluates the pattern. SCA uses OS_Regex, where `.` matches a literal dot and `\\.` matches any character \u2014 the inverse of grep's ERE (see docs: https://documentation.wazuh.com/current/user-manual/ruleset/ruleset-xml-syntax/regex.html). So `.+\\.rules$` can't match `audit.rules` under OS_Regex. This was also confirmed empirically by @Nikhil201Gurjar's own test on a live 4.14.5 agent, where the check only passed after applying `\\.+\\.rules$`.\r\n\r\nThat said, the regex point only applies to one part of 33257 and 33270. Both checks had additional, unrelated bugs: 33270's path filter pointed to `/usr/bin/usermod`, which doesn't exist on Debian (correct path is `/usr/sbin/usermod`), and 33257 had a malformed duplicate rule line plus an invalid double-arrow chain on a `c:` rule. Those two issues stand regardless of the regex discussion.\r\n\r\nKind regards, Tout\n\n"
+ ],
+ "external_community": [],
+ "url": "https://github.com/wazuh/community/issues/65764",
+ "labels": [
+ "reviewer/team/community/Stuti3097",
+ "review/quality",
+ "LGTM",
+ "GitHub Discussion"
+ ]
+ },
+ {
+ "number": 65698,
+ "title": "kelvinkosgei538 https://wazuh.slack.com/archives/C0A933R8E/p1782283664246459",
+ "body": "Hello <@U01HXEV6SL8> I have a problem with a decoder that I saved whereby when\nI test it using a log, I only get the name of the decoder but no user and IP.\nDoes anyone know how to fix the issue. Thankyou\nhttps://wazuh.slack.com/archives/C0A933R8E/p1782283664246459",
+ "comments": [
+ "Initial responses, LGTM",
+ "LGTM"
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/65698",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/Stuti3097",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 65640,
+ "title": "vonsectech https://wazuh.slack.com/archives/C0A933R8E/p1782052390159169",
+ "body": "Can someone help get lore info on the slack cloud for small businesses?\nhttps://wazuh.slack.com/archives/C0A933R8E/p1782052390159169",
+ "comments": [
+ "Please DM this user asking if he got the help he is looking for. If not contact with salse team.",
+ "Dm'ed the user; no reply from him. I'm closing the issue as inactive. "
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/65640",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "Resolved without feedback",
+ "reviewer/team/community/sakib789",
+ "closed/inactivity",
+ "review/quality/community_team/internal",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 65639,
+ "title": "Wazuh remote upgrade for RHEL5 agent ",
+ "body": "\r\n\r\nHi,\r\n\r\nWe are encountering the following error when attempting a remote upgrade \r\nfor a RHEL 5 agent:\r\n\r\n*No upgrade task was created (1819) - The WPK for this platform is not \r\navailable*\r\n\r\nBased on this, it appears that *RHEL 5 agents cannot be upgraded through \r\nWazuh remote upgrade because no WPK package is available for that platform*. \r\nIf this understanding is correct, these systems would need to be excluded \r\nfrom the standard upgrade workflow and treated as legacy agents.\r\n\r\nCould you please confirm whether the same approach should also be applied \r\nto other legacy platforms such as *CentOS 5, Ubuntu 14/16, and SLES 11*\u2014that \r\nis, exclude them from the regular upgrade process and retain them on the \r\nhighest Wazuh agent version that can be installed initially and supported \r\non those platforms?\r\n\r\nCurrent versions:\r\n\r\n - *Wazuh server version:* 4.14.4-1\r\n - *RHEL 5 agent version:* 4.14.2-1\r\n\r\nPlease confirm if this is the recommended approach for handling these \r\nlegacy operating systems.\r\n\r\n-- \r\nYou received this message because you are subscribed to the Google Groups \"Wazuh | Mailing List\" group.\r\nTo unsubscribe from this group and stop receiving emails from it, send an email to wazuh+unsubscribe@googlegroups.com.\r\nTo view this discussion visit https://groups.google.com/d/msgid/wazuh/fdc1412b-e6fc-4d41-beea-7b5f007b4b18n%40googlegroups.com.\r\n",
+ "comments": [
+ "Hi Team,\n\nI have tested the scenario and can confirm that the issue exists. Testing of the custom WPK files for the legacy operating systems is still pending, as I have been occupied with back-to-back support calls. I will continue the testing and provide an update once the validation is completed.\n\n\n",
+ "LGTM.",
+ "Hi Veera,\nThank you for your patience while I investigated this issue.\n\nI reviewed the reported behaviour on the RHEL 5 agent and performed additional validation in our lab environment to determine whether the issue was related to a missing WPK package or a limitation in the remote upgrade mechanism itself.\n\nEnvironment Details\n\nWazuh manager version: 4.14.5\nCustomer agent version: 4.12.0\nLab setup:\nRed Hat Enterprise Linux Server release 5.1 (Tikanga)\nKernel: 2.6.18-53.el5\nArchitecture: x86_64\nI first deployed a RHEL 5.1 system in the lab and successfully installed the Wazuh agent. The agent registered with the manager and remained active without issues. The manager correctly identified the agent as:\n\nOperating system: Linux | localhost.localdomain | 2.6.18-53.el5 | x86_64\nClient version: Wazuh v4.12.0\nStatus: Active\nI then carried out the following tests.\n\nTest A \u2013 Official WPK Package: I attempted a remote upgrade using Remote upgrade and the official WPK package(reference document: https://documentation.wazuh.com/current/user-manual/agent/agent-management/remote-upgrading/wpk-files/wpk-list.html):\n\n/var/ossec/bin/agent_upgrade -a -f wazuh_agent_v4.14.5_linux_x86_64.rpm.wpk\nBoth are returning the same error: Error 1819 - The WPK for this platform is not available\nScreenshot_624.png\n\nTest B \u2013 Custom EL5 WPK Package\n\nNext, I followed the official Wazuh documentation to rule out any packaging limitations and generated a custom WPK using the EL5-specific RPM: wazuh-agent-4.14.5-1.el5.x86_64.rpm\n\nReferences documents:\n\nhttps://documentation.wazuh.com/current/installation-guide/packages-list.html#wazuh-agent\nhttps://documentation.wazuh.com/current/user-manual/agent/agent-management/remote-upgrading/wpk-files/wpk-list.html\nhttps://documentation.wazuh.com/current/user-manual/agent/agent-management/remote-upgrading/wpk-files/create-custom-wpk.html\nAfter deploying the custom WPK, the result remained unchanged: Error 1819 - The WPK for this platform is not available\n\nIn addition, the manager's logs showed:\n\nwazuh-modulesd:agent-upgrade: WARNING: (8160): There are no valid agents to upgrade.\n\nScreenshot_628.png\nConclusion\n\nFrom my testing, I observed the following:\n\nThe RHEL 5 agent installs and communicates with the Wazuh manager without issues.\nBoth official and custom WPK-based upgrade attempts fail with Error 1819.\nThe manager does not create an upgrade task and indicates that there are no valid agents for the upgrade.\nBased on this behaviour, the issue does not appear to be caused simply by the absence of a WPK package. Instead, the RHEL 5 agent is not being accepted as a valid target for the remote upgrade workflow.\n\nRecommendation\n\nFor legacy operating systems such as RHEL 5, it is recommended to handle upgrades manually using the appropriate agent package for the platform(reference documents: https://documentation.wazuh.com/current/upgrade-guide/wazuh-agent/linux.html, [https://documentation.wazuh.com/current/deployment-options/wazuh-from-sources/wazuh-agent/index.html ](https://documentation.wazuh.com/current/deployment-options/wazuh-from-sources/wazuh-agent/index.html)) as these systems are outside the modern support scope and may not fully support the remote upgrade mechanism.\n\nThe same approach may be required for other end-of-life systems such as CentOS 5, Ubuntu 14/16, and SLES 11, depending on whether they exhibit similar behaviour with Error 1819.\n\nHope this information is helpful for you and clarifies your concern. Please let us know if you have any other queries & questions here. \n\nBest regards,\nNikhil\n\n**For LLM usage, a purpose is added as a comment.**",
+ "LGTM"
+ ],
+ "external_community": [
+ "window.WIZ_global_data = {\"AfY8Hf\":false,\"DpimGf\":false,\"EP1ykd\":[\"/_/*\"],\"FdrFJe\":\"-5028253355978131021\",\"HiPsbb\":1,\"Im6cmf\":\"/_/GroupsFrontendUi\",\"LVIXXb\":1,\"LoQv7e\":false,\"MT7f9b\":[],\"MUE6Ne\":\"groups-frontend\",\"PLnRge\":\"https://docs.google.com/picker\",\"QrtxK\":\"\",\"S06Grb\":\"\",\"S6lZl\":112976253,\"TSDtV\":\"%.@.[[null,[[45447917,null,true,null,null,null,\\\"OeRc3d\\\"],[45447918,null,true,null,null,null,\\\"paZwJ\\\"],[45749384,null,true,null,null,null,\\\"ervCtb\\\"],[45709804,null,false,null,null,null,\\\"PoJR0\\\"],[45697011,null,true,null,null,null,\\\"xNsvyb\\\"],[45734189,null,true,null,null,null,\\\"XGnjIb\\\"],[45448406,null,false,null,null,null,\\\"jKfwq\\\"],[45780063,null,false,null,null,null,\\\"j3OYnd\\\"],[45447921,null,false,null,null,null,\\\"qAj4w\\\"],[45655177,null,true,null,null,null,\\\"VvKUq\\\"],[45447945,null,true,null,null,null,\\\"ckfnge\\\"],[45459555,null,false,null,null,null,\\\"Imeoqb\\\"],[45447936,null,false,null,null,null,\\\"MohPG\\\"],[45532875,null,true,null,null,null,\\\"udsJQe\\\"],[45722772,null,true,null,null,null,\\\"MVCE3b\\\"],[45646796,null,false,null,null,null,\\\"Q877Ab\\\"],[45753484,null,true,null,null,null,\\\"G9u5lb\\\"],[45699412,null,true,null,null,null,\\\"xLy3Ce\\\"],[45447950,null,true,null,null,null,\\\"lU0ald\\\"],[45447953,null,true,null,null,null,\\\"jbTsAe\\\"],[45447919,null,false,null,null,null,\\\"KMEQCe\\\"],[45699332,30,null,null,null,null,\\\"grENN\\\"],[45447930,null,null,null,\\\"signed_out_users\\\",null,\\\"JTFNhb\\\"],[45447928,null,true,null,null,null,\\\"Wh7on\\\"],[45447938,null,true,null,null,null,\\\"uVDGld\\\"],[45709766,null,true,null,null,null,\\\"sSVuJ\\\"],[45776470,null,true,null,null,null,\\\"A9MFwf\\\"],[45447926,null,true,null,null,null,\\\"IUIyxe\\\"],[45793915,null,true,null,null,null,\\\"Vwrzsf\\\"],[45721043,null,true,null,null,null,\\\"FcV2Ie\\\"],[45447924,null,true,null,null,null,\\\"hMPU3c\\\"],[45447934,null,null,null,\\\"https://forms.gle/DuQUYavHhwfEo4sp9\\\",null,\\\"gvGLK\\\"],[45447932,null,false,null,null,null,\\\"jlJZI\\\"]],\\\"CAMSOx0+/7riEsDRsgYOrrekEBYEFtqsNuYD3f+wBAq20g0Kuv0FCqMuFtjVAgqrrQAOppMFCpO2DQ6Jng4K\\\"]]]\",\"Tb2qJf\":4,\"UUFaWc\":\"%.@.null,1000,2]\",\"Vvafkd\":false,\"Yllh3e\":\"%.@.1783855220430860,122392613,3222757752]\",\"YlwcZe\":\"%.@.3,[1],[3600],2,[15,4,13,14,12,2]]\",\"ZZZ7Uc\":\"\",\"b5W2zf\":\"default_GroupsFrontendUi\",\"cfb2h\":\"boq_groupsfrontendserver_20260706.01_p0\",\"eNnkwf\":\"1783855219\",\"eptZe\":\"/_/GroupsFrontendUi/\",\"fPDxwd\":[97493638,97493660,105739272],\"gGcLoe\":false,\"gSs3jc\":1,\"hpRnh\":1,\"hsFLT\":\"%.@.null,10,3]\",\"iCzhFc\":false,\"nQyAE\":{\"G9u5lb\":\"true\",\"lU0ald\":\"true\",\"grENN\":\"30\",\"uVDGld\":\"true\",\"Vwrzsf\":\"true\",\"XGnjIb\":\"true\",\"FcV2Ie\":\"true\",\"j3OYnd\":\"false\",\"qAj4w\":\"false\",\"MohPG\":\"false\",\"Q877Ab\":\"false\",\"xLy3Ce\":\"true\",\"KMEQCe\":\"false\",\"JTFNhb\":\"signed_out_users\",\"IUIyxe\":\"true\",\"OeRc3d\":\"true\",\"paZwJ\":\"true\",\"PoJR0\":\"false\",\"xNsvyb\":\"true\",\"ckfnge\":\"true\",\"udsJQe\":\"true\",\"jbTsAe\":\"true\",\"Wh7on\":\"true\",\"sSVuJ\":\"true\",\"gvGLK\":\"https://forms.gle/DuQUYavHhwfEo4sp9\",\"jlJZI\":\"false\",\"ervCtb\":\"true\",\"A9MFwf\":\"true\",\"jKfwq\":\"false\"},\"p9hQne\":\"https://www.gstatic.com/_/boq-groups/_/r/\",\"qwAQke\":\"GroupsFrontendUi\",\"qymVe\":\"sIinwKiKJckAeFSFVduN5-9JkOA\",\"rtQCxc\":-330,\"u4g7r\":\"%.@.null,1,3]\",\"w2btAe\":\"%.@.null,null,\\\"\\\",false,null,null,null,false]\",\"xn5OId\":false,\"xnI9P\":false,\"xwAfE\":true,\"y2FhP\":\"prod\",\"yFnxrf\":1884,\"zChJod\":\"%.@.]\"}; window[\"_F_toggles_default_GroupsFrontendUi\"] = [0x30081086, 0x30b0b301, 0x100c5ad, ]; (function(){'use strict';var a=window,d=a.performance,l=k();a.cc_latency_start_time=d&&d.now?0:d&&d.timing&&d.timing.navigationStart?d.timing.navigationStart:l;function k(){return d&&d.now?d.now():(new Date).getTime()}function n(e){if(d&&d.now&&d.mark){var g=d.mark(e);if(g)return g.startTime;if(d.getEntriesByName&&(e=d.getEntriesByName(e).pop()))return e.startTime}return k()}a.onaft=function(){n(\"aft\")};a._isLazyImage=function(e){return e.hasAttribute(\"data-src\")||e.hasAttribute(\"data-ils\")||e.getAttribute(\"loading\")===\"lazy\"}; a.l=function(e){function g(b){var c={};c[b]=k();a.cc_latency.push(c)}function m(b){var c=n(\"iml\");b.setAttribute(\"data-iml\",c);re"
+ ],
+ "url": "https://github.com/wazuh/community/issues/65639",
+ "labels": [
+ "Google Groups",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/Hasitha9796",
+ "review/quality",
+ "LGTM",
+ "reviewed"
+ ]
+ },
+ {
+ "number": 65631,
+ "title": "moiz.rafay https://wazuh.slack.com/archives/C0A933R8E/p1781958938368159",
+ "body": "Does any have Web Attack detection rules ? All type of web attacks\nhttps://wazuh.slack.com/archives/C0A933R8E/p1781958938368159",
+ "comments": [],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/65631",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "LGTM",
+ "reviewed"
+ ]
+ },
+ {
+ "number": 65613,
+ "title": "md.ammar27 https://wazuh.slack.com/archives/C0A933R8E/p1781874707845559",
+ "body": "Hello <#C0A933R8E>! I see that has been released, Is there some documentation\non feature parity with 4.x? Architecture, differences etc? I am planning on\nsetting up a new Wazuh cluster (separate from the current production version\nrunning 4.2.6 right now), and would prefer to wait for 5.x if it's release is\nnot that far. Happy to test out beta versions too. Thank you!\nhttps://wazuh.slack.com/archives/C0A933R8E/p1781874707845559",
+ "comments": [
+ "LGTM",
+ "LGTM"
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/65613",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/Stuti3097",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 65560,
+ "title": "Cloudflare integration ",
+ "body": "Hi Team \r\nJust wanted to ask how can i integrate cloudflare with wazuh without \r\npushing it onto a cloud storage.\r\nIs there a way of using the logpush API and directly integrate into wazuh \r\nusing the tag in ossec.conf file\r\n\r\n-- \r\nYou received this message because you are subscribed to the Google Groups \"Wazuh | Mailing List\" group.\r\nTo unsubscribe from this group and stop receiving emails from it, send an email to wazuh+unsubscribe@googlegroups.com.\r\nTo view this discussion visit https://groups.google.com/d/msgid/wazuh/aa2da237-ffc6-479d-b1cc-22518f64330cn%40googlegroups.com.\r\n",
+ "comments": [],
+ "external_community": [
+ "window.WIZ_global_data = {\"AfY8Hf\":false,\"DpimGf\":false,\"EP1ykd\":[\"/_/*\"],\"FdrFJe\":\"-6395302801109441162\",\"HiPsbb\":1,\"Im6cmf\":\"/_/GroupsFrontendUi\",\"LVIXXb\":1,\"LoQv7e\":false,\"MT7f9b\":[],\"MUE6Ne\":\"groups-frontend\",\"PLnRge\":\"https://docs.google.com/picker\",\"QrtxK\":\"\",\"S06Grb\":\"\",\"S6lZl\":112976253,\"TSDtV\":\"%.@.[[null,[[45447917,null,true,null,null,null,\\\"OeRc3d\\\"],[45447918,null,true,null,null,null,\\\"paZwJ\\\"],[45749384,null,true,null,null,null,\\\"ervCtb\\\"],[45709804,null,false,null,null,null,\\\"PoJR0\\\"],[45697011,null,true,null,null,null,\\\"xNsvyb\\\"],[45734189,null,true,null,null,null,\\\"XGnjIb\\\"],[45448406,null,false,null,null,null,\\\"jKfwq\\\"],[45780063,null,false,null,null,null,\\\"j3OYnd\\\"],[45447921,null,false,null,null,null,\\\"qAj4w\\\"],[45655177,null,true,null,null,null,\\\"VvKUq\\\"],[45447945,null,true,null,null,null,\\\"ckfnge\\\"],[45459555,null,false,null,null,null,\\\"Imeoqb\\\"],[45447936,null,false,null,null,null,\\\"MohPG\\\"],[45532875,null,true,null,null,null,\\\"udsJQe\\\"],[45722772,null,true,null,null,null,\\\"MVCE3b\\\"],[45646796,null,false,null,null,null,\\\"Q877Ab\\\"],[45753484,null,true,null,null,null,\\\"G9u5lb\\\"],[45699412,null,true,null,null,null,\\\"xLy3Ce\\\"],[45447950,null,true,null,null,null,\\\"lU0ald\\\"],[45447953,null,true,null,null,null,\\\"jbTsAe\\\"],[45447919,null,false,null,null,null,\\\"KMEQCe\\\"],[45699332,30,null,null,null,null,\\\"grENN\\\"],[45447930,null,null,null,\\\"signed_out_users\\\",null,\\\"JTFNhb\\\"],[45447928,null,true,null,null,null,\\\"Wh7on\\\"],[45447938,null,true,null,null,null,\\\"uVDGld\\\"],[45709766,null,true,null,null,null,\\\"sSVuJ\\\"],[45776470,null,true,null,null,null,\\\"A9MFwf\\\"],[45447926,null,true,null,null,null,\\\"IUIyxe\\\"],[45793915,null,true,null,null,null,\\\"Vwrzsf\\\"],[45721043,null,true,null,null,null,\\\"FcV2Ie\\\"],[45447924,null,true,null,null,null,\\\"hMPU3c\\\"],[45447934,null,null,null,\\\"https://forms.gle/DuQUYavHhwfEo4sp9\\\",null,\\\"gvGLK\\\"],[45447932,null,false,null,null,null,\\\"jlJZI\\\"]],\\\"CAMSOx0+/7riEsDRsgYOrrekEBYEFtqsNuYD3f+wBAq20g0Kuv0FCqMuFtjVAgqrrQAOppMFCpO2DQ6Jng4K\\\"]]]\",\"Tb2qJf\":4,\"UUFaWc\":\"%.@.null,1000,2]\",\"Vvafkd\":false,\"Yllh3e\":\"%.@.1783855227951564,40951232,3759060301]\",\"YlwcZe\":\"%.@.3,[1],[3600],2,[15,4,13,14,12,2]]\",\"ZZZ7Uc\":\"\",\"b5W2zf\":\"default_GroupsFrontendUi\",\"cfb2h\":\"boq_groupsfrontendserver_20260706.01_p0\",\"eNnkwf\":\"1783855225\",\"eptZe\":\"/_/GroupsFrontendUi/\",\"fPDxwd\":[97493638,97493660,105739272],\"gGcLoe\":false,\"gSs3jc\":1,\"hpRnh\":1,\"hsFLT\":\"%.@.null,10,3]\",\"iCzhFc\":false,\"nQyAE\":{\"G9u5lb\":\"true\",\"lU0ald\":\"true\",\"grENN\":\"30\",\"uVDGld\":\"true\",\"Vwrzsf\":\"true\",\"XGnjIb\":\"true\",\"FcV2Ie\":\"true\",\"j3OYnd\":\"false\",\"qAj4w\":\"false\",\"MohPG\":\"false\",\"Q877Ab\":\"false\",\"xLy3Ce\":\"true\",\"KMEQCe\":\"false\",\"JTFNhb\":\"signed_out_users\",\"IUIyxe\":\"true\",\"OeRc3d\":\"true\",\"paZwJ\":\"true\",\"PoJR0\":\"false\",\"xNsvyb\":\"true\",\"ckfnge\":\"true\",\"udsJQe\":\"true\",\"jbTsAe\":\"true\",\"Wh7on\":\"true\",\"sSVuJ\":\"true\",\"gvGLK\":\"https://forms.gle/DuQUYavHhwfEo4sp9\",\"jlJZI\":\"false\",\"ervCtb\":\"true\",\"A9MFwf\":\"true\",\"jKfwq\":\"false\"},\"p9hQne\":\"https://www.gstatic.com/_/boq-groups/_/r/\",\"qwAQke\":\"GroupsFrontendUi\",\"qymVe\":\"1zU_Sg1aEk2-Ay7VVq6H8GzYcdY\",\"rtQCxc\":-330,\"u4g7r\":\"%.@.null,1,3]\",\"w2btAe\":\"%.@.null,null,\\\"\\\",false,null,null,null,false]\",\"xn5OId\":false,\"xnI9P\":false,\"xwAfE\":true,\"y2FhP\":\"prod\",\"yFnxrf\":1884,\"zChJod\":\"%.@.]\"}; window[\"_F_toggles_default_GroupsFrontendUi\"] = [0x30081086, 0x30b0b301, 0x100c5ad, ]; (function(){'use strict';var a=window,d=a.performance,l=k();a.cc_latency_start_time=d&&d.now?0:d&&d.timing&&d.timing.navigationStart?d.timing.navigationStart:l;function k(){return d&&d.now?d.now():(new Date).getTime()}function n(e){if(d&&d.now&&d.mark){var g=d.mark(e);if(g)return g.startTime;if(d.getEntriesByName&&(e=d.getEntriesByName(e).pop()))return e.startTime}return k()}a.onaft=function(){n(\"aft\")};a._isLazyImage=function(e){return e.hasAttribute(\"data-src\")||e.hasAttribute(\"data-ils\")||e.getAttribute(\"loading\")===\"lazy\"}; a.l=function(e){function g(b){var c={};c[b]=k();a.cc_latency.push(c)}function m(b){var c=n(\"iml\");b.setAttribute(\"data-iml\",c);ret"
+ ],
+ "url": "https://github.com/wazuh/community/issues/65560",
+ "labels": [
+ "Google Groups",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/oajani",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 65553,
+ "title": "adam.kalinko https://wazuh.slack.com/archives/C0A933R8E/p1781690536233679",
+ "body": "Hello, I'm trying to write a rule for Wazuh that would detect port scanning.\nHow would go about detecting 15 UNIQUE ports being a part of connection\nbetween same src and dest IP in span of 60 seconds - for now my tries led me\nto make a rule that detects any 15 ports, not unique - as in 14 connections on\nport 53 (DNS) and then 1 more on port 123 for example - and it triggers the\nrule. Should I go other way about it, or is there a way to make sure that each\nport of the 15 is unique in span of 60 seconds?\nhttps://wazuh.slack.com/archives/C0A933R8E/p1781690536233679",
+ "comments": [
+ "Answered. Waiting for user response",
+ "Update #1: No response from user. ",
+ "Update #2: No further questions / response. I'll proceed and mark this request as done. "
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/65553",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/oajani",
+ "ambassador",
+ "review/quality",
+ "reviewer/mentor/Stuti",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 65552,
+ "title": "openedgarage https://wazuh.slack.com/archives/C0A933R8E/p1781686630586969",
+ "body": "if the wazuh manager is compromised does it mean all agents are vulnerable\ntoo? can an attacker upload and execute stuff if he doenst have access to\nagent cli? i am thinking that he can enable active response via agent central\nconfig , ask agent to download some malicious files from wazuh server and\nexecute them thus compromising the agents. any thoughts on that?\nhttps://wazuh.slack.com/archives/C0A933R8E/p1781686630586969",
+ "comments": [
+ "LGTM"
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/65552",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/oajani",
+ "ambassador",
+ "review/quality",
+ "reviewer/mentor/Stuti",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 65475,
+ "title": "al_zero_ https://discord.com/channels/1049711339578331186/1049711340316541004/1515366895597916350",
+ "body": "Hello everyone,\n\nI have an on-premises/local Wazuh deployment, and I want to integrate Google Workspace / Gmail security logs with Wazuh.\n\nMy goal is not to collect full email content or message bodies. I only need important security metadata such as login events, failed logins, suspicious access, admin activities, OAuth/app access, phishing-related alerts, and other audit/security events from Google Workspace.\n\nWhat is the recommended architecture for this integration with a local Wazuh setup?\n\nIs the correct approach:\n\nGoogle Workspace / Gmail audit logs \u2192 Google Cloud Logging \u2192 Pub/Sub \u2192 Wazuh GCP Pub/Sub module \u2192 custom decoders/rules?\n\nOr is there a better method for on-prem Wazuh?\n\nAlso, are there any existing decoders/rules or examples for parsing Google Workspace/Gmail audit logs in Wazuh?\n\nThank you.\nhttps://discord.com/channels/1049711339578331186/1049711340316541004/1515366895597916350",
+ "comments": [],
+ "external_community": [
+ "[Linked Discord conversation \u2014 not fetchable without a bot token in that server.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/65475",
+ "labels": [
+ "Discord",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "Resolved without feedback",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 65470,
+ "title": "thomas_highstakes https://discord.com/channels/1049711339578331186/1049711340316541004/1515259708917944500",
+ "body": "Hello,\n\nI'm trying to implement a custom rule to suppress the FIM alerts related to normal operations, specifically upgrades.\n\nThe aim is to suppress the alerts from rules 550,553,554 that I am getting when unattended-upgrades runs. But it's not working as expected ; in particular I can't seem to eliminate alerts when `syscheck.audit.process.parent_name` contains `preinst` or `postrm` .\n\nIn `/var/ossec/etc/rules/local_rules.xml`, I have added the following -- current state after many, many tries :\n\n```\n\n\n \n \n 550,553,554\n ^(dpkg|apt|apt-get|update-rc\\.d|systemctl|chmod|rm)$|.*(preinst|postinst|prerm|postrm)$\n Ignore FIM alerts triggered by child scripts of Debian/Ubuntu package managers.\n \n\n\n \n \n 550,553,554\n ^(dpkg|apt|apt-get|update-rc\\.d|systemctl|chmod|rm)$|.*(preinst|postinst|prerm|postrm)$\n Ignore FIM alerts triggered directly by Debian/Ubuntu package managers.\n \n\n```\n\nwhen running /var/ossec/bin/wazuh-logtest on the alert json, it clearly shows that rule 550 is triggered, and not mine.\n\non the agent, I have install v4.14.6-rc1 to have ebpf supported, the manager is still on v4.14.1, in case this is relevant.\n\nIf anyone can give me some pointers, it would be very much appreciated\nhttps://discord.com/channels/1049711339578331186/1049711340316541004/1515259708917944500",
+ "comments": [
+ "Hi @Thomas \ud83c\udfd4 | HighStakes.ch \n\nIn Wazuh FIM custom rules, you cannot use the alert field name directly as the field name in your rule. Instead, you must use the corresponding internal field name defined by Wazuh.\n\nAccording to the FIM field correspondence guide:\n\nprocess_name \u2192 audit.process.name\nDescription: The name of the process run by a user that triggered the event.\n\nTherefore, if you want to match against audit.process.name, you should use process_name as the field name in your custom rule.\n\nExample:\n```xml\n\n 550,553,554\n ^xxxxxxxx\n Ignore FIM alerts triggered by child scripts of Debian/Ubuntu package managers.\n\n\n\n 550,553,554\n ^xxxxxxxx\n Ignore FIM alerts triggered directly by Debian/Ubuntu package managers.\n\n```\n\nFor the complete mapping of FIM alert fields and their corresponding rule field names, refer to the [Wazuh documentation](https://documentation.wazuh.com/current/user-manual/capabilities/file-integrity/creating-custom-fim-rules.html#fim-alerts-fields-correspondence).\n\nYou can also review the Wazuh regular expression documentation for details on supported regex types and syntax:\n[Regex](https://documentation.wazuh.com/current/user-manual/ruleset/ruleset-xml-syntax/regex.html)\n\nLet me know if you need any further assistance."
+ ],
+ "external_community": [
+ "[Linked Discord conversation \u2014 not fetchable without a bot token in that server.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/65470",
+ "labels": [
+ "Discord",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "Resolved",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 65454,
+ "title": "WAZUH - migration from v4.x to 5.x - toolset https://www.reddit.com/r/Wazuh/comments/1u3y7j9/wazuh_migration_from_v4x_to_5x_toolset/",
+ "body": "Hi All :)\n\nI am just wondering, now when Wazuh 5 beta is out and available, what will be the migration strategy.\n\nIs there a plan to create some support toolset which will help to automate, semi-automate migration of established installations with current version of Wazuh to the new Wazuh 5 - new architecture - in future?\n\nI thinking about some migration scripts or whatsoever, to make this migration task as simple as possible?\n\nThanks for any answers\n\nLukas\n\n submitted by /u/Fun_Advantage3812\n\n[link] [comments]\nhttps://www.reddit.com/r/Wazuh/comments/1u3y7j9/wazuh_migration_from_v4x_to_5x_toolset/",
+ "comments": [
+ "Hi @jr0me \n\nYour response looks good to me. However, you can improve the response by sharing the currently open GitHub issues with the users to see the progress.\nPlease share the update as soon as you have discussed with the internal team. Thanks!",
+ "LGTM"
+ ],
+ "external_community": [],
+ "url": "https://github.com/wazuh/community/issues/65454",
+ "labels": [
+ "Reddit",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/Hasitha9796",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 65442,
+ "title": "vishnuprasad.magnifie https://wazuh.slack.com/archives/C0A933R8E/p1781255271053859",
+ "body": "Hi all, Can I know minimum EC2 Instance requirement to deploy wazuh server?\nhttps://wazuh.slack.com/archives/C0A933R8E/p1781255271053859",
+ "comments": [
+ "LGTM",
+ "Answered by @IsmailChemmala:\n\nHi,\n\nThe minimum EC2 instance size depends on the deployment type (single-node (AIO) vs distributed) and the number of monitored endpoints.\n\nFor a lab, PoC, or very small environment, a single-node Wazuh deployment can typically run on:\n\n2 vCPU\n4 GB RAM (8 GB recommended)\n50+ GB SSD storage\n\n\nFor a production environment, the sizing depends on:\n\nNumber of agents/endpoints\nNumber of Servers\nEvents per second (EPS)\nLog retention period\nEnabled modules\nNetwork devices and cloud services etc\n\n\nAs a general starting point for a small production deployment:\n\n4 vCPU\n8\u201316 GB RAM\n100+ GB SSD storage\nIf you can share the expected number of agents, log volume/EPS, and retention requirements, we can provide a more accurate sizing recommendation.\n\nPlease refer to this architecture document https://documentation.wazuh.com/current/quickstart.html\n\nAdditionally, refer to this installation document https://documentation.wazuh.com/current/installation-guide/wazuh-indexer/step-by-step.html\n\nI hope it helps. Please let us know if you have any further questions or concerns."
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/65442",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 65435,
+ "title": "amit.j https://wazuh.slack.com/archives/C0A933R8E/p1781243968724029",
+ "body": "Hi Team, Can I know how we can set anti tampering policy for Wazuh agents?\nhttps://wazuh.slack.com/archives/C0A933R8E/p1781243968724029",
+ "comments": [
+ "Answered by Bony:\nWazuh provides an agent anti-tampering feature that helps prevent unauthorized removal of the Wazuh Linux agent from monitored endpoints. When this feature is enabled, the Wazuh Linux agent cannot be uninstalled without validation from the Wazuh manager.\n\nYou can enable anti-tampering either through the[ centralized agent configuration](https://documentation.wazuh.com/current/user-manual/reference/centralized-configuration.html) on the Wazuh manager or by editing the agent\u2019s local configuration file. For detailed steps, refer to the[ Wazuh anti-tampering documentation](https://documentation.wazuh.com/current/user-manual/agent/agent-management/anti-tampering.html#enabling-anti-tampering).\n\nTo enable this protection, add the following configuration:\n\n yes\nOnce configured, endpoint users will not be able to uninstall the agent without manager approval. If the agent needs to be removed, follow the [documented ](https://documentation.wazuh.com/current/user-manual/agent/agent-management/anti-tampering.html#uninstalling-an-agent-with-anti-tampering-enabled)process for uninstalling agents with anti-tampering enabled.\nThis feature helps ensure that agents remain protected and reduces the risk of unauthorized uninstallation or tampering.\n\n\nFor Windows, the anti-tampering option is not currently available, and Wazuh does not provide a built-in method to block agent uninstallation in the same way as on Linux.\nHowever, if you are using [Windows Group Policy](https://learn.microsoft.com/en-us/training/modules/create-configure-group-policy-objects-active-directory/), you can enforce restrictions to prevent users from uninstalling the Wazuh agent package. For example, you can use GPO policies to deny uninstall actions or restrict access to software removal options."
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/65435",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "Resolved",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 65433,
+ "title": "nivedita_29219 https://discord.com/channels/1049711339578331186/1049711340316541004/1514785073545609388",
+ "body": "hi.. https://wazuh.com/blog/automating-linux-endpoint-hardening-with-wazuh/ gives details about ubuntu 24 .. i have ubuntu22 servers.. is there any such script or guide available for ubuntu22?\nhttps://discord.com/channels/1049711339578331186/1049711340316541004/1514785073545609388",
+ "comments": [
+ "end of shift",
+ "LGTM"
+ ],
+ "external_community": [
+ "[Linked Discord conversation \u2014 not fetchable without a bot token in that server.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/65433",
+ "labels": [
+ "Discord",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 65421,
+ "title": "daryllanthony.fortuna https://wazuh.slack.com/archives/C07CCCCGHHP/p1781183023767659",
+ "body": "There is an issue with my vulnerability scanner wazuh\nMy current version is 4.14.1 wazuh single node docker setup\nThe scanner is previously working\nhttps://wazuh.slack.com/archives/C07CCCCGHHP/p1781183023767659",
+ "comments": [
+ "First response has been provided to the user. This is a known issue affecting the package zip file and has been re-uploaded. More information here: https://wazuh-team.slack.com/archives/GPAMKJHDM/p1781195132410629?thread_ts=1781180287.990859&cid=GPAMKJHDM\n\n"
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/65421",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "review/moderation",
+ "reviewer/team/community/oajani",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 65414,
+ "title": "sank3t.cigit https://wazuh.slack.com/archives/C07CNG3M11N/p1781160965098319",
+ "body": "Hello @ganalytics,\n\nIs it possible to create a anomaly detection logic on Wazuh?\nOur primary purpose would be to detect certain parameters (hits, counts, frequency) if it has increased or decreased by certain threshold percentage?\nAnd this comparison is supposed to be does based on a time window like previous 24 hour compared to next 24 hour window.\n\nAlso, display these alerts in some visual format on a dashboard.\n\nSome ideas or solutions any of you would have implemented in your Wazuh environment would be very helpful.\n\nThanks!\nhttps://wazuh.slack.com/archives/C07CNG3M11N/p1781160965098319",
+ "comments": [],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/65414",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/oajani",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 65365,
+ "title": "Wazuh version upgrade from 4.2.7 to a latest version ",
+ "body": "Hi Team,\r\n\r\nWe have an old Wazuh version running in our environment and are planning \r\nfor an upgrade.\r\n\r\nAs this is a very old version, could you let us know which upgrade path to \r\nfollow?\r\n\r\n1 - Can we directly upgrade to the latest 4.14 version?\r\n2 - We have Windows agents installed on several devices. Should these \r\nagents also be upgraded to the latest version?\r\n3 - Are there any other key areas that I should be aware of?\r\n\r\nThank you\r\n\r\n-- \r\nYou received this message because you are subscribed to the Google Groups \"Wazuh | Mailing List\" group.\r\nTo unsubscribe from this group and stop receiving emails from it, send an email to wazuh+unsubscribe@googlegroups.com.\r\nTo view this discussion visit https://groups.google.com/d/msgid/wazuh/4bab7832-e2e2-425d-96c5-abd1889e1b33n%40googlegroups.com.\r\n",
+ "comments": [
+ "\nhasitha.upekshitha@wazuh.com\nJun 9, 2026, 2:07:34\u202fPM\nto Wazuh | Mailing List\nHi Shenal,\nI have replicated this on my end and successfully upgraded it to the latest version.\n\nTo upgrade to Wazuh 4.2.7, you should follow the official migration process for both OpenDistro/OpenSearch components and Kibana/Wazuh Dashboard.\n\nI recommend following this sequence: Indexer \u2192 Manager \u2192 Filebeat \u2192 Dashboard, as this ensures a smooth upgrade flow.\n\nStart with the Wazuh Indexer migration guide:\nhttps://documentation.wazuh.com/4.3/migration-guide/wazuh-indexer.html\n\nFollow the steps up to step 5. In step 6, instead of installing a fixed version of the indexer, you can install the package without specifying a version so it upgrades to the latest available release, for example:\n\n```bash\napt-get -y install wazuh-indexer\n```\nThen continue with the remaining steps up to step 18.\n\nAfter that, proceed with upgrading the Wazuh Manager and Filebeat by following the upgrade guide:\nhttps://documentation.wazuh.com/current/upgrade-guide/upgrading-central-components.html#upgrading-the-wazuh-server\n\nSince you are upgrading from an earlier version, you may need to update your ossec.conf, especially for:\n\nruleset configuration blocks\n\nvulnerability detection settings\n\nindexer connector configuration\n\nYou can refer to the shared documentation section to apply the correct updates.\n\nContinue the process through Filebeat installation and configuration as described.\n\nFinally, follow the Wazuh Dashboard migration guide (not the general upgrade guide):\nhttps://documentation.wazuh.com/4.3/migration-guide/wazuh-dashboard.html\n\nIn step 3, make sure to run:\n\napt-get -y install wazuh-dashboard\nOtherwise, an older version may be installed by default and follow the rest of the steps to complete the configuration.\n\nLet me know if you face any issues during the process, and I\u2019ll be happy to help further.\n\n"
+ ],
+ "external_community": [
+ "window.WIZ_global_data = {\"AfY8Hf\":false,\"DpimGf\":false,\"EP1ykd\":[\"/_/*\"],\"FdrFJe\":\"4128192124880452969\",\"HiPsbb\":1,\"Im6cmf\":\"/_/GroupsFrontendUi\",\"LVIXXb\":1,\"LoQv7e\":false,\"MT7f9b\":[],\"MUE6Ne\":\"groups-frontend\",\"PLnRge\":\"https://docs.google.com/picker\",\"QrtxK\":\"\",\"S06Grb\":\"\",\"S6lZl\":112976253,\"TSDtV\":\"%.@.[[null,[[45447917,null,true,null,null,null,\\\"OeRc3d\\\"],[45447918,null,true,null,null,null,\\\"paZwJ\\\"],[45749384,null,true,null,null,null,\\\"ervCtb\\\"],[45709804,null,false,null,null,null,\\\"PoJR0\\\"],[45697011,null,true,null,null,null,\\\"xNsvyb\\\"],[45734189,null,true,null,null,null,\\\"XGnjIb\\\"],[45448406,null,false,null,null,null,\\\"jKfwq\\\"],[45780063,null,false,null,null,null,\\\"j3OYnd\\\"],[45447921,null,false,null,null,null,\\\"qAj4w\\\"],[45655177,null,true,null,null,null,\\\"VvKUq\\\"],[45447945,null,true,null,null,null,\\\"ckfnge\\\"],[45459555,null,false,null,null,null,\\\"Imeoqb\\\"],[45447936,null,false,null,null,null,\\\"MohPG\\\"],[45532875,null,true,null,null,null,\\\"udsJQe\\\"],[45722772,null,true,null,null,null,\\\"MVCE3b\\\"],[45646796,null,false,null,null,null,\\\"Q877Ab\\\"],[45753484,null,true,null,null,null,\\\"G9u5lb\\\"],[45699412,null,true,null,null,null,\\\"xLy3Ce\\\"],[45447950,null,true,null,null,null,\\\"lU0ald\\\"],[45447953,null,true,null,null,null,\\\"jbTsAe\\\"],[45447919,null,false,null,null,null,\\\"KMEQCe\\\"],[45699332,30,null,null,null,null,\\\"grENN\\\"],[45447930,null,null,null,\\\"signed_out_users\\\",null,\\\"JTFNhb\\\"],[45447928,null,true,null,null,null,\\\"Wh7on\\\"],[45447938,null,true,null,null,null,\\\"uVDGld\\\"],[45709766,null,true,null,null,null,\\\"sSVuJ\\\"],[45776470,null,true,null,null,null,\\\"A9MFwf\\\"],[45447926,null,true,null,null,null,\\\"IUIyxe\\\"],[45793915,null,true,null,null,null,\\\"Vwrzsf\\\"],[45721043,null,true,null,null,null,\\\"FcV2Ie\\\"],[45447924,null,true,null,null,null,\\\"hMPU3c\\\"],[45447934,null,null,null,\\\"https://forms.gle/DuQUYavHhwfEo4sp9\\\",null,\\\"gvGLK\\\"],[45447932,null,false,null,null,null,\\\"jlJZI\\\"]],\\\"CAMSOx0+/7riEsDRsgYOrrekEBYEFtqsNuYD3f+wBAq20g0Kuv0FCqMuFtjVAgqrrQAOppMFCpO2DQ6Jng4K\\\"]]]\",\"Tb2qJf\":4,\"UUFaWc\":\"%.@.null,1000,2]\",\"Vvafkd\":false,\"Yllh3e\":\"%.@.1783855244048742,122397255,3441995392]\",\"YlwcZe\":\"%.@.3,[1],[3600],2,[15,4,13,14,12,2]]\",\"ZZZ7Uc\":\"\",\"b5W2zf\":\"default_GroupsFrontendUi\",\"cfb2h\":\"boq_groupsfrontendserver_20260706.01_p0\",\"eNnkwf\":\"1783855243\",\"eptZe\":\"/_/GroupsFrontendUi/\",\"fPDxwd\":[97493638,97493660,105739272],\"gGcLoe\":false,\"gSs3jc\":1,\"hpRnh\":1,\"hsFLT\":\"%.@.null,10,3]\",\"iCzhFc\":false,\"nQyAE\":{\"G9u5lb\":\"true\",\"lU0ald\":\"true\",\"grENN\":\"30\",\"uVDGld\":\"true\",\"Vwrzsf\":\"true\",\"XGnjIb\":\"true\",\"FcV2Ie\":\"true\",\"j3OYnd\":\"false\",\"qAj4w\":\"false\",\"MohPG\":\"false\",\"Q877Ab\":\"false\",\"xLy3Ce\":\"true\",\"KMEQCe\":\"false\",\"JTFNhb\":\"signed_out_users\",\"IUIyxe\":\"true\",\"OeRc3d\":\"true\",\"paZwJ\":\"true\",\"PoJR0\":\"false\",\"xNsvyb\":\"true\",\"ckfnge\":\"true\",\"udsJQe\":\"true\",\"jbTsAe\":\"true\",\"Wh7on\":\"true\",\"sSVuJ\":\"true\",\"gvGLK\":\"https://forms.gle/DuQUYavHhwfEo4sp9\",\"jlJZI\":\"false\",\"ervCtb\":\"true\",\"A9MFwf\":\"true\",\"jKfwq\":\"false\"},\"p9hQne\":\"https://www.gstatic.com/_/boq-groups/_/r/\",\"qwAQke\":\"GroupsFrontendUi\",\"qymVe\":\"YW5hmfmMv4TkFzGxp_KAHxBAPC4\",\"rtQCxc\":-330,\"u4g7r\":\"%.@.null,1,3]\",\"w2btAe\":\"%.@.null,null,\\\"\\\",false,null,null,null,false]\",\"xn5OId\":false,\"xnI9P\":false,\"xwAfE\":true,\"y2FhP\":\"prod\",\"yFnxrf\":1884,\"zChJod\":\"%.@.]\"}; window[\"_F_toggles_default_GroupsFrontendUi\"] = [0x30081086, 0x30b0b301, 0x100c5ad, ]; (function(){'use strict';var a=window,d=a.performance,l=k();a.cc_latency_start_time=d&&d.now?0:d&&d.timing&&d.timing.navigationStart?d.timing.navigationStart:l;function k(){return d&&d.now?d.now():(new Date).getTime()}function n(e){if(d&&d.now&&d.mark){var g=d.mark(e);if(g)return g.startTime;if(d.getEntriesByName&&(e=d.getEntriesByName(e).pop()))return e.startTime}return k()}a.onaft=function(){n(\"aft\")};a._isLazyImage=function(e){return e.hasAttribute(\"data-src\")||e.hasAttribute(\"data-ils\")||e.getAttribute(\"loading\")===\"lazy\"}; a.l=function(e){function g(b){var c={};c[b]=k();a.cc_latency.push(c)}function m(b){var c=n(\"iml\");b.setAttribute(\"data-iml\",c);ret"
+ ],
+ "url": "https://github.com/wazuh/community/issues/65365",
+ "labels": [
+ "Google Groups",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "Resolved without feedback",
+ "reviewer/team/community/sakib789",
+ "review/quality",
+ "review/quality/community_team/internal",
+ "LGTM",
+ "reviewed"
+ ]
+ },
+ {
+ "number": 65273,
+ "title": "thesillygoose. https://discord.com/channels/1049711339578331186/1049711340316541004/1511654035248386189",
+ "body": "Having an issue where my Sysmon logs are only being forwarded when I restart `WazuhSvc` on all of my endpoints. The Sysmon config is identical on all endpoints -- I can confirm that regular windows eventchannel events are being forwarded to Wazuh with very little latency, and endpoint events (both sysmon and regular) are being generated in Event Viewer.\n\nSnippet of endpoint `ossec.conf`:\n```xml\n \n Microsoft-Windows-Sysmon/Operational\n eventchannel\n \n```\n\nRestarting the virtual machine which Wazuh is hosted makes no difference either. These machines are all tunneled via TailScale, which I can confirm has a healthy node-to-node connection throughout the mesh.\nhttps://discord.com/channels/1049711339578331186/1049711340316541004/1511654035248386189",
+ "comments": [
+ "This issue is stale because it has been open for 15 days with no activity.",
+ "LGTM. Closing this as the user is inactive. Please reopen if the user replies back."
+ ],
+ "external_community": [
+ "[Linked Discord conversation \u2014 not fetchable without a bot token in that server.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/65273",
+ "labels": [
+ "Discord",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "stale",
+ "reviewer/team/community/sakib789",
+ "review/quality/community_team/internal",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 65254,
+ "title": "vijay.y https://wazuh.slack.com/archives/C0A933R8E/p1780391914038309",
+ "body": "Hey <@U01HXEV6SL8> How can I monitor the Wazuh Manager server itself for: \u2022\nVulnerabilities and missing security patches \u2022 Malware detection \u2022 Rootkits \u2022\nFile integrity monitoring (FIM) \u2022 SSH brute-force attacks and authentication\nattacks \u2022 Security hygiene and hardening issues \u2022 Suspicious processes and\nunauthorized changes\nhttps://wazuh.slack.com/archives/C0A933R8E/p1780391914038309",
+ "comments": [
+ "Closing this. Reopen if the user replies back."
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/65254",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/sakib789",
+ "review/quality/community_team/internal",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 65226,
+ "title": "What actually moved the needle on our alert fatigue (Wazuh + some automation, lessons after ~6 months) https://www.reddit.com/r/Wazuh/comments/1tsx2oz/what_actually_moved_the_needle_on_our_alert/",
+ "body": "Sharing this because the \"just tune your rules\" advice you see everywhere skips the part where you're drowning while you tune them. Here's what actually reduced our noise, roughly in order of impact.\n\n1. Dedupe before triage, not after. A huge chunk of our \"volume\" was the same alert firing repeatedly from the same source. Collapsing those into one enriched event before a human ever saw it cut the queue more than any rule change did.\n\n2. Enrich at ingest, not at investigation. Pulling asset criticality, recent CVEs for the host, and basic threat-intel context onto the alert meant the analyst wasn't opening five tabs per alert. The triage decision got faster because the context was already there.\n\n3. Write the \"why this fired\" in plain language. We started auto-generating a one-line human summary of each alert (\"SSH brute force against [host], which runs [service], no successful auth yet\"). Sounds trivial. Cut onboarding time for junior analysts dramatically.\n\n4. Accept that some tuning is permanent triage. Not every rule can be made quiet. Some you just route straight to a low-priority bucket and review weekly instead of in real time.\n\nNone of this is novel, but the sequencing mattered \u2014 we wasted a month tuning rules before realizing dedup + enrichment was the bigger lever. Curious what worked for others, especially anyone running this at a smaller team where you can't just throw headcount at the queue.\n\n submitted by /u/Annual_Bear_4733\n\n[link] [comments]\nhttps://www.reddit.com/r/Wazuh/comments/1tsx2oz/what_actually_moved_the_needle_on_our_alert/",
+ "comments": [
+ "Answered"
+ ],
+ "external_community": [],
+ "url": "https://github.com/wazuh/community/issues/65226",
+ "labels": [
+ "Reddit",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/sakib789",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 65224,
+ "title": "naamabiton https://wazuh.slack.com/archives/C07CCCCGHHP/p1780216602926279",
+ "body": "Hi everyone,\nI am planning to manage our Wazuh manager configuration files (`ossec.conf`, custom rules, and decoders) in Git, but I am struggling with the architectural workflow.\n\nRight now, it feels like \"reverse version control.\" Our team tends to change and test configuration files directly on our AWS EC2 instance first, and then push those changes back to Git later...? This is creating a lot of configuratiis it recommended to managed Wazuh Manager under git repo?\nhttps://wazuh.slack.com/archives/C07CCCCGHHP/p1780216602926279",
+ "comments": [
+ "This issue is stale because it has been open for 15 days with no activity.",
+ "Closing this due to inactivity"
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/65224",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "stale",
+ "Resolved without feedback",
+ "reviewer/team/community/sakib789",
+ "closed/inactivity",
+ "review/quality/community_team/internal",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 65223,
+ "title": "mohiuddin.shahbaaz1 https://wazuh.slack.com/archives/C0A933R8E/p1780209383242559",
+ "body": "Hello Team I have been receiving logs from Sophos MDR without any issues until\n27th May after which I don't see any logs in my dashboard. There has been no\nchanges in the environment. Please advise as to what could be the issue and\nwhat we need to check.\nhttps://wazuh.slack.com/archives/C0A933R8E/p1780209383242559",
+ "comments": [
+ "LGTM",
+ "LGTM"
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/65223",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/bonyjohn05",
+ "review/quality",
+ "LGTM",
+ "reviewed"
+ ]
+ },
+ {
+ "number": 65222,
+ "title": "How do you monitor Linux servers in Wazuh? https://www.reddit.com/r/Wazuh/comments/1trz42b/how_do_you_monitor_linux_servers_in_wazuh/",
+ "body": "I am trying to find a reliable and maintainable way to monitor Linux systems, but I am struggling to find the right approach. The default, out-of-the-box options don't give me what I need, but the advanced alternatives require too much maintenance.\n\nRight now, my setup is very basic:\n\nSSH logs: I have specific alerts for things like non-existent usernames or logins from external IP addresses. Bash history: I use File Integrity Monitoring (FIM) via Wazuh just to alert when the history file changes. I also tried monitoring cron files with FIM, but I stopped doing that. I have spent a lot of time testing other tools, but each had significant downsides:\n\nAuditd: There are many advanced rules and configurations available, but maintaining this service is difficult. The logs are not user-friendly, and you have to do extra work with Wazuh and OpenSearch decoders just to see the actual commands, because they are encoded in hex. I eventually gave up on Auditd because it takes too much time to manage. Tetragon (eBPF): I spent a lot of time trying to get this to monitor inbound and outbound traffic. The problem is that you need different queries for different Linux kernels and architectures. I got it working on Ubuntu 24.04 ARM, but the same config failed on x86-64. Testing different configurations across different distributions and versions is a huge time sink. You also need to write custom decoders to normalize the logs. For me it is a middle-ground option, like Auditd, it's generate huge noise of events. Falco: This works well as a runtime detection engine, but it is not designed to log all events to a SIEM. It is mostly focused on container and microservice security. While it has some overlap with standard Linux OS monitoring, it is not the right tool for full system logging. Sysmon for Linux: Out of the box, it did not show all event types, which means it requires a custom configuration file. However, out of all the options I tried, this is the one I like the most so far. If you have experience with Linux security monitoring, how do you handle it? What tools or configurations do you use to get good visibility without spending all your time on maintenance?\n\nPlease share your thoughts and experience in the comments.\n\n submitted by /u/athanielx\n\n[link] [comments]\nhttps://www.reddit.com/r/Wazuh/comments/1trz42b/how_do_you_monitor_linux_servers_in_wazuh/",
+ "comments": [
+ "This issue is stale because it has been open for 15 days with no activity.",
+ "Closing this. Please reopen if the user replies back\n"
+ ],
+ "external_community": [],
+ "url": "https://github.com/wazuh/community/issues/65222",
+ "labels": [
+ "Reddit",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "stale",
+ "reviewer/team/community/sakib789",
+ "ambassador",
+ "review/quality/community_team/internal",
+ "reviewer/mentor/Bony",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 65221,
+ "title": "daryllanthony.fortuna https://wazuh.slack.com/archives/C07BZJY86G3/p1780145087817649",
+ "body": "Hello\nI have an issue with the user. Every time I docker compose down the user I created in the dashboard, it always removes it using the default docker compose file. What is the best solution for this issue? Thanks\nhttps://wazuh.slack.com/archives/C07BZJY86G3/p1780145087817649",
+ "comments": [
+ "@Nikhil201Gurjar The initial response is very well structured. Good job on this community!",
+ "LGTM"
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/65221",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/sakib789",
+ "review/quality",
+ "LGTM",
+ "reviewed"
+ ]
+ },
+ {
+ "number": 65203,
+ "title": "rohit.joshi https://wazuh.slack.com/archives/C07CNG3M11N/p1780045977528399",
+ "body": "hlo guys i am new to these community and so i have some questions can you guys help me\nhttps://wazuh.slack.com/archives/C07CNG3M11N/p1780045977528399",
+ "comments": [],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/65203",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "Resolved without feedback",
+ "reviewer/team/community/sakib789",
+ "review/quality/community_team/internal",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 65145,
+ "title": "harshradadiya42 https://wazuh.slack.com/archives/C0A933R8E/p1779863862535759",
+ "body": "Hi <@U01HXEV6SL8>..!! Is it possible in Wazuh to manually delete specific logs\nat a specific time based on a rule ID? If yes, how can this be done from the\nWazuh dashboard?\nhttps://wazuh.slack.com/archives/C0A933R8E/p1779863862535759",
+ "comments": [
+ "LGTM. Great answer.",
+ "Good answer"
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/65145",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/Stuti3097",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 65128,
+ "title": "robinchann https://discord.com/channels/1049711339578331186/1049711340316541004/1508760702465343618",
+ "body": "Hey team iam working on Active response correlation and i grouped multiple rules into single rule but iam getting warning\n\nMay 26, 2026 @ 14:38:25.000 wazuh-analysisd WARNING (7617): Signature ID '113141' was not found and will be ignored in the 'if_sid' option of rule '290100'.\n\n\nSAMPLE RULE ID:\n\n 61616\n ^technique_id=T1113,technique_name=Recall Enabled via Registry Delete$\n Sysmon - Event 14: RegistryEvent (Key and Value Rename) by $(win.eventdata.image)\n \n T1113\n \n no_full_log\n sysmon_event_14\n\n\nCORRELATION RULE ID:\n\n \n 100099,100634,100651,101703,108130,108146,108148,113141,140109,140517,140542,140578,140597,140629,190104,80239,80270,800205,101204,100703,910045,700202,700206,290025,290026,290027,290028,290032,290051,290052,290053,290054,290055,290056,290059,290060,290061\n Automated Firewall Block Triggered for Source IP.\n firewall_drop,active_response\n \n\n\nthe warning message shown rule ids are already in our system but still iam getting this kinda of error what to do anyone have any\nhttps://discord.com/channels/1049711339578331186/1049711340316541004/1508760702465343618",
+ "comments": [
+ "This issue is stale because it has been open for 15 days with no activity.",
+ "Closing this due to inactivity."
+ ],
+ "external_community": [
+ "[Linked Discord conversation \u2014 not fetchable without a bot token in that server.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/65128",
+ "labels": [
+ "Discord",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "stale",
+ "Resolved without feedback",
+ "reviewer/team/community/sakib789",
+ "closed/inactivity",
+ "review/quality/community_team/internal",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 65119,
+ "title": "syscollector events not archiving due to data mapping issue ",
+ "body": "Our organisation uses Wazuh as a centralised logging system and SIEM.\r\nDue to the desire of capturing EVERYTHING, not just alerts, we archive all \r\nlogs.\r\n\r\nossec.conf:\r\nyes\r\n\r\nfilebeat.yml:\r\nfilebeat.modules:\r\n - module: wazuh\r\n alerts:\r\n enabled: true\r\n archives:\r\n enabled: true\r\n\r\nRecently (I'm not sure after which wazuh upgrade), filebeat has started \r\nemitting warnings:\r\n[elasticsearch]#011elasticsearch/client.go:408#011Cannot index event \r\npublisher.Event{Content:beat.Event{Timestamp:time.Time{wall:0xc27d52deaf11b27b, \r\next:346113401807658, loc:(*time.Location)(0x42417a0)}, \r\nMeta:{\"pipeline\":\"filebeat-7.10.2-wazuh-archives-pipeline\"},\r\n\r\n\\\"decoder\\\":{\\\"name\\\":\\\"syscollector\\\"},\\\"data\\\":{\\\"type\\\":\\\"dbsync_services\\\",\\\"service\\\":{\\\"service_id\\\":\\\"user@0\\\",\\\"service_name\\\":\\\"user@0\\\",\\\"service_description\\\":\\\"User \r\nManager for UID 0\\\",\\\"service_type\\\":\\\" \r\n\\\",\\\"service_state\\\":\\\"active\\\",\\\"service_sub_state\\\":\\\"running\\\",\\\"service_enabled\\\":\\\"static\\\",\\\"service_start_type\\\":\\\" \r\n\\\",\\\"service_restart\\\":\\\" \r\n\\\",\\\"service_frequency\\\":\\\"0\\\",\\\"service_starts_on_mount\\\":\\\"0\\\",\\\"service_starts_on_path_modified\\\":\\\" \r\n\\\",\\\"service_starts_on_not_empty_directory\\\":\\\" \r\n\\\",\\\"service_inetd_compatibility\\\":\\\"0\\\",\\\"process_pid\\\":\\\"0\\\",\\\"process_executable\\\":\\\"/lib/systemd/system/user@.service\\\",\\\"process_args\\\":\\\" \r\n\\\",\\\"process_user_name\\\":\\\"0\\\",\\\"process_group_name\\\":\\\" \r\n\\\",\\\"file_path\\\":\\\" \\\",\\\"service_address\\\":\\\" \\\",\\\"log_file_path\\\":\\\" \r\n\\\",\\\"error_log_file_path\\\":\\\" \r\n\\\",\\\"service_exit_code\\\":\\\"0\\\",\\\"service_win32_exit_code\\\":\\\"0\\\",\\\"service_object_path\\\":\\\"/org/freedesktop/systemd1/unit/user_400_2eservice\\\",\\\"service_target_ephemeral_id\\\":\\\"0\\\",\\\"service_target_address\\\":\\\"/\\\"}\r\n\r\n(status=400): {\"type\":\"mapper_parsing_exception\",\"reason\":\"failed to parse \r\nfield [data.service] of type [keyword] in document with id \r\n'gT5cYZ4BX1qU-zniDIHk'. Preview of field's value: '{service_state=active, \r\nfile_path= , process_user_name=0, service_address= , \r\nservice_starts_on_not_empty_directory= , log_file_path= , \r\nservice_id=user@0, service_frequency=0, process_group_name= , \r\nservice_sub_state=running, service_description=User Manager for UID 0, \r\nservice_object_path=/org/freedesktop/systemd1/unit/user_400_2eservice, \r\nerror_log_file_path= , process_args= , service_name=user@0, \r\nprocess_executable=/lib/systemd/system/user@.service, \r\nservice_enabled=static, service_exit_code=0, process_pid=0, \r\nservice_inetd_compatibility=0, service_type= , service_starts_on_mount=0, \r\nservice_start_type= , service_starts_on_path_modified= , \r\nservice_target_ephemeral_id=0, service_win32_exit_code=0, \r\nservice_target_address=/, service_restart= \r\n}'\",\"caused_by\":{\"type\":\"illegal_state_exception\",\"reason\":\"Can't get text \r\non a START_OBJECT at 1:190\"}}\r\n\r\nIn particular:\r\n* Can't map 'data.service' of this payload - currently is a 'keyword' in \r\nelasticsearch, but data is parsed as an object\r\n* Decoder is 'syscollector'\r\n\r\nLooking in the decoder rules, ruleset/rules/0016-wazuh_rules.xml, I can see \r\nthat syscollector is intentionally set to level 0 to ignore the events.\r\nHowever, since we are archiving all data, these payloads are still pushed \r\ninto the elasticsearch backend archives, but conflict with the mapping in \r\nthere already.\r\n\r\nAs I see it, there are two approaches:\r\n\r\n* hardcode the pipeline mapping into archives to force data.service to be \r\nan object (or flattened)\r\n* disable the syscollector entries from being archived (pipeline drop?)\r\n\r\nAnyone know why this behaviour appears to have changed in recent releases \r\n(we are currently on 4.14.5), and what the most reasonable approach to stop \r\nthis warning is? I'm leaning towards dropping syscollector entries from \r\nthe archives pipeline...\r\n\r\nThoughts appreciated!\r\n\r\n-- \r\nYou received this message because you are subscribed to the Google Groups \"Wazuh | Mailing List\" group.\r\nTo unsubscribe from this group and stop receiving emails from it, send an email to wazuh+unsubscribe@googlegroups.com.\r\nTo view this discussion visit https://groups.google.com/d/msgid/wazuh/452aeebb-393b-4ace-81ef-1744c4b1a49en%40googlegroups.com.\r\n",
+ "comments": [],
+ "external_community": [
+ "window.WIZ_global_data = {\"AfY8Hf\":false,\"DpimGf\":false,\"EP1ykd\":[\"/_/*\"],\"FdrFJe\":\"-7294752276061769086\",\"HiPsbb\":1,\"Im6cmf\":\"/_/GroupsFrontendUi\",\"LVIXXb\":1,\"LoQv7e\":false,\"MT7f9b\":[],\"MUE6Ne\":\"groups-frontend\",\"PLnRge\":\"https://docs.google.com/picker\",\"QrtxK\":\"\",\"S06Grb\":\"\",\"S6lZl\":112976253,\"TSDtV\":\"%.@.[[null,[[45447917,null,true,null,null,null,\\\"OeRc3d\\\"],[45447918,null,true,null,null,null,\\\"paZwJ\\\"],[45749384,null,true,null,null,null,\\\"ervCtb\\\"],[45709804,null,false,null,null,null,\\\"PoJR0\\\"],[45697011,null,true,null,null,null,\\\"xNsvyb\\\"],[45734189,null,true,null,null,null,\\\"XGnjIb\\\"],[45448406,null,false,null,null,null,\\\"jKfwq\\\"],[45780063,null,false,null,null,null,\\\"j3OYnd\\\"],[45447921,null,false,null,null,null,\\\"qAj4w\\\"],[45655177,null,true,null,null,null,\\\"VvKUq\\\"],[45447945,null,true,null,null,null,\\\"ckfnge\\\"],[45459555,null,false,null,null,null,\\\"Imeoqb\\\"],[45447936,null,false,null,null,null,\\\"MohPG\\\"],[45532875,null,true,null,null,null,\\\"udsJQe\\\"],[45722772,null,true,null,null,null,\\\"MVCE3b\\\"],[45646796,null,false,null,null,null,\\\"Q877Ab\\\"],[45753484,null,true,null,null,null,\\\"G9u5lb\\\"],[45699412,null,true,null,null,null,\\\"xLy3Ce\\\"],[45447950,null,true,null,null,null,\\\"lU0ald\\\"],[45447953,null,true,null,null,null,\\\"jbTsAe\\\"],[45447919,null,false,null,null,null,\\\"KMEQCe\\\"],[45699332,30,null,null,null,null,\\\"grENN\\\"],[45447930,null,null,null,\\\"signed_out_users\\\",null,\\\"JTFNhb\\\"],[45447928,null,true,null,null,null,\\\"Wh7on\\\"],[45447938,null,true,null,null,null,\\\"uVDGld\\\"],[45709766,null,true,null,null,null,\\\"sSVuJ\\\"],[45776470,null,true,null,null,null,\\\"A9MFwf\\\"],[45447926,null,true,null,null,null,\\\"IUIyxe\\\"],[45793915,null,true,null,null,null,\\\"Vwrzsf\\\"],[45721043,null,true,null,null,null,\\\"FcV2Ie\\\"],[45447924,null,true,null,null,null,\\\"hMPU3c\\\"],[45447934,null,null,null,\\\"https://forms.gle/DuQUYavHhwfEo4sp9\\\",null,\\\"gvGLK\\\"],[45447932,null,false,null,null,null,\\\"jlJZI\\\"]],\\\"CAMSOx0+/7riEsDRsgYOrrekEBYEFtqsNuYD3f+wBAq20g0Kuv0FCqMuFtjVAgqrrQAOppMFCpO2DQ6Jng4K\\\"]]]\",\"Tb2qJf\":4,\"UUFaWc\":\"%.@.null,1000,2]\",\"Vvafkd\":false,\"Yllh3e\":\"%.@.1783855263558303,122390012,622000959]\",\"YlwcZe\":\"%.@.3,[1],[3600],2,[15,4,13,14,12,2]]\",\"ZZZ7Uc\":\"\",\"b5W2zf\":\"default_GroupsFrontendUi\",\"cfb2h\":\"boq_groupsfrontendserver_20260706.01_p0\",\"eNnkwf\":\"1783855261\",\"eptZe\":\"/_/GroupsFrontendUi/\",\"fPDxwd\":[97493638,97493660,105739272],\"gGcLoe\":false,\"gSs3jc\":1,\"hpRnh\":1,\"hsFLT\":\"%.@.null,10,3]\",\"iCzhFc\":false,\"nQyAE\":{\"G9u5lb\":\"true\",\"lU0ald\":\"true\",\"grENN\":\"30\",\"uVDGld\":\"true\",\"Vwrzsf\":\"true\",\"XGnjIb\":\"true\",\"FcV2Ie\":\"true\",\"j3OYnd\":\"false\",\"qAj4w\":\"false\",\"MohPG\":\"false\",\"Q877Ab\":\"false\",\"xLy3Ce\":\"true\",\"KMEQCe\":\"false\",\"JTFNhb\":\"signed_out_users\",\"IUIyxe\":\"true\",\"OeRc3d\":\"true\",\"paZwJ\":\"true\",\"PoJR0\":\"false\",\"xNsvyb\":\"true\",\"ckfnge\":\"true\",\"udsJQe\":\"true\",\"jbTsAe\":\"true\",\"Wh7on\":\"true\",\"sSVuJ\":\"true\",\"gvGLK\":\"https://forms.gle/DuQUYavHhwfEo4sp9\",\"jlJZI\":\"false\",\"ervCtb\":\"true\",\"A9MFwf\":\"true\",\"jKfwq\":\"false\"},\"p9hQne\":\"https://www.gstatic.com/_/boq-groups/_/r/\",\"qwAQke\":\"GroupsFrontendUi\",\"qymVe\":\"enwSiaAFzT7X15z4rep7I96imHY\",\"rtQCxc\":-330,\"u4g7r\":\"%.@.null,1,3]\",\"w2btAe\":\"%.@.null,null,\\\"\\\",false,null,null,null,false]\",\"xn5OId\":false,\"xnI9P\":false,\"xwAfE\":true,\"y2FhP\":\"prod\",\"yFnxrf\":1884,\"zChJod\":\"%.@.]\"}; window[\"_F_toggles_default_GroupsFrontendUi\"] = [0x30081086, 0x30b0b301, 0x100c5ad, ]; (function(){'use strict';var a=window,d=a.performance,l=k();a.cc_latency_start_time=d&&d.now?0:d&&d.timing&&d.timing.navigationStart?d.timing.navigationStart:l;function k(){return d&&d.now?d.now():(new Date).getTime()}function n(e){if(d&&d.now&&d.mark){var g=d.mark(e);if(g)return g.startTime;if(d.getEntriesByName&&(e=d.getEntriesByName(e).pop()))return e.startTime}return k()}a.onaft=function(){n(\"aft\")};a._isLazyImage=function(e){return e.hasAttribute(\"data-src\")||e.hasAttribute(\"data-ils\")||e.getAttribute(\"loading\")===\"lazy\"}; a.l=function(e){function g(b){var c={};c[b]=k();a.cc_latency.push(c)}function m(b){var c=n(\"iml\");b.setAttribute(\"data-iml\",c);ret"
+ ],
+ "url": "https://github.com/wazuh/community/issues/65119",
+ "labels": [
+ "Google Groups",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "Resolved",
+ "reviewer/team/community/sakib789",
+ "review/quality/community_team/internal",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 65077,
+ "title": "praveenkyadav17 https://wazuh.slack.com/archives/C0A933R8E/p1779450272237239",
+ "body": "Hi All, Sir tell me manual rule creation and testing tuning with decoder\nhttps://wazuh.slack.com/archives/C0A933R8E/p1779450272237239",
+ "comments": [
+ "LGTM"
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/65077",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/oajani",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 65054,
+ "title": "cdb list doble ",
+ "body": "Hi team,\r\n\r\nIt is posible in wazuh 14.1 a rule with two CDB Lists? Can be possible that \r\nthe rule check only one?\r\n\r\n \r\n 60106,92657\r\n etc/lists/admins\r\n etc/lists/admins-list-no\r\n User \"$(win.eventdata.targetUserName)\" logged\r\n \r\n T1564.001\r\n \r\n \r\n\r\nRegards\r\n\r\nGerman\r\n\r\n-- \r\nYou received this message because you are subscribed to the Google Groups \"Wazuh | Mailing List\" group.\r\nTo unsubscribe from this group and stop receiving emails from it, send an email to wazuh+unsubscribe@googlegroups.com.\r\nTo view this discussion visit https://groups.google.com/d/msgid/wazuh/93806118-cfbb-4910-b676-c5827227580an%40googlegroups.com.\r\n",
+ "comments": [
+ "No feedback from user.",
+ "LGTM"
+ ],
+ "external_community": [
+ "window.WIZ_global_data = {\"AfY8Hf\":false,\"DpimGf\":false,\"EP1ykd\":[\"/_/*\"],\"FdrFJe\":\"7072332851582499417\",\"HiPsbb\":1,\"Im6cmf\":\"/_/GroupsFrontendUi\",\"LVIXXb\":1,\"LoQv7e\":false,\"MT7f9b\":[],\"MUE6Ne\":\"groups-frontend\",\"PLnRge\":\"https://docs.google.com/picker\",\"QrtxK\":\"\",\"S06Grb\":\"\",\"S6lZl\":112976253,\"TSDtV\":\"%.@.[[null,[[45447917,null,true,null,null,null,\\\"OeRc3d\\\"],[45447918,null,true,null,null,null,\\\"paZwJ\\\"],[45749384,null,true,null,null,null,\\\"ervCtb\\\"],[45709804,null,false,null,null,null,\\\"PoJR0\\\"],[45697011,null,true,null,null,null,\\\"xNsvyb\\\"],[45734189,null,true,null,null,null,\\\"XGnjIb\\\"],[45448406,null,false,null,null,null,\\\"jKfwq\\\"],[45780063,null,false,null,null,null,\\\"j3OYnd\\\"],[45447921,null,false,null,null,null,\\\"qAj4w\\\"],[45655177,null,true,null,null,null,\\\"VvKUq\\\"],[45447945,null,true,null,null,null,\\\"ckfnge\\\"],[45459555,null,false,null,null,null,\\\"Imeoqb\\\"],[45447936,null,false,null,null,null,\\\"MohPG\\\"],[45532875,null,true,null,null,null,\\\"udsJQe\\\"],[45722772,null,true,null,null,null,\\\"MVCE3b\\\"],[45646796,null,false,null,null,null,\\\"Q877Ab\\\"],[45753484,null,true,null,null,null,\\\"G9u5lb\\\"],[45699412,null,true,null,null,null,\\\"xLy3Ce\\\"],[45447950,null,true,null,null,null,\\\"lU0ald\\\"],[45447953,null,true,null,null,null,\\\"jbTsAe\\\"],[45447919,null,false,null,null,null,\\\"KMEQCe\\\"],[45699332,30,null,null,null,null,\\\"grENN\\\"],[45447930,null,null,null,\\\"signed_out_users\\\",null,\\\"JTFNhb\\\"],[45447928,null,true,null,null,null,\\\"Wh7on\\\"],[45447938,null,true,null,null,null,\\\"uVDGld\\\"],[45709766,null,true,null,null,null,\\\"sSVuJ\\\"],[45776470,null,true,null,null,null,\\\"A9MFwf\\\"],[45447926,null,true,null,null,null,\\\"IUIyxe\\\"],[45793915,null,true,null,null,null,\\\"Vwrzsf\\\"],[45721043,null,true,null,null,null,\\\"FcV2Ie\\\"],[45447924,null,true,null,null,null,\\\"hMPU3c\\\"],[45447934,null,null,null,\\\"https://forms.gle/DuQUYavHhwfEo4sp9\\\",null,\\\"gvGLK\\\"],[45447932,null,false,null,null,null,\\\"jlJZI\\\"]],\\\"CAMSOx0+/7riEsDRsgYOrrekEBYEFtqsNuYD3f+wBAq20g0Kuv0FCqMuFtjVAgqrrQAOppMFCpO2DQ6Jng4K\\\"]]]\",\"Tb2qJf\":4,\"UUFaWc\":\"%.@.null,1000,2]\",\"Vvafkd\":false,\"Yllh3e\":\"%.@.1783855267846952,40951747,855689629]\",\"YlwcZe\":\"%.@.3,[1],[3600],2,[15,4,13,14,12,2]]\",\"ZZZ7Uc\":\"\",\"b5W2zf\":\"default_GroupsFrontendUi\",\"cfb2h\":\"boq_groupsfrontendserver_20260706.01_p0\",\"eNnkwf\":\"1783855266\",\"eptZe\":\"/_/GroupsFrontendUi/\",\"fPDxwd\":[97493638,97493660,105739272],\"gGcLoe\":false,\"gSs3jc\":1,\"hpRnh\":1,\"hsFLT\":\"%.@.null,10,3]\",\"iCzhFc\":false,\"nQyAE\":{\"G9u5lb\":\"true\",\"lU0ald\":\"true\",\"grENN\":\"30\",\"uVDGld\":\"true\",\"Vwrzsf\":\"true\",\"XGnjIb\":\"true\",\"FcV2Ie\":\"true\",\"j3OYnd\":\"false\",\"qAj4w\":\"false\",\"MohPG\":\"false\",\"Q877Ab\":\"false\",\"xLy3Ce\":\"true\",\"KMEQCe\":\"false\",\"JTFNhb\":\"signed_out_users\",\"IUIyxe\":\"true\",\"OeRc3d\":\"true\",\"paZwJ\":\"true\",\"PoJR0\":\"false\",\"xNsvyb\":\"true\",\"ckfnge\":\"true\",\"udsJQe\":\"true\",\"jbTsAe\":\"true\",\"Wh7on\":\"true\",\"sSVuJ\":\"true\",\"gvGLK\":\"https://forms.gle/DuQUYavHhwfEo4sp9\",\"jlJZI\":\"false\",\"ervCtb\":\"true\",\"A9MFwf\":\"true\",\"jKfwq\":\"false\"},\"p9hQne\":\"https://www.gstatic.com/_/boq-groups/_/r/\",\"qwAQke\":\"GroupsFrontendUi\",\"qymVe\":\"QTH0K-v0SCTeCE2W_bx7fpxEVBA\",\"rtQCxc\":-330,\"u4g7r\":\"%.@.null,1,3]\",\"w2btAe\":\"%.@.null,null,\\\"\\\",false,null,null,null,false]\",\"xn5OId\":false,\"xnI9P\":false,\"xwAfE\":true,\"y2FhP\":\"prod\",\"yFnxrf\":1884,\"zChJod\":\"%.@.]\"}; window[\"_F_toggles_default_GroupsFrontendUi\"] = [0x30081086, 0x30b0b301, 0x100c5ad, ]; (function(){'use strict';var a=window,d=a.performance,l=k();a.cc_latency_start_time=d&&d.now?0:d&&d.timing&&d.timing.navigationStart?d.timing.navigationStart:l;function k(){return d&&d.now?d.now():(new Date).getTime()}function n(e){if(d&&d.now&&d.mark){var g=d.mark(e);if(g)return g.startTime;if(d.getEntriesByName&&(e=d.getEntriesByName(e).pop()))return e.startTime}return k()}a.onaft=function(){n(\"aft\")};a._isLazyImage=function(e){return e.hasAttribute(\"data-src\")||e.hasAttribute(\"data-ils\")||e.getAttribute(\"loading\")===\"lazy\"}; a.l=function(e){function g(b){var c={};c[b]=k();a.cc_latency.push(c)}function m(b){var c=n(\"iml\");b.setAttribute(\"data-iml\",c);retur"
+ ],
+ "url": "https://github.com/wazuh/community/issues/65054",
+ "labels": [
+ "Google Groups",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/oajani",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 65053,
+ "title": "Wazuh for RHEL5 and SLES11 ",
+ "body": "\r\n\r\nHi,\r\n\r\nWe can see that the Wazuh agent packages for RHEL 5 and SLES 11 are listed \r\nin the Wazuh Installation Guide \r\n \r\ndocumentation, with version 4.14.1-1.\r\n\r\nCould you please confirm whether jq is a mandatory requirement for \r\ninstalling the Wazuh agent on legacy/obsolete operating systems such as \r\nRHEL 5 and SLES 11?\r\n\r\nIf jq cannot be installed on these older platforms, how does Wazuh handle \r\nor parse JSON data internally in such environments?\r\n\r\n-- \r\nYou received this message because you are subscribed to the Google Groups \"Wazuh | Mailing List\" group.\r\nTo unsubscribe from this group and stop receiving emails from it, send an email to wazuh+unsubscribe@googlegroups.com.\r\nTo view this discussion visit https://groups.google.com/d/msgid/wazuh/a6b4e172-f089-4437-8ea5-6390b7268c8cn%40googlegroups.com.\r\n",
+ "comments": [
+ "Hello,\n\nYou can install the Wazuh Agent on RHEL systems using the packages provided in https://documentation.wazuh.com/current/installation-guide/packages-list.html. You might require jq if you use it for incident response scripts on your agents but it's not a mandatory requirement.\n",
+ "LGTM"
+ ],
+ "external_community": [
+ "window.WIZ_global_data = {\"AfY8Hf\":false,\"DpimGf\":false,\"EP1ykd\":[\"/_/*\"],\"FdrFJe\":\"680397964924757946\",\"HiPsbb\":1,\"Im6cmf\":\"/_/GroupsFrontendUi\",\"LVIXXb\":1,\"LoQv7e\":false,\"MT7f9b\":[],\"MUE6Ne\":\"groups-frontend\",\"PLnRge\":\"https://docs.google.com/picker\",\"QrtxK\":\"\",\"S06Grb\":\"\",\"S6lZl\":112976253,\"TSDtV\":\"%.@.[[null,[[45447917,null,true,null,null,null,\\\"OeRc3d\\\"],[45447918,null,true,null,null,null,\\\"paZwJ\\\"],[45749384,null,true,null,null,null,\\\"ervCtb\\\"],[45709804,null,false,null,null,null,\\\"PoJR0\\\"],[45697011,null,true,null,null,null,\\\"xNsvyb\\\"],[45734189,null,true,null,null,null,\\\"XGnjIb\\\"],[45448406,null,false,null,null,null,\\\"jKfwq\\\"],[45780063,null,false,null,null,null,\\\"j3OYnd\\\"],[45447921,null,false,null,null,null,\\\"qAj4w\\\"],[45655177,null,true,null,null,null,\\\"VvKUq\\\"],[45447945,null,true,null,null,null,\\\"ckfnge\\\"],[45459555,null,false,null,null,null,\\\"Imeoqb\\\"],[45447936,null,false,null,null,null,\\\"MohPG\\\"],[45532875,null,true,null,null,null,\\\"udsJQe\\\"],[45722772,null,true,null,null,null,\\\"MVCE3b\\\"],[45646796,null,false,null,null,null,\\\"Q877Ab\\\"],[45753484,null,true,null,null,null,\\\"G9u5lb\\\"],[45699412,null,true,null,null,null,\\\"xLy3Ce\\\"],[45447950,null,true,null,null,null,\\\"lU0ald\\\"],[45447953,null,true,null,null,null,\\\"jbTsAe\\\"],[45447919,null,false,null,null,null,\\\"KMEQCe\\\"],[45699332,30,null,null,null,null,\\\"grENN\\\"],[45447930,null,null,null,\\\"signed_out_users\\\",null,\\\"JTFNhb\\\"],[45447928,null,true,null,null,null,\\\"Wh7on\\\"],[45447938,null,true,null,null,null,\\\"uVDGld\\\"],[45709766,null,true,null,null,null,\\\"sSVuJ\\\"],[45776470,null,true,null,null,null,\\\"A9MFwf\\\"],[45447926,null,true,null,null,null,\\\"IUIyxe\\\"],[45793915,null,true,null,null,null,\\\"Vwrzsf\\\"],[45721043,null,true,null,null,null,\\\"FcV2Ie\\\"],[45447924,null,true,null,null,null,\\\"hMPU3c\\\"],[45447934,null,null,null,\\\"https://forms.gle/DuQUYavHhwfEo4sp9\\\",null,\\\"gvGLK\\\"],[45447932,null,false,null,null,null,\\\"jlJZI\\\"]],\\\"CAMSOx0+/7riEsDRsgYOrrekEBYEFtqsNuYD3f+wBAq20g0Kuv0FCqMuFtjVAgqrrQAOppMFCpO2DQ6Jng4K\\\"]]]\",\"Tb2qJf\":4,\"UUFaWc\":\"%.@.null,1000,2]\",\"Vvafkd\":false,\"Yllh3e\":\"%.@.1783855272069333,40946048,3221520414]\",\"YlwcZe\":\"%.@.3,[1],[3600],2,[15,4,13,14,12,2]]\",\"ZZZ7Uc\":\"\",\"b5W2zf\":\"default_GroupsFrontendUi\",\"cfb2h\":\"boq_groupsfrontendserver_20260706.01_p0\",\"eNnkwf\":\"1783855270\",\"eptZe\":\"/_/GroupsFrontendUi/\",\"fPDxwd\":[97493638,97493660,105739272],\"gGcLoe\":false,\"gSs3jc\":1,\"hpRnh\":1,\"hsFLT\":\"%.@.null,10,3]\",\"iCzhFc\":false,\"nQyAE\":{\"G9u5lb\":\"true\",\"lU0ald\":\"true\",\"grENN\":\"30\",\"uVDGld\":\"true\",\"Vwrzsf\":\"true\",\"XGnjIb\":\"true\",\"FcV2Ie\":\"true\",\"j3OYnd\":\"false\",\"qAj4w\":\"false\",\"MohPG\":\"false\",\"Q877Ab\":\"false\",\"xLy3Ce\":\"true\",\"KMEQCe\":\"false\",\"JTFNhb\":\"signed_out_users\",\"IUIyxe\":\"true\",\"OeRc3d\":\"true\",\"paZwJ\":\"true\",\"PoJR0\":\"false\",\"xNsvyb\":\"true\",\"ckfnge\":\"true\",\"udsJQe\":\"true\",\"jbTsAe\":\"true\",\"Wh7on\":\"true\",\"sSVuJ\":\"true\",\"gvGLK\":\"https://forms.gle/DuQUYavHhwfEo4sp9\",\"jlJZI\":\"false\",\"ervCtb\":\"true\",\"A9MFwf\":\"true\",\"jKfwq\":\"false\"},\"p9hQne\":\"https://www.gstatic.com/_/boq-groups/_/r/\",\"qwAQke\":\"GroupsFrontendUi\",\"qymVe\":\"eFPVePEW4Jk_f-cOC31QF8y7TwQ\",\"rtQCxc\":-330,\"u4g7r\":\"%.@.null,1,3]\",\"w2btAe\":\"%.@.null,null,\\\"\\\",false,null,null,null,false]\",\"xn5OId\":false,\"xnI9P\":false,\"xwAfE\":true,\"y2FhP\":\"prod\",\"yFnxrf\":1884,\"zChJod\":\"%.@.]\"}; window[\"_F_toggles_default_GroupsFrontendUi\"] = [0x30081086, 0x30b0b301, 0x100c5ad, ]; (function(){'use strict';var a=window,d=a.performance,l=k();a.cc_latency_start_time=d&&d.now?0:d&&d.timing&&d.timing.navigationStart?d.timing.navigationStart:l;function k(){return d&&d.now?d.now():(new Date).getTime()}function n(e){if(d&&d.now&&d.mark){var g=d.mark(e);if(g)return g.startTime;if(d.getEntriesByName&&(e=d.getEntriesByName(e).pop()))return e.startTime}return k()}a.onaft=function(){n(\"aft\")};a._isLazyImage=function(e){return e.hasAttribute(\"data-src\")||e.hasAttribute(\"data-ils\")||e.getAttribute(\"loading\")===\"lazy\"}; a.l=function(e){function g(b){var c={};c[b]=k();a.cc_latency.push(c)}function m(b){var c=n(\"iml\");b.setAttribute(\"data-iml\",c);retur"
+ ],
+ "url": "https://github.com/wazuh/community/issues/65053",
+ "labels": [
+ "Google Groups",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/oajani",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 65052,
+ "title": "martin.kimanzi https://wazuh.slack.com/archives/C07CNG3M11N/p1779373926353379",
+ "body": "if i have over 280k alerts in a week what is the best period to avoid the shards maxing out 1000 limit and denying access to the dashboard? My current config in wazuh.yml is set to wazuh.monitoring.creation: h\n\nMy wazuh server has 331GB free currently.\nhttps://wazuh.slack.com/archives/C07CNG3M11N/p1779373926353379",
+ "comments": [
+ "This issue is stale because it has been open for 15 days with no activity."
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/65052",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "stale",
+ "reviewer/team/community/oajani",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 65003,
+ "title": "anonym_266 https://discord.com/channels/1049711339578331186/1049711340316541004/1506248606611542057",
+ "body": "Hi all,\n\nAccording to the Wazuh documentation on SCA:\n\n> \u201cEach Wazuh agent has its own local database where it stores the current state of each SCA check.\u201d\n\nI\u2019m currently using Wazuh v4.14.2, but I haven\u2019t been able to locate this database on the agent.\n\nCould someone point me to its location or clarify how/where these SCA states are stored on the agent?\n\nDocumentation reference: [Wazuh SCA Documentation](https://documentation.wazuh.com/current/user-manual/capabilities/sec-config-assessment/how-it-works.html?utm_source=chatgpt.com)\nhttps://discord.com/channels/1049711339578331186/1049711340316541004/1506248606611542057",
+ "comments": [],
+ "external_community": [
+ "[Linked Discord conversation \u2014 not fetchable without a bot token in that server.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/65003",
+ "labels": [
+ "Discord",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/oajani",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 64999,
+ "title": "tobias.karsch128 https://wazuh.slack.com/archives/C07BK5RJM3R/p1779184113383699",
+ "body": "Dear all,\nI have wazuh agents installed Red Hat Enterprise Linux with the version v4.14.5. For some reason I dont see any IT Hygiene information for those endpoints. As far as I know I did not configure any settings for the endpoints just moved them to a Group. So it should all be the default settings.\nDoes anyone know what that is? Is that OS not supported?\nAs far as I have been online, the IT Hygiene should at least also work for Ubuntu. Therefore, I am wondering.Thank you in advance!\nhttps://wazuh.slack.com/archives/C07BK5RJM3R/p1779184113383699",
+ "comments": [],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/64999",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "Resolved without feedback",
+ "reviewer/team/community/sakib789",
+ "review/quality/community_team/internal",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 64965,
+ "title": "xmultx13 https://wazuh.slack.com/archives/C0A933R8E/p1779103028575109",
+ "body": "hi community! could you recommend any AI tools that can write decoders and\nrules for Wazuh normally? My experience so far is mostly negative. I still end\nup writing most of the decoders and correlation rules manually, which becomes\npainful when onboarding new log sources. I\u2019m especially interested in: \u2022\ntools/workflows that can generate reliable Wazuh decoders/rules from sample\nlogs \u2022 approaches for speeding up parser/rule creation Also, where do you\nusually look for ready-made rulesets and community decoders besides the\nofficial Wazuh repository?\nhttps://wazuh.slack.com/archives/C0A933R8E/p1779103028575109",
+ "comments": [],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/64965",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/oajani",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 64844,
+ "title": "JSON decoder not decoding in 4.14.x - in 4.10.3 everything is fine #35935",
+ "body": "Hi @ all,\n\nduring some testing we figured out that suricata json formated logs are no longer parsed. By digging in in more detail we found out that JSON parsing at all does not work any more.\n\nHere is a simple test using the following JSON-Test-String:\n{\"name\":\"Gilbert\",\"session\":\"2013\",\"score\":24,\"completed\":true}\n\nParsing result with wazuh-logtest (version 4.14.5):\nimage\n\nParsing result with wazuh-logtest (version 4.10.3):\nimage\n\nAs you can see above the json is parsed correctly in version 4.10.3 but no longer in 4.14.5. The overall configuration of wazuh is the same. There are no special decoders, rules or any other configuration differences.\n\nDoes anybody has an idea what is going on here or how this can be solved?\n\nThank you in advance and greetings,\nDragan\n\nhttps://github.com/wazuh/wazuh/discussions/35935",
+ "comments": [],
+ "external_community": [],
+ "url": "https://github.com/wazuh/community/issues/64844",
+ "labels": [
+ "GitHub",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "Resolved",
+ "reviewer/team/community/sakib789",
+ "review/quality/community_team/internal",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 64760,
+ "title": "Open-source Wazuh \u2192 Telegram alert integration for self-hosted labs https://www.reddit.com/r/Wazuh/comments/1t6ry4h/opensource_wazuh_telegram_alert_integration_for/",
+ "body": "Made a lightweight open-source integration that sends Wazuh alerts directly to Telegram.\n\nBuilt mainly for self-hosted environments, homelabs and small SOC setups where quick alert visibility matters.\n\nFeatures:\n\n- simple Python setup\n\n- customizable notifications\n\n- lightweight\n\n- works with existing Wazuh deployments\n\n- fully open-source\n\nGitHub:\n\n[https://github.com/abbas-babayev/wazuh-telegram-alerting/tree/main\\ ]\n\nWould appreciate feedback or suggestions for improvements.\n\n submitted by /u/minjunhen\n\n[link] [comments]\nhttps://www.reddit.com/r/Wazuh/comments/1t6ry4h/opensource_wazuh_telegram_alert_integration_for/",
+ "comments": [
+ "LGTM",
+ "Resolved"
+ ],
+ "external_community": [],
+ "url": "https://github.com/wazuh/community/issues/64760",
+ "labels": [
+ "Reddit",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "ambassador",
+ "reviewer/team/community/bonyjohn05",
+ "review/quality",
+ "reviewer/mentor/Bony",
+ "LGTM",
+ "reviewed"
+ ]
+ },
+ {
+ "number": 64711,
+ "title": "dileepkumarchokkapu https://wazuh.slack.com/archives/C0A933R8E/p1778072581527719",
+ "body": "Hi <@U01HXEV6SL8> I'm running into an issue with GeoLocation fields in custom\nO365 rules. I have a rule that checks `GeoLocation.country_name` to detect\nlogins from outside India: \n91531\\d+Office 365: $(office365.Workload)\n$(office365.Operation) operation.no_full_log\n91532^UserLoggedIn$^Not Available$\n Office 365: Login\nsuccess by $(office365.UserId)T1078\n\n91532^UserLoginFailed$^Not Available$\n Office 365: Login\nfailed by $(office365.UserId)T1110\n100902\noffice365.UserIdOffice 365: Multiple\nfailed logins by $(office365.UserId)T1110\nT1110.003100901^India$\nOffice 365: Successful login from $(GeoLocation.country_name) by\n$(office365.UserId)T1078 \n100902^India$Office 365: Failed login from\n$(GeoLocation.country_name) by $(office365.UserId)\nT1110 Problem: ```xml \n100902^India$Office 365: Failed login from\n$(GeoLocation.country_name) ``` The rule is NOT firing\nas expected \u2014 instead, the parent rule (100902) triggered and the GeoLocation-\nbased rule is skipped entirely. In Discover can see `GeoLocation.country_name`\nis populated correctly (e.g., `United States`). But the raw log coming in has\nno GeoLocation fields \u2014 Wazuh appears to add them after enrichment. My\nunderstanding: GeoIP enrichment happens after the rule engine fires, so\n`GeoLocation.*` fields are unavailable during rule matching. 1\\. Is this the\nexpected behavior \u2014 GeoLocation fields are enriched after rule evaluation and\ncannot be used in rule conditions? 2\\. Is the recommended workaround to use a\nCDB list with IP/CIDR ranges and `lookup=\"not_address_match_key\"` for country-\nbased filtering at rule time? 3\\. Are there any other supported simple\napproaches to achieve geo-based rule matching in Wazuh? Wazuh version: 4.14.4\nAppreciate any guidance or best practices from the community. Thanks in\nadvance!\nhttps://wazuh.slack.com/archives/C0A933R8E/p1778072581527719",
+ "comments": [],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/64711",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/oajani",
+ "ambassador",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 64684,
+ "title": "Users and Groups display in Wazuh inventory data section https://www.reddit.com/r/Wazuh/comments/1t4k2qg/users_and_groups_display_in_wazuh_inventory_data/",
+ "body": "i want groups and users to be displayed in the inventory data section for each agent, i've tried to add these two lines\n\nyes\n\nyes\n\nin the syscollector section (ossec.conf file)\n\n\n\nno\n\n1h\n\nyes\n\nyes\n\nyes\n\nyes\n\nyes\n\nyes\n\nyes\n\nyes\n\nyes\n\nyes\n\n\n\n\n\n10\n\n\n\n\n\nbut that showed the error : \" Error: Could not update configuration (1908) - Error validating configuration: No such tag 'groups' at module 'syscollector'., (1202): Configuration error at 'etc/ossec.conf'.\"\n\nis there a specifique config to add before this step ?\n\nThanks in advance.\n\n#wazuh\n\n submitted by /u/Willing-Star-9751\n\n[link] [comments]\nhttps://www.reddit.com/r/Wazuh/comments/1t4k2qg/users_and_groups_display_in_wazuh_inventory_data/",
+ "comments": [],
+ "external_community": [],
+ "url": "https://github.com/wazuh/community/issues/64684",
+ "labels": [
+ "Reddit",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/sakib789",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 64578,
+ "title": "virgil.m.tankdozer https://discord.com/channels/1049711339578331186/1049711340316541004/1499335156669091943",
+ "body": "We have a multi-hop network where external traffic enters via an frp tunnel. This means:\n\nfrps logs (on VPS) contain the real external IP + proxy name + timestamp\nMinecraft server logs (on a separate host) contain a bot username + DMZ IP (192.168.90.xx) + timestamp\nThe two events are causally linked \u2014 the frps connection at time T results in the Minecraft event at T+~1 second\n\nWe want to correlate these two events to answer: \"Which real external IP was behind this bot username?\"\nThe problem: there is no common field between the two sources. The only link is timestamp proximity (~1 second window).\nQuestions:\n\n1. Is there a native Wazuh mechanism for time-window correlation between events from different agents that have no common field?\n2. Is the CDB list + active response approach the recommended solution for this, and if so, how would you implement it for a timestamp-based lookup rather than an IP-based lookup?\n3. Is this a known limitation of the analysisd rule engine, and is it on the roadmap?\nhttps://discord.com/channels/1049711339578331186/1049711340316541004/1499335156669091943",
+ "comments": [
+ "This issue is stale because it has been open for 15 days with no activity."
+ ],
+ "external_community": [
+ "[Linked Discord conversation \u2014 not fetchable without a bot token in that server.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/64578",
+ "labels": [
+ "Discord",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "stale",
+ "reviewer/team/community/oajani",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 64494,
+ "title": "marcin.strzyzewski https://wazuh.slack.com/archives/C0A933R8E/p1777285300341609",
+ "body": "Hello. I use the s3 wodle to fetch logs from multiple AWS accounts and\nregions. However, one of them fails with `ClientError('An error occurred\n(ExpiredToken) when calling the GetObject operation: The provided token has\nexpired.')` even though it works fine with other regions for the same account\nand its data uses the same encryption key and role as the others. Wodle's\ninterval is set to 10m but my log shows there is sometimes >2h between checks\non the same account.\nhttps://wazuh.slack.com/archives/C0A933R8E/p1777285300341609",
+ "comments": [],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/64494",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/oajani",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 64479,
+ "title": "nickyhuyskens https://github.com/wazuh/wazuh/issues/35581",
+ "body": "when trying to install wazuh-agent_4.14.4-1_amd64.deb. \nOn debian 13, I often get that the package \"lsb-release\" is missing.\ninstalling the package and then attempting to install wazuh-agent keeps the /var/ossec/etc/ossec.conf on MANAGER_IP instead of resolving it.\nWhen you go to a clean debian 13 and install the lsb-release first, then install wazuh-agent, it works perfectly!\n\nhttps://github.com/wazuh/wazuh/issues/35581",
+ "comments": [
+ "HI @vikman90 \n\nYour response is good, helpful, and on point. If the user replies or needs further help. Please feel free to reopen it. \n\nThank you!"
+ ],
+ "external_community": [],
+ "url": "https://github.com/wazuh/community/issues/64479",
+ "labels": [
+ "GitHub",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "Resolved without feedback",
+ "review/moderation",
+ "reviewer/team/community/Stuti3097",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 64473,
+ "title": "skuratovskiy.geek https://wazuh.slack.com/archives/C07BZJY86G3/p1777196920198609",
+ "body": "Hi all!\nAfter upgrading 4.14.1-1 to 4.14.4-1 data nodes does not connect to master node\nI done upgrade through https://documentation.wazuh.com/current/upgrade-guide/upgrading-central-components.htmlhttps://documentation.wazuh.com/current/upgrade-guide/upgrading-central-components.html\nMaster node has cluster_uuid, data node does not have this uuid, it has __na_ uuid_\n_Can someone help me ?_\nhttps://wazuh.slack.com/archives/C07BZJY86G3/p1777196920198609",
+ "comments": [
+ "The troubleshooting steps look fine. The user made some custom changes to the indexer configuration related to hostname verification and resolved the issue after reverting the configuration.\n\nLGTM \u2705"
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/64473",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "Resolved",
+ "reviewer/team/community/sakib789",
+ "review/quality/community_team/internal",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 64404,
+ "title": "What's the best practice to retrieve data from remote site to a wazuh server? https://www.reddit.com/r/Wazuh/comments/1st4h16/whats_the_best_practice_to_retrieve_data_from/",
+ "body": "I am trying to monitor a remote endpoint with Wazuh and I was thinking whether exposing Wazuh to the public is a good idea or I just use some VPN or tunneling method.\n\n submitted by /u/roti_kaya_42\n\n[link] [comments]\nhttps://www.reddit.com/r/Wazuh/comments/1st4h16/whats_the_best_practice_to_retrieve_data_from/",
+ "comments": [
+ "End of my shift",
+ "Hi @syscon3, when unassigning a ticket, please make sure to remove the wazuh-community bot as well. If the bot remains assigned after you unassign yourself, it can prevent the ticket from being reassigned to another member.\n\nPlease ensure both you and the bot are unassigned going forward.",
+ "@bonyjohn05 LGTM"
+ ],
+ "external_community": [],
+ "url": "https://github.com/wazuh/community/issues/64404",
+ "labels": [
+ "Reddit",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "Resolved without feedback",
+ "review/moderation",
+ "reviewer/team/community/sakib789",
+ "ambassador",
+ "reviewer/team/community/bonyjohn05",
+ "review/quality/community_team/internal",
+ "reviewer/mentor/Bony",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 64294,
+ "title": "vijay.y https://wazuh.slack.com/archives/C0A933R8E/p1776434673911929",
+ "body": "Hello Wazuh Community, I am currently using the OVA deployment of Wazuh on a\nsingle VM. I want to move toward a distributed architecture and had a couple\nof questions: 1\\. Multi-VM Setup (Same Network / Data Center) How can I\nproperly configure: Two Wazuh Managers Two Indexer nodes running on separate\nVMs? I\u2019m looking for guidance on: Cluster configuration for managers Indexer\ncluster setup Required network ports and communication flow Best practices for\nhigh availability and synchronization 2\\. Multi-Location Setup (Different\nNetworks / Regions) If one VM is in one location and another VM is in a\ndifferent location (different network/public IP), how will they communicate?\nSpecifically: What network configurations are required (VPN, public IP,\nfirewall rules)? How to ensure secure communication between nodes? Any latency\nor performance considerations? Recommended architecture for geo-distributed\ndeployment Any documentation links or real-world setup examples would be very\nhelpful.\nhttps://wazuh.slack.com/archives/C0A933R8E/p1776434673911929",
+ "comments": [
+ "Closing due to inactivity from the user"
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/64294",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/oajani",
+ "ambassador",
+ "review/quality",
+ "reviewer/mentor/Stuti",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 64280,
+ "title": "rule from specific agent IP ",
+ "body": "Hello,\r\n\r\nIf I want to create a custom rule to notify about a new agent connection \r\nbased on the IP address of a specific agent, which option is best to use?\r\nI try , but it is not working:\r\n\r\n \r\n 501\r\n 10.255.17.104\r\n New ossec agent connected.\r\n \r\n\r\n-- \r\nYou received this message because you are subscribed to the Google Groups \"Wazuh | Mailing List\" group.\r\nTo unsubscribe from this group and stop receiving emails from it, send an email to wazuh+unsubscribe@googlegroups.com.\r\nTo view this discussion visit https://groups.google.com/d/msgid/wazuh/d7a64e42-3d40-449f-b5c5-141b1f38dd59n%40googlegroups.com.\r\n",
+ "comments": [],
+ "external_community": [
+ "window.WIZ_global_data = {\"AfY8Hf\":false,\"DpimGf\":false,\"EP1ykd\":[\"/_/*\"],\"FdrFJe\":\"-1935457838351184468\",\"HiPsbb\":1,\"Im6cmf\":\"/_/GroupsFrontendUi\",\"LVIXXb\":1,\"LoQv7e\":false,\"MT7f9b\":[],\"MUE6Ne\":\"groups-frontend\",\"PLnRge\":\"https://docs.google.com/picker\",\"QrtxK\":\"\",\"S06Grb\":\"\",\"S6lZl\":112976253,\"TSDtV\":\"%.@.[[null,[[45447917,null,true,null,null,null,\\\"OeRc3d\\\"],[45447918,null,true,null,null,null,\\\"paZwJ\\\"],[45749384,null,true,null,null,null,\\\"ervCtb\\\"],[45709804,null,false,null,null,null,\\\"PoJR0\\\"],[45697011,null,true,null,null,null,\\\"xNsvyb\\\"],[45734189,null,true,null,null,null,\\\"XGnjIb\\\"],[45448406,null,false,null,null,null,\\\"jKfwq\\\"],[45780063,null,false,null,null,null,\\\"j3OYnd\\\"],[45447921,null,false,null,null,null,\\\"qAj4w\\\"],[45655177,null,true,null,null,null,\\\"VvKUq\\\"],[45447945,null,true,null,null,null,\\\"ckfnge\\\"],[45459555,null,false,null,null,null,\\\"Imeoqb\\\"],[45447936,null,false,null,null,null,\\\"MohPG\\\"],[45532875,null,true,null,null,null,\\\"udsJQe\\\"],[45722772,null,true,null,null,null,\\\"MVCE3b\\\"],[45646796,null,false,null,null,null,\\\"Q877Ab\\\"],[45753484,null,true,null,null,null,\\\"G9u5lb\\\"],[45699412,null,true,null,null,null,\\\"xLy3Ce\\\"],[45447950,null,true,null,null,null,\\\"lU0ald\\\"],[45447953,null,true,null,null,null,\\\"jbTsAe\\\"],[45447919,null,false,null,null,null,\\\"KMEQCe\\\"],[45699332,30,null,null,null,null,\\\"grENN\\\"],[45447930,null,null,null,\\\"signed_out_users\\\",null,\\\"JTFNhb\\\"],[45447928,null,true,null,null,null,\\\"Wh7on\\\"],[45447938,null,true,null,null,null,\\\"uVDGld\\\"],[45709766,null,true,null,null,null,\\\"sSVuJ\\\"],[45776470,null,true,null,null,null,\\\"A9MFwf\\\"],[45447926,null,true,null,null,null,\\\"IUIyxe\\\"],[45793915,null,true,null,null,null,\\\"Vwrzsf\\\"],[45721043,null,true,null,null,null,\\\"FcV2Ie\\\"],[45447924,null,true,null,null,null,\\\"hMPU3c\\\"],[45447934,null,null,null,\\\"https://forms.gle/DuQUYavHhwfEo4sp9\\\",null,\\\"gvGLK\\\"],[45447932,null,false,null,null,null,\\\"jlJZI\\\"]],\\\"CAMSOx0+/7riEsDRsgYOrrekEBYEFtqsNuYD3f+wBAq20g0Kuv0FCqMuFtjVAgqrrQAOppMFCpO2DQ6Jng4K\\\"]]]\",\"Tb2qJf\":4,\"UUFaWc\":\"%.@.null,1000,2]\",\"Vvafkd\":false,\"Yllh3e\":\"%.@.1783855296193098,40951747,855689629]\",\"YlwcZe\":\"%.@.3,[1],[3600],2,[15,4,13,14,12,2]]\",\"ZZZ7Uc\":\"\",\"b5W2zf\":\"default_GroupsFrontendUi\",\"cfb2h\":\"boq_groupsfrontendserver_20260706.01_p0\",\"eNnkwf\":\"1783855295\",\"eptZe\":\"/_/GroupsFrontendUi/\",\"fPDxwd\":[97493638,97493660,105739272],\"gGcLoe\":false,\"gSs3jc\":1,\"hpRnh\":1,\"hsFLT\":\"%.@.null,10,3]\",\"iCzhFc\":false,\"nQyAE\":{\"G9u5lb\":\"true\",\"lU0ald\":\"true\",\"grENN\":\"30\",\"uVDGld\":\"true\",\"Vwrzsf\":\"true\",\"XGnjIb\":\"true\",\"FcV2Ie\":\"true\",\"j3OYnd\":\"false\",\"qAj4w\":\"false\",\"MohPG\":\"false\",\"Q877Ab\":\"false\",\"xLy3Ce\":\"true\",\"KMEQCe\":\"false\",\"JTFNhb\":\"signed_out_users\",\"IUIyxe\":\"true\",\"OeRc3d\":\"true\",\"paZwJ\":\"true\",\"PoJR0\":\"false\",\"xNsvyb\":\"true\",\"ckfnge\":\"true\",\"udsJQe\":\"true\",\"jbTsAe\":\"true\",\"Wh7on\":\"true\",\"sSVuJ\":\"true\",\"gvGLK\":\"https://forms.gle/DuQUYavHhwfEo4sp9\",\"jlJZI\":\"false\",\"ervCtb\":\"true\",\"A9MFwf\":\"true\",\"jKfwq\":\"false\"},\"p9hQne\":\"https://www.gstatic.com/_/boq-groups/_/r/\",\"qwAQke\":\"GroupsFrontendUi\",\"qymVe\":\"9j2ZEeN9ZWtFYrxqWMcGGLvrhyU\",\"rtQCxc\":-330,\"u4g7r\":\"%.@.null,1,3]\",\"w2btAe\":\"%.@.null,null,\\\"\\\",false,null,null,null,false]\",\"xn5OId\":false,\"xnI9P\":false,\"xwAfE\":true,\"y2FhP\":\"prod\",\"yFnxrf\":1884,\"zChJod\":\"%.@.]\"}; window[\"_F_toggles_default_GroupsFrontendUi\"] = [0x30081086, 0x30b0b301, 0x100c5ad, ]; (function(){'use strict';var a=window,d=a.performance,l=k();a.cc_latency_start_time=d&&d.now?0:d&&d.timing&&d.timing.navigationStart?d.timing.navigationStart:l;function k(){return d&&d.now?d.now():(new Date).getTime()}function n(e){if(d&&d.now&&d.mark){var g=d.mark(e);if(g)return g.startTime;if(d.getEntriesByName&&(e=d.getEntriesByName(e).pop()))return e.startTime}return k()}a.onaft=function(){n(\"aft\")};a._isLazyImage=function(e){return e.hasAttribute(\"data-src\")||e.hasAttribute(\"data-ils\")||e.getAttribute(\"loading\")===\"lazy\"}; a.l=function(e){function g(b){var c={};c[b]=k();a.cc_latency.push(c)}function m(b){var c=n(\"iml\");b.setAttribute(\"data-iml\",c);retu"
+ ],
+ "url": "https://github.com/wazuh/community/issues/64280",
+ "labels": [
+ "Google Groups",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "Resolved without feedback",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 64258,
+ "title": "driftwood8891 https://discord.com/channels/1049711339578331186/1049711340316541004/1494375920885301389",
+ "body": "Hello everyone, we are building a wazuh server at our work and my boss is wanting to have the alerts sent to a SQL database so we can query alerts and keep them for a extended period of time. Does the database install path essentailly just send a copy of the alerts to a database or does the database replace the indexer? I would just like to hear everyone's thoughts on sending alerts to a database because from what I understand, the indexer already is the database for Wazuh and we can already set retention policies without have a SQL database. Any advice and/or wisdom is greatly appreciated. Have a great day, everyone!\nhttps://discord.com/channels/1049711339578331186/1049711340316541004/1494375920885301389",
+ "comments": [
+ "Closing after 7 days."
+ ],
+ "external_community": [
+ "[Linked Discord conversation \u2014 not fetchable without a bot token in that server.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/64258",
+ "labels": [
+ "Discord",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/oajani",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 64209,
+ "title": "How to populate the field data.rule_name based on the value of data.rule_uuid ",
+ "body": "\r\n\r\nHello,\r\n\r\nI have configured a Wazuh instance (Docker) to receive logs from OPNsense \r\nfirewalls.\r\nThe decoders and rules are working properly, and I can see logs in Discover \r\n(wazuh-alert).\r\n\r\nI would like to populate the field data.rule_name based on the value of \r\ndata.rule_uuid:\r\n\r\nIf data.rule_uuid = \"123213\", then data.rule_name = \"block all vlan\".\r\n\r\nHow can this be done? I checked the documentation and found that it is \r\nnecessary to modify the default pipeline to achieve this, as described here:\r\nhttps://groups.google.com/g/wazuh/c/nB28mgfYANo/m/8mHOSI4hAAAJ\r\n\r\nIs this the correct solution? How should this be handled during updates?\r\n\r\nI also see that it is possible to create a custom module, but will it then \r\nbe taken into account by Wazuh?\r\n\r\nThank you for your answers.\r\n\r\nKind regards\r\n\r\n-- \r\nYou received this message because you are subscribed to the Google Groups \"Wazuh | Mailing List\" group.\r\nTo unsubscribe from this group and stop receiving emails from it, send an email to wazuh+unsubscribe@googlegroups.com.\r\nTo view this discussion visit https://groups.google.com/d/msgid/wazuh/09840dec-bb73-4d1d-8838-f6db79302456n%40googlegroups.com.\r\n",
+ "comments": [
+ "LGTM. Please share feedback with the user.\n\nThank you",
+ "close due to user inactivity"
+ ],
+ "external_community": [
+ "window.WIZ_global_data = {\"AfY8Hf\":false,\"DpimGf\":false,\"EP1ykd\":[\"/_/*\"],\"FdrFJe\":\"-5671166622236160060\",\"HiPsbb\":1,\"Im6cmf\":\"/_/GroupsFrontendUi\",\"LVIXXb\":1,\"LoQv7e\":false,\"MT7f9b\":[],\"MUE6Ne\":\"groups-frontend\",\"PLnRge\":\"https://docs.google.com/picker\",\"QrtxK\":\"\",\"S06Grb\":\"\",\"S6lZl\":112976253,\"TSDtV\":\"%.@.[[null,[[45447917,null,true,null,null,null,\\\"OeRc3d\\\"],[45447918,null,true,null,null,null,\\\"paZwJ\\\"],[45749384,null,true,null,null,null,\\\"ervCtb\\\"],[45709804,null,false,null,null,null,\\\"PoJR0\\\"],[45697011,null,true,null,null,null,\\\"xNsvyb\\\"],[45734189,null,true,null,null,null,\\\"XGnjIb\\\"],[45448406,null,false,null,null,null,\\\"jKfwq\\\"],[45780063,null,false,null,null,null,\\\"j3OYnd\\\"],[45447921,null,false,null,null,null,\\\"qAj4w\\\"],[45655177,null,true,null,null,null,\\\"VvKUq\\\"],[45447945,null,true,null,null,null,\\\"ckfnge\\\"],[45459555,null,false,null,null,null,\\\"Imeoqb\\\"],[45447936,null,false,null,null,null,\\\"MohPG\\\"],[45532875,null,true,null,null,null,\\\"udsJQe\\\"],[45722772,null,true,null,null,null,\\\"MVCE3b\\\"],[45646796,null,false,null,null,null,\\\"Q877Ab\\\"],[45753484,null,true,null,null,null,\\\"G9u5lb\\\"],[45699412,null,true,null,null,null,\\\"xLy3Ce\\\"],[45447950,null,true,null,null,null,\\\"lU0ald\\\"],[45447953,null,true,null,null,null,\\\"jbTsAe\\\"],[45447919,null,false,null,null,null,\\\"KMEQCe\\\"],[45699332,30,null,null,null,null,\\\"grENN\\\"],[45447930,null,null,null,\\\"signed_out_users\\\",null,\\\"JTFNhb\\\"],[45447928,null,true,null,null,null,\\\"Wh7on\\\"],[45447938,null,true,null,null,null,\\\"uVDGld\\\"],[45709766,null,true,null,null,null,\\\"sSVuJ\\\"],[45776470,null,true,null,null,null,\\\"A9MFwf\\\"],[45447926,null,true,null,null,null,\\\"IUIyxe\\\"],[45793915,null,true,null,null,null,\\\"Vwrzsf\\\"],[45721043,null,true,null,null,null,\\\"FcV2Ie\\\"],[45447924,null,true,null,null,null,\\\"hMPU3c\\\"],[45447934,null,null,null,\\\"https://forms.gle/DuQUYavHhwfEo4sp9\\\",null,\\\"gvGLK\\\"],[45447932,null,false,null,null,null,\\\"jlJZI\\\"]],\\\"CAMSOx0+/7riEsDRsgYOrrekEBYEFtqsNuYD3f+wBAq20g0Kuv0FCqMuFtjVAgqrrQAOppMFCpO2DQ6Jng4K\\\"]]]\",\"Tb2qJf\":4,\"UUFaWc\":\"%.@.null,1000,2]\",\"Vvafkd\":false,\"Yllh3e\":\"%.@.1783855300012332,40944331,674690125]\",\"YlwcZe\":\"%.@.3,[1],[3600],2,[15,4,13,14,12,2]]\",\"ZZZ7Uc\":\"\",\"b5W2zf\":\"default_GroupsFrontendUi\",\"cfb2h\":\"boq_groupsfrontendserver_20260706.01_p0\",\"eNnkwf\":\"1783855300\",\"eptZe\":\"/_/GroupsFrontendUi/\",\"fPDxwd\":[97493638,97493660,105739272],\"gGcLoe\":false,\"gSs3jc\":1,\"hpRnh\":1,\"hsFLT\":\"%.@.null,10,3]\",\"iCzhFc\":false,\"nQyAE\":{\"G9u5lb\":\"true\",\"lU0ald\":\"true\",\"grENN\":\"30\",\"uVDGld\":\"true\",\"Vwrzsf\":\"true\",\"XGnjIb\":\"true\",\"FcV2Ie\":\"true\",\"j3OYnd\":\"false\",\"qAj4w\":\"false\",\"MohPG\":\"false\",\"Q877Ab\":\"false\",\"xLy3Ce\":\"true\",\"KMEQCe\":\"false\",\"JTFNhb\":\"signed_out_users\",\"IUIyxe\":\"true\",\"OeRc3d\":\"true\",\"paZwJ\":\"true\",\"PoJR0\":\"false\",\"xNsvyb\":\"true\",\"ckfnge\":\"true\",\"udsJQe\":\"true\",\"jbTsAe\":\"true\",\"Wh7on\":\"true\",\"sSVuJ\":\"true\",\"gvGLK\":\"https://forms.gle/DuQUYavHhwfEo4sp9\",\"jlJZI\":\"false\",\"ervCtb\":\"true\",\"A9MFwf\":\"true\",\"jKfwq\":\"false\"},\"p9hQne\":\"https://www.gstatic.com/_/boq-groups/_/r/\",\"qwAQke\":\"GroupsFrontendUi\",\"qymVe\":\"T3FRZfOofDu0jszSQW57tNKAus8\",\"rtQCxc\":-330,\"u4g7r\":\"%.@.null,1,3]\",\"w2btAe\":\"%.@.null,null,\\\"\\\",false,null,null,null,false]\",\"xn5OId\":false,\"xnI9P\":false,\"xwAfE\":true,\"y2FhP\":\"prod\",\"yFnxrf\":1884,\"zChJod\":\"%.@.]\"}; window[\"_F_toggles_default_GroupsFrontendUi\"] = [0x30081086, 0x30b0b301, 0x100c5ad, ]; (function(){'use strict';var a=window,d=a.performance,l=k();a.cc_latency_start_time=d&&d.now?0:d&&d.timing&&d.timing.navigationStart?d.timing.navigationStart:l;function k(){return d&&d.now?d.now():(new Date).getTime()}function n(e){if(d&&d.now&&d.mark){var g=d.mark(e);if(g)return g.startTime;if(d.getEntriesByName&&(e=d.getEntriesByName(e).pop()))return e.startTime}return k()}a.onaft=function(){n(\"aft\")};a._isLazyImage=function(e){return e.hasAttribute(\"data-src\")||e.hasAttribute(\"data-ils\")||e.getAttribute(\"loading\")===\"lazy\"}; a.l=function(e){function g(b){var c={};c[b]=k();a.cc_latency.push(c)}function m(b){var c=n(\"iml\");b.setAttribute(\"data-iml\",c);retu",
+ "window.WIZ_global_data = {\"AfY8Hf\":false,\"DpimGf\":false,\"EP1ykd\":[\"/_/*\"],\"FdrFJe\":\"-996371749730056419\",\"HiPsbb\":1,\"Im6cmf\":\"/_/GroupsFrontendUi\",\"LVIXXb\":1,\"LoQv7e\":false,\"MT7f9b\":[],\"MUE6Ne\":\"groups-frontend\",\"PLnRge\":\"https://docs.google.com/picker\",\"QrtxK\":\"\",\"S06Grb\":\"\",\"S6lZl\":112976253,\"TSDtV\":\"%.@.[[null,[[45447917,null,true,null,null,null,\\\"OeRc3d\\\"],[45447918,null,true,null,null,null,\\\"paZwJ\\\"],[45749384,null,true,null,null,null,\\\"ervCtb\\\"],[45709804,null,false,null,null,null,\\\"PoJR0\\\"],[45697011,null,true,null,null,null,\\\"xNsvyb\\\"],[45734189,null,true,null,null,null,\\\"XGnjIb\\\"],[45448406,null,false,null,null,null,\\\"jKfwq\\\"],[45780063,null,false,null,null,null,\\\"j3OYnd\\\"],[45447921,null,false,null,null,null,\\\"qAj4w\\\"],[45655177,null,true,null,null,null,\\\"VvKUq\\\"],[45447945,null,true,null,null,null,\\\"ckfnge\\\"],[45459555,null,false,null,null,null,\\\"Imeoqb\\\"],[45447936,null,false,null,null,null,\\\"MohPG\\\"],[45532875,null,true,null,null,null,\\\"udsJQe\\\"],[45722772,null,true,null,null,null,\\\"MVCE3b\\\"],[45646796,null,false,null,null,null,\\\"Q877Ab\\\"],[45753484,null,true,null,null,null,\\\"G9u5lb\\\"],[45699412,null,true,null,null,null,\\\"xLy3Ce\\\"],[45447950,null,true,null,null,null,\\\"lU0ald\\\"],[45447953,null,true,null,null,null,\\\"jbTsAe\\\"],[45447919,null,false,null,null,null,\\\"KMEQCe\\\"],[45699332,30,null,null,null,null,\\\"grENN\\\"],[45447930,null,null,null,\\\"signed_out_users\\\",null,\\\"JTFNhb\\\"],[45447928,null,true,null,null,null,\\\"Wh7on\\\"],[45447938,null,true,null,null,null,\\\"uVDGld\\\"],[45709766,null,true,null,null,null,\\\"sSVuJ\\\"],[45776470,null,true,null,null,null,\\\"A9MFwf\\\"],[45447926,null,true,null,null,null,\\\"IUIyxe\\\"],[45793915,null,true,null,null,null,\\\"Vwrzsf\\\"],[45721043,null,true,null,null,null,\\\"FcV2Ie\\\"],[45447924,null,true,null,null,null,\\\"hMPU3c\\\"],[45447934,null,null,null,\\\"https://forms.gle/DuQUYavHhwfEo4sp9\\\",null,\\\"gvGLK\\\"],[45447932,null,false,null,null,null,\\\"jlJZI\\\"]],\\\"CAMSOx0+/7riEsDRsgYOrrekEBYEFtqsNuYD3f+wBAq20g0Kuv0FCqMuFtjVAgqrrQAOppMFCpO2DQ6Jng4K\\\"]]]\",\"Tb2qJf\":4,\"UUFaWc\":\"%.@.null,1000,2]\",\"Vvafkd\":false,\"Yllh3e\":\"%.@.1783855303035198,122390333,3089100502]\",\"YlwcZe\":\"%.@.3,[1],[3600],2,[15,4,13,14,12,2]]\",\"ZZZ7Uc\":\"\",\"b5W2zf\":\"default_GroupsFrontendUi\",\"cfb2h\":\"boq_groupsfrontendserver_20260706.01_p0\",\"eNnkwf\":\"1783855302\",\"eptZe\":\"/_/GroupsFrontendUi/\",\"fPDxwd\":[97493638,97493660,105739272],\"gGcLoe\":false,\"gSs3jc\":1,\"hpRnh\":1,\"hsFLT\":\"%.@.null,10,3]\",\"iCzhFc\":false,\"nQyAE\":{\"G9u5lb\":\"true\",\"lU0ald\":\"true\",\"grENN\":\"30\",\"uVDGld\":\"true\",\"Vwrzsf\":\"true\",\"XGnjIb\":\"true\",\"FcV2Ie\":\"true\",\"j3OYnd\":\"false\",\"qAj4w\":\"false\",\"MohPG\":\"false\",\"Q877Ab\":\"false\",\"xLy3Ce\":\"true\",\"KMEQCe\":\"false\",\"JTFNhb\":\"signed_out_users\",\"IUIyxe\":\"true\",\"OeRc3d\":\"true\",\"paZwJ\":\"true\",\"PoJR0\":\"false\",\"xNsvyb\":\"true\",\"ckfnge\":\"true\",\"udsJQe\":\"true\",\"jbTsAe\":\"true\",\"Wh7on\":\"true\",\"sSVuJ\":\"true\",\"gvGLK\":\"https://forms.gle/DuQUYavHhwfEo4sp9\",\"jlJZI\":\"false\",\"ervCtb\":\"true\",\"A9MFwf\":\"true\",\"jKfwq\":\"false\"},\"p9hQne\":\"https://www.gstatic.com/_/boq-groups/_/r/\",\"qwAQke\":\"GroupsFrontendUi\",\"qymVe\":\"rgJAJVKfNWAtB9TrkXTajGmzJdc\",\"rtQCxc\":-330,\"u4g7r\":\"%.@.null,1,3]\",\"w2btAe\":\"%.@.null,null,\\\"\\\",false,null,null,null,false]\",\"xn5OId\":false,\"xnI9P\":false,\"xwAfE\":true,\"y2FhP\":\"prod\",\"yFnxrf\":1884,\"zChJod\":\"%.@.]\"}; window[\"_F_toggles_default_GroupsFrontendUi\"] = [0x30081086, 0x30b0b301, 0x100c5ad, ]; (function(){'use strict';var a=window,d=a.performance,l=k();a.cc_latency_start_time=d&&d.now?0:d&&d.timing&&d.timing.navigationStart?d.timing.navigationStart:l;function k(){return d&&d.now?d.now():(new Date).getTime()}function n(e){if(d&&d.now&&d.mark){var g=d.mark(e);if(g)return g.startTime;if(d.getEntriesByName&&(e=d.getEntriesByName(e).pop()))return e.startTime}return k()}a.onaft=function(){n(\"aft\")};a._isLazyImage=function(e){return e.hasAttribute(\"data-src\")||e.hasAttribute(\"data-ils\")||e.getAttribute(\"loading\")===\"lazy\"}; a.l=function(e){function g(b){var c={};c[b]=k();a.cc_latency.push(c)}function m(b){var c=n(\"iml\");b.setAttribute(\"data-iml\",c);ret"
+ ],
+ "url": "https://github.com/wazuh/community/issues/64209",
+ "labels": [
+ "Google Groups",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/oajani",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 64177,
+ "title": "Wazuh detected python 3.9 but I can't find it anywhere? https://www.reddit.com/r/Wazuh/comments/1slb1ex/wazuh_detected_python_39_but_i_cant_find_it/",
+ "body": "In the detection events it doesn't say where the exe file is either.\n\n submitted by /u/lgq2002\n\n[link] [comments]\nhttps://www.reddit.com/r/Wazuh/comments/1slb1ex/wazuh_detected_python_39_but_i_cant_find_it/",
+ "comments": [
+ "Closing due to inactivity"
+ ],
+ "external_community": [],
+ "url": "https://github.com/wazuh/community/issues/64177",
+ "labels": [
+ "Reddit",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/sakib789",
+ "ambassador",
+ "review/quality",
+ "reviewer/mentor/Bony",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 64170,
+ "title": "dileepkumarchokkapu https://wazuh.slack.com/archives/C0A933R8E/p1776175229611059",
+ "body": "Hi <@U01HXEV6SL8> We have a customer deployment where Wazuh is hosted on\nAmazon Web Services. For the indexing, we are evaluating scalable storage\noptions and would appreciate guidance from the community. Specifically: 1\\. Is\nusing EFS a recommended approach for scalable index storage in Wazuh\ndeployments? 2\\. Or is EBS still the preferred option for performance and\nreliability? Any insights on best practices or real-world implementations\nwould be highly valuable.\nhttps://wazuh.slack.com/archives/C0A933R8E/p1776175229611059",
+ "comments": [
+ "Hi @fervbmx \n\nYour response is technically correct. For furture reference, I would suggest please attache refrences as well.\nThe user is statisfied with your answer so I'm closing this issue. Please reopne it if needed. \n\nThank you!"
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/64170",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "Resolved",
+ "reviewer/team/community/Stuti3097",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 64155,
+ "title": "cuppeligandalf https://discord.com/channels/1049711339578331186/1049711340316541004/1493525281804259420",
+ "body": "Hello, I need to find a way to overcome high cpu usage and of my wazuh cluster. This happens because of a scheduled scan jobs in our network. It causes cluster to force its eps limits. There are few scanners that scans entire network and creates unnecessary noise. Most of the logs are timeout and server-rst logs. I have load balancers in front of wazuh using nginx. These are firewall logs and are not created by wazuh agents. I researched some ways such as setting rule level 0 in logs for the scanner source ips but i think that doesnt affect cpu usage. What are my viable options here?\nhttps://discord.com/channels/1049711339578331186/1049711340316541004/1493525281804259420",
+ "comments": [
+ "This issue is stale because it has been open for 15 days with no activity.",
+ "Closing due to inactivity"
+ ],
+ "external_community": [
+ "[Linked Discord conversation \u2014 not fetchable without a bot token in that server.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/64155",
+ "labels": [
+ "Discord",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "stale",
+ "reviewer/team/community/sakib789",
+ "closed/inactivity",
+ "review/quality/community_team/internal",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 64079,
+ "title": "shimanointern https://wazuh.slack.com/archives/C0A933R8E/p1775811211131919",
+ "body": "Hi <@U01HXEV6SL8>, Can you help me to check my Wazuh pipeline.json file? I\nwant to route FortiGate and PaloAlto logs into different indexer. Some of the\nlog sources are in different timezone so I used the `devid` and `location`\nfields to identify which log source is UTC+8, UTC+7, or UTC+1/2* I want the\n`timestamp` field used in Wazuh Discover page more accurate, so I appended\nboth `data.date` and `data.time` fields into the `timestamp` field for\nFortiGate log. I try to send a test log over using another machine: ```echo\n'date=2026-04-10 time=13:57:12 devname=\"TEST-FW1\" devid=\"test10\"\neventtime=1773824353106412412 tz=\"+0800\" logid=\"0101039947\" type=\"event\"\nsubtype=\"vpn\" level=\"information\" vd=\"root\" logdesc=\"SSL VPN tunnel up\"\naction=\"tunnel-up\" tunneltype=\"ssl-tunnel\" tunnelid=XXXXXXXX remip=XX.XX.XX.XX\ntunnelip=XX.XX.XX.XX user=\"testuser\" group=\"Remote vpn\" dst_host=\"N/A\"\nreason=\"tunnel established\" msg=\"SSL tunnel established' | nc -u wazuh-IP\n5514``` I saw the log is shown in the alerts.json file:\n```{\"timestamp\":\"2026-04-10T16:23:15.699+0800\",\"rule\":{\"level\":3,\"description\":\"Fortigate:\nVPN user connected.\",\"id\":\"81622\",\"mitre\":{\"id\":[\"T1078\"],\"tactic\":[\"Defense\nEvasion\",\"Persistence\",\"Privilege Escalation\",\"Initial\nAccess\"],\"technique\":[\"Valid\nAccounts\"]},\"firedtimes\":9,\"mail\":false,\"groups\":[\"fortigate\",\"syslog\",\"authentication_success\"],\"gdpr\":[\"IV_32.2\"],\"gpg13\":[\"7.1\"],\"hipaa\":[\"164.312.b\"],\"nist_800_53\":[\"AC.7\",\"AU.14\"],\"pci_dss\":[\"10.2.5\"]},\"agent\":{\"id\":\"000\",\"name\":\"wazuh-\nsiem\"},\"manager\":{\"name\":\"wazuh-\nsiem\"},\"id\":\"1775809395.287867\",\"full_log\":\"date=2026-04-10 time=16:01:01\ndevname=\\\"TEST-FW1\\\" devid=\\\"test10\\\" eventtime=1773824353106412412\ntz=\\\"+0800\\\" logid=\\\"0101039947\\\" type=\\\"event\\\" subtype=\\\"vpn\\\"\nlevel=\\\"information\\\" vd=\\\"root\\\" logdesc=\\\"SSL VPN tunnel up\\\"\naction=\\\"tunnel-up\\\" tunneltype=\\\"ssl-tunnel\\\" tunnelid=XXXXXXXXX\nremip=XX.XX.XX.XX tunnelip=XX.XX.XX.XX user=\\\"testuser\\\" group=\\\"Remote vpn\\\"\ndst_host=\\\"N/A\\\" reason=\\\"tunnel established\\\" msg=\\\"SSL tunnel\nestablished\",\"decoder\":{\"name\":\"fortigate-\nfirewall-v6\"},\"data\":{\"action\":\"tunnel-\nup\",\"dstuser\":\"testuser\",\"tunneltype\":\"ssl-\ntunnel\",\"devid\":\"test10\",\"eventtime\":\"1773824353106412412\",\"level\":\"information\",\"logdesc\":\"SSL\nVPN tunnel up\",\"logid\":\"0101039947\",\"msg\":\"\\\"SSL\",\"reason\":\"tunnel\nestablished\",\"subtype\":\"vpn\",\"date\":\"2026-04-10\",\"time\":\"16:01:01\",\"type\":\"event\",\"vd\":\"root\",\"remip\":\"XX.XX.XX.XX\",\"tunnelip\":\"XX.XX.XX.XX\"},\"location\":\"XX.XX.XX.XX\"}```\nThis `devid: test10` supposed to be UTC+7 but I don't know why it show UTC+8\nin the timestamp. And the `timestamp` output field is not the time in the log\nbut the time Wazuh received the log. Also, even though this log is seen in the\nalerts.json file. I don't see this log in the Discover Page with the correct\nindexer selected. Please verify with my `pipeline.json` in this chat's reply.\nhttps://wazuh.slack.com/archives/C0A933R8E/p1775811211131919",
+ "comments": [
+ "Troubleshooting blog post due for today",
+ "This issue is stale because it has been open for 15 days with no activity.",
+ "Close as completed"
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/64079",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "stale",
+ "reviewer/team/community/oajani",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 64076,
+ "title": "saiivarun.mr https://wazuh.slack.com/archives/C07BK5RJM3R/p1775802406259909",
+ "body": "Hello Team,\nWe came across the following advisory:\nhttps://github.com/wazuh/wazuh/security/advisories/GHSA-wvg9-7q49-c7mg\nIt mentions a vulnerability in the agent Dockerfile related to downloading files using the `curl` command. As per the advisory, this issue has been addressed in the latest 4.14 agent version by introducing a PEM certificate for secure HTTPS communication.\nGiven this, could you please clarify why the manager Dockerfile does not include a similar certificate configuration step?\nIf updates are required for both the manager and agent, it would be more straightforward for us to handle them together. Kindly provide clarification on this at the earliest.\nhttps://wazuh.slack.com/archives/C07BK5RJM3R/p1775802406259909",
+ "comments": [
+ "LGTM"
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/64076",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/oajani",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 64041,
+ "title": "augustocubilla19 https://wazuh.slack.com/archives/C0A933R8E/p1775751636209529",
+ "body": "Hello everyone. Has anyone here integrated or implemented MCP with Wazuh for\nthreat hunting enhancement, etc.?\nhttps://wazuh.slack.com/archives/C0A933R8E/p1775751636209529",
+ "comments": [
+ "Currently working on a major incident",
+ "Close due to user inactivity"
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/64041",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/sakib789",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 63947,
+ "title": "chermitinader https://wazuh.slack.com/archives/C0A933R8E/p1775481991082259",
+ "body": "hi everyone can i ask for assistance on something else i cant seem to be able\nto see ram usage in the discover dashboard even when filtering i dont get any\nresult PS: i already enabled the needed config to monitor system resources\nheres a screenshot\nhttps://wazuh.slack.com/archives/C0A933R8E/p1775481991082259",
+ "comments": [
+ "LGTM"
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/63947",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/sakib789",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 63855,
+ "title": "sivamaddineni333 https://wazuh.slack.com/archives/C0A933R8E/p1775082383981959",
+ "body": "HI <@U04HKJDKZ9T>, is it possible to configure agent in a way to get browser\nhistory or the url's users used, we are really struggling to the see what url\nhttps://wazuh.slack.com/archives/C0A933R8E/p1775082383981959",
+ "comments": [
+ "Moved to the current week.",
+ "LGTM"
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/63855",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/sakib789",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 63542,
+ "title": "oknivrech https://wazuh.slack.com/archives/C0A933R8E/p1774012186113609",
+ "body": "Will this rule work? Or should I escape the \u201c/\u201c? And if it possible to use\ndifferent is_sid with comma? ``` 255003,\n255002postfix/active|postfix/incoming|postfix/maildrop|exim4/input|exim4/msglog\nWhitelist postfix and exim4```\nhttps://wazuh.slack.com/archives/C0A933R8E/p1774012186113609",
+ "comments": [
+ "LGTM.",
+ "This issue is stale because it has been open for 15 days with no activity.",
+ "Hi @dgamboa1605,\n\nIt seems the issue has been resolved. Please reopen if they need further assistance on this. Thanks!"
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/63542",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "stale",
+ "reviewer/team/community/Hasitha9796",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 63452,
+ "title": "D3vil0p3r https://github.com/wazuh/wazuh-docker/pull/2239",
+ "body": "Expliciting the registry in the docker-compose files make podman users to use compose quickly, instead of manually editing the `registries.conf` file in the systems.\n\nhttps://github.com/wazuh/wazuh-docker/pull/2239",
+ "comments": [
+ "Hello \n\nThank you so much for your contribution!\n\nWe currently also use deployment YAML files to test our deployment processes, which need to maintain this format to be able to modify the registry where the images come from.\nI appreciate your contribution again, but we won't be able to merge this change since we've also considered using variables to assign the image registry, as we have in our image build YAML. However, that would add more complexity to the deployment process for most users, and these YAML files provide the foundation that users can then adapt to create the deployment they need in their environments, as they contain only what is strictly necessary for deployment.",
+ "This issue is stale because it has been open for 15 days with no activity.",
+ "Hi @vcerenu,\n\nI'm closing this as the PR is already closed. Please feel free to reopen it if needed.\n\nThank you!"
+ ],
+ "external_community": [],
+ "url": "https://github.com/wazuh/community/issues/63452",
+ "labels": [
+ "GitHub",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "stale",
+ "Resolved",
+ "reviewer/team/community/Stuti3097",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 63372,
+ "title": "geryniel https://wazuh.slack.com/archives/C07CNG3M11N/p1773397523699919",
+ "body": "I login to my wazuh dashboard but it says could not select any API entry and got stucked on Server APIs tab. I already make some develop like change the password of wazuh-wui, make another hosts, make a permission for wazuh-wui in internal_user.yml, make a token. But there's nothing change. Help me solve this\nhttps://wazuh.slack.com/archives/C07CNG3M11N/p1773397523699919",
+ "comments": [
+ "LGTM ",
+ "Closing this for user's inactivity. Please reopen if the user replies back.",
+ "The user is active again",
+ "The problem is solved, the user confirm that the issue was solved: \n```\nGery Nield Marcheliant\u00a0\u00a0[23:40]\nIT WORKS\nBut I didn't configure it on Cloudflare\nI configured my conf in Apache2 using Proxy to port 55000\nAnd in wazuh.yml, I changed the url to the name of web browser instead using localhost or 127.0.0.1\nAnd now, it is online\nThank you so much for your help\nYou're the best\n```"
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/63372",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/sakib789",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 63136,
+ "title": "peter199042 https://wazuh.slack.com/archives/C07BZJY86G3/p1772692210638849",
+ "body": "Hi Team,\nI lost my admin pasword, and when i run wazuh-passwords-tool.sh to reset admin password on indexer node, it shows an error:\n```/usr/share/wazuh-indexer/plugins/opensearch-security/tools/wazuh-passwords-tool.sh -u admin -p '123asdASF!@#'```\nhttps://wazuh.slack.com/archives/C07BZJY86G3/p1772692210638849",
+ "comments": [
+ "The issue has been resolved. Closing this"
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/63136",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/oajani",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 62961,
+ "title": "hassan.ahmed https://wazuh.slack.com/archives/C0A933R8E/p1772098375290869",
+ "body": "Hey there, I just have a question. Do I need to scan the whole C drive in real\ntime in FIM through Wazuh?\nhttps://wazuh.slack.com/archives/C0A933R8E/p1772098375290869",
+ "comments": [
+ "The FIM module (`syscheck` in the configuration files) comes with a default configuration that you can view here: https://documentation.wazuh.com/current/user-manual/reference/ossec-conf/syscheck.html#default-syscheck-configuration\n\nSo, by default, it does not scan the entire disk. In fact, scanning the entire disk is not a good idea, as both performance and space in the environment will decrease if you do not have sufficient resources.\n\nTo better understand how the FIM module works and all the options you can use, you can read this documentation: https://documentation.wazuh.com/current/user-manual/capabilities/file-integrity/basic-settings.html#basic-settings.\n\nYou can specify the directories you want to scan using in the `ossec.conf` file:\n\n```\n/etc,/usr/bin,/usr/sbin\n```\n\nThis way, the module will only scan those directories, without even looking at the rest of the disk's contents.",
+ "This issue is stale because it has been open for 15 days with no activity.",
+ "Closing as completed"
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/62961",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "stale",
+ "reviewer/team/community/oajani",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 62957,
+ "title": "Shard Failed ",
+ "body": "I have this issue and couldnot fix it.\r\nResponse:\r\n\"took\": 39, \"timed_out\": false, \"_shards\": { \"total\": 394, \"successful\": \r\n393, \"skipped\": 392, \"failed\": 1, \"failures\": [ { \"shard\": 0, \"index\": \r\n\"wazuh-alerts-4.x-2026.02.26\", \"node\": \"R-tf_22kQQaXR77j4-ih9w\", \"reason\": \r\n{ \"type\": \"illegal_argument_exception\", \"reason\": \"Text fields are not \r\noptimised for operations that require per-document field data like \r\naggregations and sorting, so these operations are disabled by default. \r\nPlease use a keyword field instead. Alternatively, set fielddata=true on \r\n[manager.name] in order to load field data by uninverting the inverted \r\nindex. Note that this can use significant memory.\"\r\n\r\n\r\n{ \"took\": 584, \"timed_out\": false, \"_shards\": { \"total\": 394, \"successful\": \r\n393, \"skipped\": 392, \"failed\": 1, \"failures\": [ { \"shard\": 0, \"index\": \r\n\"wazuh-alerts-4.x-2026.02.26\", \"node\": \"R-tf_22kQQaXR77j4-ih9w\", \"reason\": \r\n{ \"type\": \"illegal_argument_exception\", \"reason\": \"Text fields are not \r\noptimised for operations that require per-document field data like \r\naggregations and sorting, so these operations are disabled by default. \r\nPlease use a keyword field instead. Alternatively, set fielddata=true on \r\n[manager.name] in order to load field data by uninverting the inverted \r\nindex. Note that this can use significant memory.\" } } ] }, \"hits\": { \r\n\"total\": 0, \"max_score\": null, \"hits\": [] }, \"aggregations\": { \"buckets\": { \r\n\"doc_count_error_upper_bound\": 0, \"sum_other_doc_count\": 0, \"buckets\": [] } \r\n} }\r\n\r\n-- \r\nYou received this message because you are subscribed to the Google Groups \"Wazuh | Mailing List\" group.\r\nTo unsubscribe from this group and stop receiving emails from it, send an email to wazuh+unsubscribe@googlegroups.com.\r\nTo view this discussion visit https://groups.google.com/d/msgid/wazuh/36eac110-87bf-40fe-8493-192c47e14a9dn%40googlegroups.com.\r\n",
+ "comments": [],
+ "external_community": [
+ "window.WIZ_global_data = {\"AfY8Hf\":false,\"DpimGf\":false,\"EP1ykd\":[\"/_/*\"],\"FdrFJe\":\"9123293403687542560\",\"HiPsbb\":1,\"Im6cmf\":\"/_/GroupsFrontendUi\",\"LVIXXb\":1,\"LoQv7e\":false,\"MT7f9b\":[],\"MUE6Ne\":\"groups-frontend\",\"PLnRge\":\"https://docs.google.com/picker\",\"QrtxK\":\"\",\"S06Grb\":\"\",\"S6lZl\":112976253,\"TSDtV\":\"%.@.[[null,[[45447917,null,true,null,null,null,\\\"OeRc3d\\\"],[45447918,null,true,null,null,null,\\\"paZwJ\\\"],[45749384,null,true,null,null,null,\\\"ervCtb\\\"],[45709804,null,false,null,null,null,\\\"PoJR0\\\"],[45697011,null,true,null,null,null,\\\"xNsvyb\\\"],[45734189,null,true,null,null,null,\\\"XGnjIb\\\"],[45448406,null,false,null,null,null,\\\"jKfwq\\\"],[45780063,null,false,null,null,null,\\\"j3OYnd\\\"],[45447921,null,false,null,null,null,\\\"qAj4w\\\"],[45655177,null,true,null,null,null,\\\"VvKUq\\\"],[45447945,null,true,null,null,null,\\\"ckfnge\\\"],[45459555,null,false,null,null,null,\\\"Imeoqb\\\"],[45447936,null,false,null,null,null,\\\"MohPG\\\"],[45532875,null,true,null,null,null,\\\"udsJQe\\\"],[45722772,null,true,null,null,null,\\\"MVCE3b\\\"],[45646796,null,false,null,null,null,\\\"Q877Ab\\\"],[45753484,null,true,null,null,null,\\\"G9u5lb\\\"],[45699412,null,true,null,null,null,\\\"xLy3Ce\\\"],[45447950,null,true,null,null,null,\\\"lU0ald\\\"],[45447953,null,true,null,null,null,\\\"jbTsAe\\\"],[45447919,null,false,null,null,null,\\\"KMEQCe\\\"],[45699332,30,null,null,null,null,\\\"grENN\\\"],[45447930,null,null,null,\\\"signed_out_users\\\",null,\\\"JTFNhb\\\"],[45447928,null,true,null,null,null,\\\"Wh7on\\\"],[45447938,null,true,null,null,null,\\\"uVDGld\\\"],[45709766,null,true,null,null,null,\\\"sSVuJ\\\"],[45776470,null,true,null,null,null,\\\"A9MFwf\\\"],[45447926,null,true,null,null,null,\\\"IUIyxe\\\"],[45793915,null,true,null,null,null,\\\"Vwrzsf\\\"],[45721043,null,true,null,null,null,\\\"FcV2Ie\\\"],[45447924,null,true,null,null,null,\\\"hMPU3c\\\"],[45447934,null,null,null,\\\"https://forms.gle/DuQUYavHhwfEo4sp9\\\",null,\\\"gvGLK\\\"],[45447932,null,false,null,null,null,\\\"jlJZI\\\"]],\\\"CAMSOx0+/7riEsDRsgYOrrekEBYEFtqsNuYD3f+wBAq20g0Kuv0FCqMuFtjVAgqrrQAOppMFCpO2DQ6Jng4K\\\"]]]\",\"Tb2qJf\":4,\"UUFaWc\":\"%.@.null,1000,2]\",\"Vvafkd\":false,\"Yllh3e\":\"%.@.1783855325423318,122393020,1611334472]\",\"YlwcZe\":\"%.@.3,[1],[3600],2,[15,4,13,14,12,2]]\",\"ZZZ7Uc\":\"\",\"b5W2zf\":\"default_GroupsFrontendUi\",\"cfb2h\":\"boq_groupsfrontendserver_20260706.01_p0\",\"eNnkwf\":\"1783855323\",\"eptZe\":\"/_/GroupsFrontendUi/\",\"fPDxwd\":[97493638,97493660,105739272],\"gGcLoe\":false,\"gSs3jc\":1,\"hpRnh\":1,\"hsFLT\":\"%.@.null,10,3]\",\"iCzhFc\":false,\"nQyAE\":{\"G9u5lb\":\"true\",\"lU0ald\":\"true\",\"grENN\":\"30\",\"uVDGld\":\"true\",\"Vwrzsf\":\"true\",\"XGnjIb\":\"true\",\"FcV2Ie\":\"true\",\"j3OYnd\":\"false\",\"qAj4w\":\"false\",\"MohPG\":\"false\",\"Q877Ab\":\"false\",\"xLy3Ce\":\"true\",\"KMEQCe\":\"false\",\"JTFNhb\":\"signed_out_users\",\"IUIyxe\":\"true\",\"OeRc3d\":\"true\",\"paZwJ\":\"true\",\"PoJR0\":\"false\",\"xNsvyb\":\"true\",\"ckfnge\":\"true\",\"udsJQe\":\"true\",\"jbTsAe\":\"true\",\"Wh7on\":\"true\",\"sSVuJ\":\"true\",\"gvGLK\":\"https://forms.gle/DuQUYavHhwfEo4sp9\",\"jlJZI\":\"false\",\"ervCtb\":\"true\",\"A9MFwf\":\"true\",\"jKfwq\":\"false\"},\"p9hQne\":\"https://www.gstatic.com/_/boq-groups/_/r/\",\"qwAQke\":\"GroupsFrontendUi\",\"qymVe\":\"bpYnQh7dQr_WCjeKYa8rm60XZi8\",\"rtQCxc\":-330,\"u4g7r\":\"%.@.null,1,3]\",\"w2btAe\":\"%.@.null,null,\\\"\\\",false,null,null,null,false]\",\"xn5OId\":false,\"xnI9P\":false,\"xwAfE\":true,\"y2FhP\":\"prod\",\"yFnxrf\":1884,\"zChJod\":\"%.@.]\"}; window[\"_F_toggles_default_GroupsFrontendUi\"] = [0x30081086, 0x30b0b301, 0x100c5ad, ]; (function(){'use strict';var a=window,d=a.performance,l=k();a.cc_latency_start_time=d&&d.now?0:d&&d.timing&&d.timing.navigationStart?d.timing.navigationStart:l;function k(){return d&&d.now?d.now():(new Date).getTime()}function n(e){if(d&&d.now&&d.mark){var g=d.mark(e);if(g)return g.startTime;if(d.getEntriesByName&&(e=d.getEntriesByName(e).pop()))return e.startTime}return k()}a.onaft=function(){n(\"aft\")};a._isLazyImage=function(e){return e.hasAttribute(\"data-src\")||e.hasAttribute(\"data-ils\")||e.getAttribute(\"loading\")===\"lazy\"}; a.l=function(e){function g(b){var c={};c[b]=k();a.cc_latency.push(c)}function m(b){var c=n(\"iml\");b.setAttribute(\"data-iml\",c);ret"
+ ],
+ "url": "https://github.com/wazuh/community/issues/62957",
+ "labels": [
+ "Google Groups",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "No feedback",
+ "reviewer/team/community/sakib789",
+ "review/quality/community_team/internal",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 62874,
+ "title": "openedgarage https://wazuh.slack.com/archives/C0A933R8E/p1771901555567699",
+ "body": "heklo community, what is the best way to mark an alert as a false positive ?\nhttps://wazuh.slack.com/archives/C0A933R8E/p1771901555567699",
+ "comments": [
+ "Hi @Nikhil201Gurjar,\n\nYour response looks good to me. The user is back again. Please check as soon as you can. Thanks!",
+ "Good answer with followup answers also."
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/62874",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/Hasitha9796",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 62867,
+ "title": "vidarpt https://discord.com/channels/1049711339578331186/1049711340316541004/1475542462738206855",
+ "body": "Does the CVE threat intelligence snapshot ever update older CVEs, or does it only add new ones? I've noticed several discrepancies between what is availalbe Wazuh's CTI webpage and the threat intelligence snapshot.\nFor example, CVE-2025-37778. In the snapshot, the \"cna\" and \"cveMetadata\" containers say that the shortName is \"DISCARDED_CNA\" and the publish date is the year 2000. However, if we take a look at the more updated information over at Wazuh's CTI website: https://cti.wazuh.com/vulnerabilities/cves/CVE-2025-37778/json5 we can see that the publish date was in 2025 and there's a ton of information including its attack vector. This is annoying because I first check the snapshot for information and often get bad or missing information because of this, having to fallback to Wazuh's CTI. So does the snapshot never update older CVEs, or am I downloading the wrong, older snapshot? Thanks.\nhttps://discord.com/channels/1049711339578331186/1049711340316541004/1475542462738206855",
+ "comments": [
+ "The user has resolved their issue. ",
+ "LGTM"
+ ],
+ "external_community": [
+ "[Linked Discord conversation \u2014 not fetchable without a bot token in that server.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/62867",
+ "labels": [
+ "Discord",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/oajani",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 62845,
+ "title": "Is this log entry a threat? Looking for a Custom Rule to detect Web Shell activity in static paths. ",
+ "body": "\r\n\r\nHi everyone,\r\n\r\nI am currently monitoring logs via *Wazuh Manager* for a WordPress site. I \r\ndo not have direct terminal access to the web server, so I am relying on \r\nWazuh alerts and logs to report issues to the server admin team.\r\n\r\nI\u2019ve spotted some suspicious log entries that weren't flagged by default \r\nrules, and I\u2019m concerned they might be a *Web Shell* (Backdoor) activity.\r\n\r\n*Example Log Entries (Redacted for Security):*\r\n\r\n 1. [DOMAIN] 172.68.x.x - - [19/Feb/2026:14:01:21 +0700] \"GET \r\n /wp-content/themes/[THEME]/assets/src/js/theme/FieldsetRowPlain.php?d=2f7661722f77[...HEX_REDACTED...]&s=2e6874616363657373 \r\n HTTP/1.1\" 200 4228\r\n 2. [DOMAIN] 172.68.x.x - - [23/Feb/2026:12:18:12 +0700] \"GET \r\n /wp-content/plugins/[PLUGIN]/Admin/BuilderComponentRow.php?6c696768746f6e&d=2f7661722f77[...HEX_REDACTED...] \r\n HTTP/1.1\" 200 5085\r\n\r\n*Observations:*\r\n\r\n - *Suspicious Path:* PHP files are being executed from within /js/ or \r\n deep inside Plugin/Theme asset folders.\r\n - *Hex Parameters:* The parameters d= and s= are Hex-encoded (Decodes to \r\n server paths \r\n like(d) /var/www/html/... and(s) .htaccess,index.php,web.config). \r\n like s=2e6874616363657373(.htaccess) , s=7765622e636f6e666967 (index.php) \r\n , s=696e6465782e706870 (web.config)\r\n\r\n*My Questions:*\r\n1.From your experience, is this a confirmed threat (Web Shell or anything \r\nelse)?\r\n2. Is it normal for a WordPress site to have hex-encoded strings in URL \r\nparameters\r\n\r\n-- \r\nYou received this message because you are subscribed to the Google Groups \"Wazuh | Mailing List\" group.\r\nTo unsubscribe from this group and stop receiving emails from it, send an email to wazuh+unsubscribe@googlegroups.com.\r\nTo view this discussion visit https://groups.google.com/d/msgid/wazuh/11960774-5131-4818-8341-a856c989e8d3n%40googlegroups.com.\r\n",
+ "comments": [
+ "Hello @thony4uu I believe you are currently working on this, hence the status \"in progress.\" If you need time to investigate, kindly let the user know their query is getting attention so they don't feel like nobody is attending to them, then create another community query.\n\nThank you",
+ "Hello @Oajani , my apologies. It was not intentional not to reply to the user all the time. I just had some challenges but will provide a response now",
+ "This issue is stale because it has been open for 15 days with no activity.",
+ "Closing this due to inactivity from the user."
+ ],
+ "external_community": [
+ "window.WIZ_global_data = {\"AfY8Hf\":false,\"DpimGf\":false,\"EP1ykd\":[\"/_/*\"],\"FdrFJe\":\"8381895981547213414\",\"HiPsbb\":1,\"Im6cmf\":\"/_/GroupsFrontendUi\",\"LVIXXb\":1,\"LoQv7e\":false,\"MT7f9b\":[],\"MUE6Ne\":\"groups-frontend\",\"PLnRge\":\"https://docs.google.com/picker\",\"QrtxK\":\"\",\"S06Grb\":\"\",\"S6lZl\":112976253,\"TSDtV\":\"%.@.[[null,[[45447917,null,true,null,null,null,\\\"OeRc3d\\\"],[45447918,null,true,null,null,null,\\\"paZwJ\\\"],[45749384,null,true,null,null,null,\\\"ervCtb\\\"],[45709804,null,false,null,null,null,\\\"PoJR0\\\"],[45697011,null,true,null,null,null,\\\"xNsvyb\\\"],[45734189,null,true,null,null,null,\\\"XGnjIb\\\"],[45448406,null,false,null,null,null,\\\"jKfwq\\\"],[45780063,null,false,null,null,null,\\\"j3OYnd\\\"],[45447921,null,false,null,null,null,\\\"qAj4w\\\"],[45655177,null,true,null,null,null,\\\"VvKUq\\\"],[45447945,null,true,null,null,null,\\\"ckfnge\\\"],[45459555,null,false,null,null,null,\\\"Imeoqb\\\"],[45447936,null,false,null,null,null,\\\"MohPG\\\"],[45532875,null,true,null,null,null,\\\"udsJQe\\\"],[45722772,null,true,null,null,null,\\\"MVCE3b\\\"],[45646796,null,false,null,null,null,\\\"Q877Ab\\\"],[45753484,null,true,null,null,null,\\\"G9u5lb\\\"],[45699412,null,true,null,null,null,\\\"xLy3Ce\\\"],[45447950,null,true,null,null,null,\\\"lU0ald\\\"],[45447953,null,true,null,null,null,\\\"jbTsAe\\\"],[45447919,null,false,null,null,null,\\\"KMEQCe\\\"],[45699332,30,null,null,null,null,\\\"grENN\\\"],[45447930,null,null,null,\\\"signed_out_users\\\",null,\\\"JTFNhb\\\"],[45447928,null,true,null,null,null,\\\"Wh7on\\\"],[45447938,null,true,null,null,null,\\\"uVDGld\\\"],[45709766,null,true,null,null,null,\\\"sSVuJ\\\"],[45776470,null,true,null,null,null,\\\"A9MFwf\\\"],[45447926,null,true,null,null,null,\\\"IUIyxe\\\"],[45793915,null,true,null,null,null,\\\"Vwrzsf\\\"],[45721043,null,true,null,null,null,\\\"FcV2Ie\\\"],[45447924,null,true,null,null,null,\\\"hMPU3c\\\"],[45447934,null,null,null,\\\"https://forms.gle/DuQUYavHhwfEo4sp9\\\",null,\\\"gvGLK\\\"],[45447932,null,false,null,null,null,\\\"jlJZI\\\"]],\\\"CAMSOx0+/7riEsDRsgYOrrekEBYEFtqsNuYD3f+wBAq20g0Kuv0FCqMuFtjVAgqrrQAOppMFCpO2DQ6Jng4K\\\"]]]\",\"Tb2qJf\":4,\"UUFaWc\":\"%.@.null,1000,2]\",\"Vvafkd\":false,\"Yllh3e\":\"%.@.1783855331993641,122393272,1644206838]\",\"YlwcZe\":\"%.@.3,[1],[3600],2,[15,4,13,14,12,2]]\",\"ZZZ7Uc\":\"\",\"b5W2zf\":\"default_GroupsFrontendUi\",\"cfb2h\":\"boq_groupsfrontendserver_20260706.01_p0\",\"eNnkwf\":\"1783855331\",\"eptZe\":\"/_/GroupsFrontendUi/\",\"fPDxwd\":[97493638,97493660,105739272],\"gGcLoe\":false,\"gSs3jc\":1,\"hpRnh\":1,\"hsFLT\":\"%.@.null,10,3]\",\"iCzhFc\":false,\"nQyAE\":{\"G9u5lb\":\"true\",\"lU0ald\":\"true\",\"grENN\":\"30\",\"uVDGld\":\"true\",\"Vwrzsf\":\"true\",\"XGnjIb\":\"true\",\"FcV2Ie\":\"true\",\"j3OYnd\":\"false\",\"qAj4w\":\"false\",\"MohPG\":\"false\",\"Q877Ab\":\"false\",\"xLy3Ce\":\"true\",\"KMEQCe\":\"false\",\"JTFNhb\":\"signed_out_users\",\"IUIyxe\":\"true\",\"OeRc3d\":\"true\",\"paZwJ\":\"true\",\"PoJR0\":\"false\",\"xNsvyb\":\"true\",\"ckfnge\":\"true\",\"udsJQe\":\"true\",\"jbTsAe\":\"true\",\"Wh7on\":\"true\",\"sSVuJ\":\"true\",\"gvGLK\":\"https://forms.gle/DuQUYavHhwfEo4sp9\",\"jlJZI\":\"false\",\"ervCtb\":\"true\",\"A9MFwf\":\"true\",\"jKfwq\":\"false\"},\"p9hQne\":\"https://www.gstatic.com/_/boq-groups/_/r/\",\"qwAQke\":\"GroupsFrontendUi\",\"qymVe\":\"e80uGYjcXMQpxagJaaTHTvMs2kQ\",\"rtQCxc\":-330,\"u4g7r\":\"%.@.null,1,3]\",\"w2btAe\":\"%.@.null,null,\\\"\\\",false,null,null,null,false]\",\"xn5OId\":false,\"xnI9P\":false,\"xwAfE\":true,\"y2FhP\":\"prod\",\"yFnxrf\":1884,\"zChJod\":\"%.@.]\"}; window[\"_F_toggles_default_GroupsFrontendUi\"] = [0x30081086, 0x30b0b301, 0x100c5ad, ]; (function(){'use strict';var a=window,d=a.performance,l=k();a.cc_latency_start_time=d&&d.now?0:d&&d.timing&&d.timing.navigationStart?d.timing.navigationStart:l;function k(){return d&&d.now?d.now():(new Date).getTime()}function n(e){if(d&&d.now&&d.mark){var g=d.mark(e);if(g)return g.startTime;if(d.getEntriesByName&&(e=d.getEntriesByName(e).pop()))return e.startTime}return k()}a.onaft=function(){n(\"aft\")};a._isLazyImage=function(e){return e.hasAttribute(\"data-src\")||e.hasAttribute(\"data-ils\")||e.getAttribute(\"loading\")===\"lazy\"}; a.l=function(e){function g(b){var c={};c[b]=k();a.cc_latency.push(c)}function m(b){var c=n(\"iml\");b.setAttribute(\"data-iml\",c);ret"
+ ],
+ "url": "https://github.com/wazuh/community/issues/62845",
+ "labels": [
+ "Google Groups",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "stale",
+ "review/moderation",
+ "reviewer/team/community/oajani",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 62798,
+ "title": "assim.darwish https://wazuh.slack.com/archives/C0A933R8E/p1771587977686959",
+ "body": "Hello, we faced an issue with the vulnerability detection in Wazuh (currently\nv4.12.0) For example this CVE: We have several Debian 13 servers, which are\naffected (from wazuh perspective), but they have the latest version installed,\nwhere this CVE is fixed: Wazuh says: This issue has been patched in version\n*2.5.0.* In Debian it is fixed with version 2.3.0-3 That is also mentioned in\nthe CTI, but why is it not solved in vulnerability detection?\nhttps://wazuh.slack.com/archives/C0A933R8E/p1771587977686959",
+ "comments": [
+ "This is completed. Closing the community; please reopen if the user responds."
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/62798",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/oajani",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 62789,
+ "title": "Wazuh Windows Agent - missed events https://www.reddit.com/r/Wazuh/comments/1r9rbjq/wazuh_windows_agent_missed_events/",
+ "body": "I use the Wazuh agent to monitor the activities of two domain controllers, but sometimes some events are not recorded by Wazuh (even though they are present in the Windows Event Log).\n\nI don\u2019t think it\u2019s a filtering issue, because events with the same ID are sometimes recorded and sometimes not. What can I check?\n\n submitted by /u/WannabeHawaiiSwimmer\n\n[link] [comments]\nhttps://www.reddit.com/r/Wazuh/comments/1r9rbjq/wazuh_windows_agent_missed_events/",
+ "comments": [
+ "Closed due to inactivity."
+ ],
+ "external_community": [],
+ "url": "https://github.com/wazuh/community/issues/62789",
+ "labels": [
+ "Reddit",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/oajani",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 62785,
+ "title": "rutikmangale18 https://wazuh.slack.com/archives/C07CNG3M11N/p1771569506704569",
+ "body": "I want to understand how the risk assessment module works in Wazuh.\nSpecifically, how is the risk calculated internally?\nI\u2019m looking for a detailed explanation of the risk calculation logic, including the factors, scoring methodology, and how different inputs (like vulnerabilities, agent data, or rules) contribute to the final risk score\nhttps://wazuh.slack.com/archives/C07CNG3M11N/p1771569506704569",
+ "comments": [
+ "Good answer"
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/62785",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/oajani",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 62761,
+ "title": "not installed software ",
+ "body": "Hello, \r\nCan I find list of not installed software wih Wazuh agent. For example I \r\nwant to find list devices with not installed Sysmon or Auditd.\r\n\r\n-- \r\nYou received this message because you are subscribed to the Google Groups \"Wazuh | Mailing List\" group.\r\nTo unsubscribe from this group and stop receiving emails from it, send an email to wazuh+unsubscribe@googlegroups.com.\r\nTo view this discussion visit https://groups.google.com/d/msgid/wazuh/9dc0a1f0-8b15-4bba-b7a1-5f002d2e5d83n%40googlegroups.com.\r\n",
+ "comments": [
+ "Initial response looks good to me. ",
+ "The user has not responded in a reasonable amount of time",
+ "LGTM"
+ ],
+ "external_community": [
+ "window.WIZ_global_data = {\"AfY8Hf\":false,\"DpimGf\":false,\"EP1ykd\":[\"/_/*\"],\"FdrFJe\":\"8412754065985019246\",\"HiPsbb\":1,\"Im6cmf\":\"/_/GroupsFrontendUi\",\"LVIXXb\":1,\"LoQv7e\":false,\"MT7f9b\":[],\"MUE6Ne\":\"groups-frontend\",\"PLnRge\":\"https://docs.google.com/picker\",\"QrtxK\":\"\",\"S06Grb\":\"\",\"S6lZl\":112976253,\"TSDtV\":\"%.@.[[null,[[45447917,null,true,null,null,null,\\\"OeRc3d\\\"],[45447918,null,true,null,null,null,\\\"paZwJ\\\"],[45749384,null,true,null,null,null,\\\"ervCtb\\\"],[45709804,null,false,null,null,null,\\\"PoJR0\\\"],[45697011,null,true,null,null,null,\\\"xNsvyb\\\"],[45734189,null,true,null,null,null,\\\"XGnjIb\\\"],[45448406,null,false,null,null,null,\\\"jKfwq\\\"],[45780063,null,false,null,null,null,\\\"j3OYnd\\\"],[45447921,null,false,null,null,null,\\\"qAj4w\\\"],[45655177,null,true,null,null,null,\\\"VvKUq\\\"],[45447945,null,true,null,null,null,\\\"ckfnge\\\"],[45459555,null,false,null,null,null,\\\"Imeoqb\\\"],[45447936,null,false,null,null,null,\\\"MohPG\\\"],[45532875,null,true,null,null,null,\\\"udsJQe\\\"],[45722772,null,true,null,null,null,\\\"MVCE3b\\\"],[45646796,null,false,null,null,null,\\\"Q877Ab\\\"],[45753484,null,true,null,null,null,\\\"G9u5lb\\\"],[45699412,null,true,null,null,null,\\\"xLy3Ce\\\"],[45447950,null,true,null,null,null,\\\"lU0ald\\\"],[45447953,null,true,null,null,null,\\\"jbTsAe\\\"],[45447919,null,false,null,null,null,\\\"KMEQCe\\\"],[45699332,30,null,null,null,null,\\\"grENN\\\"],[45447930,null,null,null,\\\"signed_out_users\\\",null,\\\"JTFNhb\\\"],[45447928,null,true,null,null,null,\\\"Wh7on\\\"],[45447938,null,true,null,null,null,\\\"uVDGld\\\"],[45709766,null,true,null,null,null,\\\"sSVuJ\\\"],[45776470,null,true,null,null,null,\\\"A9MFwf\\\"],[45447926,null,true,null,null,null,\\\"IUIyxe\\\"],[45793915,null,true,null,null,null,\\\"Vwrzsf\\\"],[45721043,null,true,null,null,null,\\\"FcV2Ie\\\"],[45447924,null,true,null,null,null,\\\"hMPU3c\\\"],[45447934,null,null,null,\\\"https://forms.gle/DuQUYavHhwfEo4sp9\\\",null,\\\"gvGLK\\\"],[45447932,null,false,null,null,null,\\\"jlJZI\\\"]],\\\"CAMSOx0+/7riEsDRsgYOrrekEBYEFtqsNuYD3f+wBAq20g0Kuv0FCqMuFtjVAgqrrQAOppMFCpO2DQ6Jng4K\\\"]]]\",\"Tb2qJf\":4,\"UUFaWc\":\"%.@.null,1000,2]\",\"Vvafkd\":false,\"Yllh3e\":\"%.@.1783855341547956,122397313,1763242382]\",\"YlwcZe\":\"%.@.3,[1],[3600],2,[15,4,13,14,12,2]]\",\"ZZZ7Uc\":\"\",\"b5W2zf\":\"default_GroupsFrontendUi\",\"cfb2h\":\"boq_groupsfrontendserver_20260706.01_p0\",\"eNnkwf\":\"1783855339\",\"eptZe\":\"/_/GroupsFrontendUi/\",\"fPDxwd\":[97493638,97493660,105739272],\"gGcLoe\":false,\"gSs3jc\":1,\"hpRnh\":1,\"hsFLT\":\"%.@.null,10,3]\",\"iCzhFc\":false,\"nQyAE\":{\"G9u5lb\":\"true\",\"lU0ald\":\"true\",\"grENN\":\"30\",\"uVDGld\":\"true\",\"Vwrzsf\":\"true\",\"XGnjIb\":\"true\",\"FcV2Ie\":\"true\",\"j3OYnd\":\"false\",\"qAj4w\":\"false\",\"MohPG\":\"false\",\"Q877Ab\":\"false\",\"xLy3Ce\":\"true\",\"KMEQCe\":\"false\",\"JTFNhb\":\"signed_out_users\",\"IUIyxe\":\"true\",\"OeRc3d\":\"true\",\"paZwJ\":\"true\",\"PoJR0\":\"false\",\"xNsvyb\":\"true\",\"ckfnge\":\"true\",\"udsJQe\":\"true\",\"jbTsAe\":\"true\",\"Wh7on\":\"true\",\"sSVuJ\":\"true\",\"gvGLK\":\"https://forms.gle/DuQUYavHhwfEo4sp9\",\"jlJZI\":\"false\",\"ervCtb\":\"true\",\"A9MFwf\":\"true\",\"jKfwq\":\"false\"},\"p9hQne\":\"https://www.gstatic.com/_/boq-groups/_/r/\",\"qwAQke\":\"GroupsFrontendUi\",\"qymVe\":\"ChgneyB8gSGEBVaZ-7WHPGvg8nQ\",\"rtQCxc\":-330,\"u4g7r\":\"%.@.null,1,3]\",\"w2btAe\":\"%.@.null,null,\\\"\\\",false,null,null,null,false]\",\"xn5OId\":false,\"xnI9P\":false,\"xwAfE\":true,\"y2FhP\":\"prod\",\"yFnxrf\":1884,\"zChJod\":\"%.@.]\"}; window[\"_F_toggles_default_GroupsFrontendUi\"] = [0x30081086, 0x30b0b301, 0x100c5ad, ]; (function(){'use strict';var a=window,d=a.performance,l=k();a.cc_latency_start_time=d&&d.now?0:d&&d.timing&&d.timing.navigationStart?d.timing.navigationStart:l;function k(){return d&&d.now?d.now():(new Date).getTime()}function n(e){if(d&&d.now&&d.mark){var g=d.mark(e);if(g)return g.startTime;if(d.getEntriesByName&&(e=d.getEntriesByName(e).pop()))return e.startTime}return k()}a.onaft=function(){n(\"aft\")};a._isLazyImage=function(e){return e.hasAttribute(\"data-src\")||e.hasAttribute(\"data-ils\")||e.getAttribute(\"loading\")===\"lazy\"}; a.l=function(e){function g(b){var c={};c[b]=k();a.cc_latency.push(c)}function m(b){var c=n(\"iml\");b.setAttribute(\"data-iml\",c);ret"
+ ],
+ "url": "https://github.com/wazuh/community/issues/62761",
+ "labels": [
+ "Google Groups",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/Stuti3097",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 62754,
+ "title": "blackhunterram https://wazuh.slack.com/archives/C07CCCCGHHP/p1771499673709669",
+ "body": "Hi Team\nWazuh FIM + YARA is working, but when downloading a malware ZIP from Malware bazaar, YARA doesn\u2019t trigger \u2014 only rule 550 fires.\n\nIs this expected because YARA scans the ZIP container and not the extracted EXE?\n\nWhat\u2019s the recommended approach \u2014 auto-unzip in active response or scan only extracted files?\nhttps://wazuh.slack.com/archives/C07CCCCGHHP/p1771499673709669",
+ "comments": [
+ "Hi, When FIM detects a change in the monitored directory or file, it triggers a YARA scan active response. The Active Response module automatically executes YARA using the yara.sh script. YARA then scans the file that triggered the FIM alert against its ruleset to determine if it is malware. Here is the complete information: https://documentation.wazuh.com/current/user-manual/capabilities/malware-detection/fim-yara.html#how-it-works\n\nThe YARA scan should be performed on the uncompressed file; this should detect the malware and generate the correct alert.\n\n",
+ "This issue is stale because it has been open for 15 days with no activity.",
+ "Closing this due to inactivity from the user"
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/62754",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "stale",
+ "reviewer/team/community/oajani",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 62672,
+ "title": "rutikmangale18 https://wazuh.slack.com/archives/C07CCCCGHHP/p1771300632547819",
+ "body": "Hi,\nWhen I call the endpoint using:\n`curl -k -X GET \"https://<IP>:55000/agents\" -H \"Authorization: Bearer $TOKEN\"`\nit works correctly.\nHowever, when I try to fetch vulnerabilities by adding `/vulnerabilities` to the URL, I receive the following response:\n`{ \"title\": \"Not found\", \"details\": \"404: Not found\" }`\nCould you please help me understand the possible reasons why the vulnerabilities endpoint is returning a 404 error? Am I using the correct endpoint, or is there any additional configuration required?\nhttps://wazuh.slack.com/archives/C07CCCCGHHP/p1771300632547819",
+ "comments": [
+ "Issue resolved",
+ "LGTM"
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/62672",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/Hasitha9796",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 62661,
+ "title": "Vulnerability Detection error ",
+ "body": "Hi,\r\n\r\nHow can I fix the error below ? (Vulnerability Detection page)\r\n\r\nWhen I clusterized my Wazuh, it lost it's package name, vulnerability name, \r\nOS name.\r\nThe same ocurred to my IT Hygiene page.\r\n\r\n[image: Captura de tela 2026-02-16 140307.png]\r\n\r\n-- \r\nYou received this message because you are subscribed to the Google Groups \"Wazuh | Mailing List\" group.\r\nTo unsubscribe from this group and stop receiving emails from it, send an email to wazuh+unsubscribe@googlegroups.com.\r\nTo view this discussion visit https://groups.google.com/d/msgid/wazuh/db7bbb3e-9fe4-4fb4-b57e-1f673220ae30n%40googlegroups.com.\r\n",
+ "comments": [
+ "1st Answer",
+ "No feedback from the user. Move to **Done** this\n",
+ "Closing this due to inactivity from the user."
+ ],
+ "external_community": [
+ "window.WIZ_global_data = {\"AfY8Hf\":false,\"DpimGf\":false,\"EP1ykd\":[\"/_/*\"],\"FdrFJe\":\"5082806776851154786\",\"HiPsbb\":1,\"Im6cmf\":\"/_/GroupsFrontendUi\",\"LVIXXb\":1,\"LoQv7e\":false,\"MT7f9b\":[],\"MUE6Ne\":\"groups-frontend\",\"PLnRge\":\"https://docs.google.com/picker\",\"QrtxK\":\"\",\"S06Grb\":\"\",\"S6lZl\":112976253,\"TSDtV\":\"%.@.[[null,[[45447917,null,true,null,null,null,\\\"OeRc3d\\\"],[45447918,null,true,null,null,null,\\\"paZwJ\\\"],[45749384,null,true,null,null,null,\\\"ervCtb\\\"],[45709804,null,false,null,null,null,\\\"PoJR0\\\"],[45697011,null,true,null,null,null,\\\"xNsvyb\\\"],[45734189,null,true,null,null,null,\\\"XGnjIb\\\"],[45448406,null,false,null,null,null,\\\"jKfwq\\\"],[45780063,null,false,null,null,null,\\\"j3OYnd\\\"],[45447921,null,false,null,null,null,\\\"qAj4w\\\"],[45655177,null,true,null,null,null,\\\"VvKUq\\\"],[45447945,null,true,null,null,null,\\\"ckfnge\\\"],[45459555,null,false,null,null,null,\\\"Imeoqb\\\"],[45447936,null,false,null,null,null,\\\"MohPG\\\"],[45532875,null,true,null,null,null,\\\"udsJQe\\\"],[45722772,null,true,null,null,null,\\\"MVCE3b\\\"],[45646796,null,false,null,null,null,\\\"Q877Ab\\\"],[45753484,null,true,null,null,null,\\\"G9u5lb\\\"],[45699412,null,true,null,null,null,\\\"xLy3Ce\\\"],[45447950,null,true,null,null,null,\\\"lU0ald\\\"],[45447953,null,true,null,null,null,\\\"jbTsAe\\\"],[45447919,null,false,null,null,null,\\\"KMEQCe\\\"],[45699332,30,null,null,null,null,\\\"grENN\\\"],[45447930,null,null,null,\\\"signed_out_users\\\",null,\\\"JTFNhb\\\"],[45447928,null,true,null,null,null,\\\"Wh7on\\\"],[45447938,null,true,null,null,null,\\\"uVDGld\\\"],[45709766,null,true,null,null,null,\\\"sSVuJ\\\"],[45776470,null,true,null,null,null,\\\"A9MFwf\\\"],[45447926,null,true,null,null,null,\\\"IUIyxe\\\"],[45793915,null,true,null,null,null,\\\"Vwrzsf\\\"],[45721043,null,true,null,null,null,\\\"FcV2Ie\\\"],[45447924,null,true,null,null,null,\\\"hMPU3c\\\"],[45447934,null,null,null,\\\"https://forms.gle/DuQUYavHhwfEo4sp9\\\",null,\\\"gvGLK\\\"],[45447932,null,false,null,null,null,\\\"jlJZI\\\"]],\\\"CAMSOx0+/7riEsDRsgYOrrekEBYEFtqsNuYD3f+wBAq20g0Kuv0FCqMuFtjVAgqrrQAOppMFCpO2DQ6Jng4K\\\"]]]\",\"Tb2qJf\":4,\"UUFaWc\":\"%.@.null,1000,2]\",\"Vvafkd\":false,\"Yllh3e\":\"%.@.1783855347974546,40943654,1595597407]\",\"YlwcZe\":\"%.@.3,[1],[3600],2,[15,4,13,14,12,2]]\",\"ZZZ7Uc\":\"\",\"b5W2zf\":\"default_GroupsFrontendUi\",\"cfb2h\":\"boq_groupsfrontendserver_20260706.01_p0\",\"eNnkwf\":\"1783855346\",\"eptZe\":\"/_/GroupsFrontendUi/\",\"fPDxwd\":[97493638,97493660,105739272],\"gGcLoe\":false,\"gSs3jc\":1,\"hpRnh\":1,\"hsFLT\":\"%.@.null,10,3]\",\"iCzhFc\":false,\"nQyAE\":{\"G9u5lb\":\"true\",\"lU0ald\":\"true\",\"grENN\":\"30\",\"uVDGld\":\"true\",\"Vwrzsf\":\"true\",\"XGnjIb\":\"true\",\"FcV2Ie\":\"true\",\"j3OYnd\":\"false\",\"qAj4w\":\"false\",\"MohPG\":\"false\",\"Q877Ab\":\"false\",\"xLy3Ce\":\"true\",\"KMEQCe\":\"false\",\"JTFNhb\":\"signed_out_users\",\"IUIyxe\":\"true\",\"OeRc3d\":\"true\",\"paZwJ\":\"true\",\"PoJR0\":\"false\",\"xNsvyb\":\"true\",\"ckfnge\":\"true\",\"udsJQe\":\"true\",\"jbTsAe\":\"true\",\"Wh7on\":\"true\",\"sSVuJ\":\"true\",\"gvGLK\":\"https://forms.gle/DuQUYavHhwfEo4sp9\",\"jlJZI\":\"false\",\"ervCtb\":\"true\",\"A9MFwf\":\"true\",\"jKfwq\":\"false\"},\"p9hQne\":\"https://www.gstatic.com/_/boq-groups/_/r/\",\"qwAQke\":\"GroupsFrontendUi\",\"qymVe\":\"5wtHfxOCcL-knvUPlb09VC38roA\",\"rtQCxc\":-330,\"u4g7r\":\"%.@.null,1,3]\",\"w2btAe\":\"%.@.null,null,\\\"\\\",false,null,null,null,false]\",\"xn5OId\":false,\"xnI9P\":false,\"xwAfE\":true,\"y2FhP\":\"prod\",\"yFnxrf\":1884,\"zChJod\":\"%.@.]\"}; window[\"_F_toggles_default_GroupsFrontendUi\"] = [0x30081086, 0x30b0b301, 0x100c5ad, ]; (function(){'use strict';var a=window,d=a.performance,l=k();a.cc_latency_start_time=d&&d.now?0:d&&d.timing&&d.timing.navigationStart?d.timing.navigationStart:l;function k(){return d&&d.now?d.now():(new Date).getTime()}function n(e){if(d&&d.now&&d.mark){var g=d.mark(e);if(g)return g.startTime;if(d.getEntriesByName&&(e=d.getEntriesByName(e).pop()))return e.startTime}return k()}a.onaft=function(){n(\"aft\")};a._isLazyImage=function(e){return e.hasAttribute(\"data-src\")||e.hasAttribute(\"data-ils\")||e.getAttribute(\"loading\")===\"lazy\"}; a.l=function(e){function g(b){var c={};c[b]=k();a.cc_latency.push(c)}function m(b){var c=n(\"iml\");b.setAttribute(\"data-iml\",c);retu"
+ ],
+ "url": "https://github.com/wazuh/community/issues/62661",
+ "labels": [
+ "Google Groups",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/oajani",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 62650,
+ "title": "dileepkumarchokkapu https://wazuh.slack.com/archives/C0A933R8E/p1771246912168249",
+ "body": "Hi <@U01HXEV6SL8>, We are planning to install Wazuh on an Ubuntu server with\nthe following disk layout: nvme0n1 \u2013 100GB (OS disk mounted on /) nvme1n1 \u2013\n2TB (Additional disk attached for storage) Our plan is: Install the Wazuh\nServer(Manager / Indexer / Dashboard) on the 100GB OS disk (nvme0n1) Mount the\n2TB disk (nvme1n1) separately and use it exclusively for all event data and\nconfig storage. /var/lib/wazuh-indexer /var/ossec 1\\. Is this architecture\nsupported and technically feasible? 2\\. Is separating OS and Wazuh data\nstorage considered best practice? 3\\. For future scaling, can we expand\nstorage by attaching additional disks to the existing 2TB volume (e.g., via\nLVM)? Thanks in advance.\nhttps://wazuh.slack.com/archives/C0A933R8E/p1771246912168249",
+ "comments": [
+ "no new response from user"
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/62650",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/oajani",
+ "ambassador",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 62603,
+ "title": "Wazuh Agents Limit ",
+ "body": "Hi! I am working on limiting Wazuh agents. Currently, the process I am \r\nimplementing is using the Wazuh Key agent request to limit the number of \r\nagents that can be registered on the Wazuh Manager. The issue I am getting \r\nis that the Wazuh agent key request is being executed, but the response of \r\nthe agent key request is return code 1. authd just falls back to the \r\ndefault, and it registered the agent through auto enrollment. What I simply \r\nwant is that everything should work fine until the limit is reached, so \r\nwhen the limit is reached, no agents should get connected to that manager. \r\nHow can I fix this, or are there any alternative solutions to this other \r\nthan: \r\n \r\n - Creating a script to manually remove the agents through managing agent \r\n - blocking the port after port 1515 after the limit is reached \r\n\r\nI have listed. All of the information is below any help will be much \r\nappreciated\r\n\r\nI get this in the ossec.log\r\n\r\n* wazuh-authd: WARNING: Key request integration (/var/ossec/lic/license.py) \r\nreturned code 1.*\r\n\r\n\r\n\r\n\r\n*wazuh-remoted: WARNING: (1213): Message from '192.168.18.5' not allowed. \r\nCannot find the ID of the agent. Source agent ID is unknown.wazuh-authd: \r\nINFO: New connection from 192.168.18.5 wazuh-authd: INFO: Received request \r\nfor a new agent (DESKTOP-I5GA80N) from: 192.168.18.5 wazuh-authd: INFO: \r\nAgent key generated for 'DESKTOP-I5GA80N' (requested by 192.168.18.5)```the \r\nscript log*\r\nthe logs of the script\r\n\r\n\r\n\r\n\r\n* === ENROLLMENT REQUEST ===Args: ['/var/ossec/lic/license.py', 'ip', \r\n'192.168.18.21']License check: 1/1 DENIED: License limit reached (1/1)*\r\nthe script check limit in /var/ossec/etc/client.keys and blocks \r\nenrollment if limit is reached but the issue is when it return code 1 the \r\nauthd fall back\r\n\r\n#!/usr/bin/env python3\r\nimport sys\r\nimport json\r\nimport os\r\nimport requests\r\nimport urllib3\r\nfrom datetime import datetime\r\n\r\n# Disable SSL warnings\r\nurllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)\r\n\r\n# ---------------- CONFIG ---------------- #\r\nLICENSE_LIMIT = 1\r\nCLIENT_KEYS = \"/var/ossec/etc/client.keys\"\r\nWAZUH_API_URL = \"https://localhost:55000\"\r\nWAZUH_API_USER = \"admin\"\r\nWAZUH_API_PASSWORD = \"admin-123\" # Change this!\r\n# ---------------------------------------- #\r\n\r\nSCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))\r\nLOG_FILE = os.path.join(SCRIPT_DIR, \"license_key_request.log\")\r\n\r\ndef log_message(message):\r\n timestamp = datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\")\r\n with open(LOG_FILE, \"a\") as f:\r\n f.write(f\"[{timestamp}] {message}\\n\")\r\n\r\ndef get_agent_count():\r\n try:\r\n with open(CLIENT_KEYS, \"r\") as f:\r\n count = sum(1 for line in f if line.strip() and not \r\nline.startswith(\"000\"))\r\n return count\r\n except:\r\n return 0\r\n\r\ndef get_api_token():\r\n \"\"\"Get JWT token from Wazuh API.\"\"\"\r\n try:\r\n response = requests.post(\r\n f\"{WAZUH_API_URL}/security/user/authenticate\",\r\n auth=(WAZUH_API_USER, WAZUH_API_PASSWORD),\r\n verify=False,\r\n timeout=10\r\n )\r\n if response.status_code == 200:\r\n return response.json()['data']['token']\r\n else:\r\n raise Exception(f\"API auth failed: {response.status_code}\")\r\n except Exception as e:\r\n log_message(f\"API authentication error: {str(e)}\")\r\n raise\r\n\r\ndef create_agent_via_api(agent_name, agent_ip, token):\r\n \"\"\"Create agent using Wazuh API.\"\"\"\r\n try:\r\n headers = {\r\n 'Authorization': f'Bearer {token}',\r\n 'Content-Type': 'application/json'\r\n }\r\n\r\n agent_data = {\r\n \"name\": agent_name,\r\n \"ip\": agent_ip\r\n }\r\n\r\n response = requests.post(\r\n f\"{WAZUH_API_URL}/agents\",\r\n headers=headers,\r\n json=agent_data,\r\n verify=False,\r\n timeout=10\r\n )\r\n\r\n if response.status_code == 200:\r\n data = response.json()['data']\r\n return data['id'], data['key']\r\n else:\r\n raise Exception(f\"API error: {response.status_code} - \r\n{response.text}\")\r\n\r\n except Exception as e:\r\n log_message(f\"API error: {str(e)}\")\r\n raise\r\n\r\ndef main():\r\n log_message(f\"=== ENROLLMENT REQUEST ===\")\r\n log_message(f\"Args: {sys.argv}\")\r\n\r\n if len(sys.argv) < 3:\r\n print(json.dumps({\"error\": 1, \"message\": \"Too few arguments\"}))\r\n sys.exit(1)\r\n\r\n request_type = sys.argv[1]\r\n request_value = sys.argv[2]\r\n\r\n if request_type != \"ip\":\r\n print(json.dumps({\"error\": 1, \"message\": \"Only IP requests \r\nsupported\"}))\r\n sys.exit(1)\r\n\r\n agent_ip = request_value\r\n current_count = get_agent_count()\r\n\r\n log_message(f\"License check: {current_count}/{LICENSE_LIMIT}\")\r\n\r\n if current_count >= LICENSE_LIMIT:\r\n msg = f\"License limit reached ({current_count}/{LICENSE_LIMIT})\"\r\n log_message(f\"DENIED: {msg}\")\r\n print(json.dumps({\"error\": 1, \"message\": msg}))\r\n sys.exit(1)\r\n\r\n # Create agent via API\r\n try:\r\n agent_name = f\"agent-{agent_ip.replace('.', '-')}\"\r\n token = get_api_token()\r\n agent_id, agent_key = create_agent_via_api(agent_name, agent_ip, \r\ntoken)\r\n\r\n log_message(f\"ALLOWED: Created agent ID={agent_id}\")\r\n\r\n # Return agent data in the format authd expects\r\n print(json.dumps({\r\n \"error\": 0,\r\n \"data\": {\r\n \"id\": agent_id,\r\n \"name\": agent_name,\r\n \"ip\": agent_ip,\r\n \"key\": agent_key\r\n }\r\n }))\r\n sys.exit(0)\r\n\r\n except Exception as e:\r\n log_message(f\"ERROR: {str(e)}\")\r\n print(json.dumps({\"error\": 1, \"message\": str(e)}))\r\n sys.exit(1)\r\n\r\nif __name__ == \"__main__\":\r\n main()\r\n\r\n-- \r\nYou received this message because you are subscribed to the Google Groups \"Wazuh | Mailing List\" group.\r\nTo unsubscribe from this group and stop receiving emails from it, send an email to wazuh+unsubscribe@googlegroups.com.\r\nTo view this discussion visit https://groups.google.com/d/msgid/wazuh/f9580f28-59db-4344-8943-a0f08cdb99f9n%40googlegroups.com.\r\n",
+ "comments": [
+ "Great efforts. Look good to me "
+ ],
+ "external_community": [
+ "window.WIZ_global_data = {\"AfY8Hf\":false,\"DpimGf\":false,\"EP1ykd\":[\"/_/*\"],\"FdrFJe\":\"8862246286830059580\",\"HiPsbb\":1,\"Im6cmf\":\"/_/GroupsFrontendUi\",\"LVIXXb\":1,\"LoQv7e\":false,\"MT7f9b\":[],\"MUE6Ne\":\"groups-frontend\",\"PLnRge\":\"https://docs.google.com/picker\",\"QrtxK\":\"\",\"S06Grb\":\"\",\"S6lZl\":112976253,\"TSDtV\":\"%.@.[[null,[[45447917,null,true,null,null,null,\\\"OeRc3d\\\"],[45447918,null,true,null,null,null,\\\"paZwJ\\\"],[45749384,null,true,null,null,null,\\\"ervCtb\\\"],[45709804,null,false,null,null,null,\\\"PoJR0\\\"],[45697011,null,true,null,null,null,\\\"xNsvyb\\\"],[45734189,null,true,null,null,null,\\\"XGnjIb\\\"],[45448406,null,false,null,null,null,\\\"jKfwq\\\"],[45780063,null,false,null,null,null,\\\"j3OYnd\\\"],[45447921,null,false,null,null,null,\\\"qAj4w\\\"],[45655177,null,true,null,null,null,\\\"VvKUq\\\"],[45447945,null,true,null,null,null,\\\"ckfnge\\\"],[45459555,null,false,null,null,null,\\\"Imeoqb\\\"],[45447936,null,false,null,null,null,\\\"MohPG\\\"],[45532875,null,true,null,null,null,\\\"udsJQe\\\"],[45722772,null,true,null,null,null,\\\"MVCE3b\\\"],[45646796,null,false,null,null,null,\\\"Q877Ab\\\"],[45753484,null,true,null,null,null,\\\"G9u5lb\\\"],[45699412,null,true,null,null,null,\\\"xLy3Ce\\\"],[45447950,null,true,null,null,null,\\\"lU0ald\\\"],[45447953,null,true,null,null,null,\\\"jbTsAe\\\"],[45447919,null,false,null,null,null,\\\"KMEQCe\\\"],[45699332,30,null,null,null,null,\\\"grENN\\\"],[45447930,null,null,null,\\\"signed_out_users\\\",null,\\\"JTFNhb\\\"],[45447928,null,true,null,null,null,\\\"Wh7on\\\"],[45447938,null,true,null,null,null,\\\"uVDGld\\\"],[45709766,null,true,null,null,null,\\\"sSVuJ\\\"],[45776470,null,true,null,null,null,\\\"A9MFwf\\\"],[45447926,null,true,null,null,null,\\\"IUIyxe\\\"],[45793915,null,true,null,null,null,\\\"Vwrzsf\\\"],[45721043,null,true,null,null,null,\\\"FcV2Ie\\\"],[45447924,null,true,null,null,null,\\\"hMPU3c\\\"],[45447934,null,null,null,\\\"https://forms.gle/DuQUYavHhwfEo4sp9\\\",null,\\\"gvGLK\\\"],[45447932,null,false,null,null,null,\\\"jlJZI\\\"]],\\\"CAMSOx0+/7riEsDRsgYOrrekEBYEFtqsNuYD3f+wBAq20g0Kuv0FCqMuFtjVAgqrrQAOppMFCpO2DQ6Jng4K\\\"]]]\",\"Tb2qJf\":4,\"UUFaWc\":\"%.@.null,1000,2]\",\"Vvafkd\":false,\"Yllh3e\":\"%.@.1783855353405596,40951747,855689629]\",\"YlwcZe\":\"%.@.3,[1],[3600],2,[15,4,13,14,12,2]]\",\"ZZZ7Uc\":\"\",\"b5W2zf\":\"default_GroupsFrontendUi\",\"cfb2h\":\"boq_groupsfrontendserver_20260706.01_p0\",\"eNnkwf\":\"1783855352\",\"eptZe\":\"/_/GroupsFrontendUi/\",\"fPDxwd\":[97493638,97493660,105739272],\"gGcLoe\":false,\"gSs3jc\":1,\"hpRnh\":1,\"hsFLT\":\"%.@.null,10,3]\",\"iCzhFc\":false,\"nQyAE\":{\"G9u5lb\":\"true\",\"lU0ald\":\"true\",\"grENN\":\"30\",\"uVDGld\":\"true\",\"Vwrzsf\":\"true\",\"XGnjIb\":\"true\",\"FcV2Ie\":\"true\",\"j3OYnd\":\"false\",\"qAj4w\":\"false\",\"MohPG\":\"false\",\"Q877Ab\":\"false\",\"xLy3Ce\":\"true\",\"KMEQCe\":\"false\",\"JTFNhb\":\"signed_out_users\",\"IUIyxe\":\"true\",\"OeRc3d\":\"true\",\"paZwJ\":\"true\",\"PoJR0\":\"false\",\"xNsvyb\":\"true\",\"ckfnge\":\"true\",\"udsJQe\":\"true\",\"jbTsAe\":\"true\",\"Wh7on\":\"true\",\"sSVuJ\":\"true\",\"gvGLK\":\"https://forms.gle/DuQUYavHhwfEo4sp9\",\"jlJZI\":\"false\",\"ervCtb\":\"true\",\"A9MFwf\":\"true\",\"jKfwq\":\"false\"},\"p9hQne\":\"https://www.gstatic.com/_/boq-groups/_/r/\",\"qwAQke\":\"GroupsFrontendUi\",\"qymVe\":\"icIdiS9cB1daF_VGS36PvJ_3lI4\",\"rtQCxc\":-330,\"u4g7r\":\"%.@.null,1,3]\",\"w2btAe\":\"%.@.null,null,\\\"\\\",false,null,null,null,false]\",\"xn5OId\":false,\"xnI9P\":false,\"xwAfE\":true,\"y2FhP\":\"prod\",\"yFnxrf\":1884,\"zChJod\":\"%.@.]\"}; window[\"_F_toggles_default_GroupsFrontendUi\"] = [0x30081086, 0x30b0b301, 0x100c5ad, ]; (function(){'use strict';var a=window,d=a.performance,l=k();a.cc_latency_start_time=d&&d.now?0:d&&d.timing&&d.timing.navigationStart?d.timing.navigationStart:l;function k(){return d&&d.now?d.now():(new Date).getTime()}function n(e){if(d&&d.now&&d.mark){var g=d.mark(e);if(g)return g.startTime;if(d.getEntriesByName&&(e=d.getEntriesByName(e).pop()))return e.startTime}return k()}a.onaft=function(){n(\"aft\")};a._isLazyImage=function(e){return e.hasAttribute(\"data-src\")||e.hasAttribute(\"data-ils\")||e.getAttribute(\"loading\")===\"lazy\"}; a.l=function(e){function g(b){var c={};c[b]=k();a.cc_latency.push(c)}function m(b){var c=n(\"iml\");b.setAttribute(\"data-iml\",c);retur"
+ ],
+ "url": "https://github.com/wazuh/community/issues/62603",
+ "labels": [
+ "Google Groups",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/Stuti3097",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 62589,
+ "title": "jonas.grundner https://wazuh.slack.com/archives/C0A933R8E/p1770986661732759",
+ "body": "Hey <@U01HXEV6SL8> I fiddled around with custom decoders and noticed that I\nseemingly cannot use different names for sibling decoders with the same parent\nor only one of them is applied. Also, fields that the parent decoder matched\nseem to be ignored when it has a child decoder. For instance, I wanted to\ncreate a generic CEF decoder like this: ```\nCEF:0\\|^((?:\\\\\\\\.|[^|])*)\\|((?:\\\\\\\\.|[^|])*)\\|((?:\\\\\\\\.|[^|])*)\\|(?:((?:\\\\\\\\.|[^|])*)\\|)?((?:\\\\\\\\.|[^|])*)\\|(\\d+)\\|\nvendor, product, product_version, class_id, event_name,\nseverity``` Which works fine on its own.\n```CEF:0|Ubiquiti|UniFi Network|9.3.33|544|Admin Accessed UniFi\nNetwork|1|UNIFIcategory=System UNIFIsubCategory=Admin UNIFIhost=Office UDM Pro\nUNIFIaccessMethod=web UNIFIadmin=Craig src=105.5.138.59 msg=Craig accessed\nUniFi Network using the web. Source IP: 105.5.138.59 **Phase 1: Completed pre-\ndecoding. full event: 'CEF:0|Ubiquiti|UniFi Network|9.3.33|544|Admin Accessed\nUniFi Network|1|UNIFIcategory=System UNIFIsubCategory=Admin UNIFIhost=Office\nUDM Pro UNIFIaccessMethod=web UNIFIadmin=Craig src=105.5.138.59 msg=Craig\naccessed UniFi Network using the web. Source IP: 105.5.138.59' **Phase 2:\nCompleted decoding. name: 'cef-base' class_id: '544' event_name: 'Admin\nAccessed UniFi Network' product: 'UniFi Network' product_version: '9.3.33'\nseverity: '1' vendor: 'Ubiquiti'``` But when I add a child decoder like this\n```cef-basemsg=((?:\\\\\\\\.|[^=])*)(?:\\s+\\w+=|$)\nmsg``` only that field is captured:\n```CEF:0|Ubiquiti|UniFi Network|9.3.33|544|Admin Accessed UniFi\nNetwork|1|UNIFIcategory=System UNIFIsubCategory=Admin UNIFIhost=Office UDM Pro\nUNIFIaccessMethod=web UNIFIadmin=Craig src=105.5.138.59 msg=Craig accessed\nUniFi Network using the web. Source IP: 105.5.138.59 **Phase 1: Completed pre-\ndecoding. full event: 'CEF:0|Ubiquiti|UniFi Network|9.3.33|544|Admin Accessed\nUniFi Network|1|UNIFIcategory=System UNIFIsubCategory=Admin UNIFIhost=Office\nUDM Pro UNIFIaccessMethod=web UNIFIadmin=Craig src=105.5.138.59 msg=Craig\naccessed UniFi Network using the web. Source IP: 105.5.138.59' **Phase 2:\nCompleted decoding. name: 'cef-base' msg: 'Craig accessed UniFi Network using\nthe web. Source IP: 105.5.138.59'``` So the only thing that seems to help is\nto move the regex captures of the parent decoder to a child decoder with the\nsame name as the other decoders. Is this expected and intended behaviour? It\nseems kind of unintuitive...\nhttps://wazuh.slack.com/archives/C0A933R8E/p1770986661732759",
+ "comments": [
+ "DM @luissalaz11 ",
+ "Hi,\n\nWhat you're seeing is expected behavior in Wazuh and is related to how the decoder engine processes parent/child relationships internally.\n\nThe element is not intended to provide additive inheritance of extracted fields. Instead, it acts as a structural classifier. The parent establishes a common entry point, and then only the child that matches is executed for that specific log structure.\n\nThis means:\n\nWhen a child decoder matches, it becomes the effective extraction layer.\n\nFields extracted by the parent are not automatically merged unless the parsing is layered under the same decoder name.\n\nThe decoder name shown in Phase 2 corresponds to the root decoder in the chain, not the last matching child.\n\n in rules will match the root decoder name, not the child decoder name.\n\n`\n\n\n\n CEF:\n\n\n\n\n Ubiquiti-Custom\n ^CEF:(\\d+)\n CEF_Version\n\n\n\n Ubiquiti-Custom\n ^CEF:\\d+\\p(\\w+)\\p\n Device_Vendor\n\n\n\n Ubiquiti-Custom\n ^CEF:\\d+\\p\\w+\\p(\\w+\\s\\w+)\\p\n Device_Product\n\n\n\n Ubiquiti-Custom\n ^CEF:\\d+\\p\\w+\\p\\w+\\s\\w+\\p(\\d+.\\d+.\\d+)\\p\n Device_Version\n\n\n\n Ubiquiti-Custom\n ^CEF:\\d+\\p\\w+\\p\\w+\\s\\w+\\p\\d+.\\d+.\\d+\\p(\\d+)\\p\n Event_Code\n\n\n\n Ubiquiti-Custom\n ^CEF:\\d+\\p\\w+\\p\\w+\\s\\w+\\p\\d+.\\d+.\\d+\\p\\d+\\p(\\.+)\\p\n Action\n\n\n\n Ubiquiti-Custom\n ^CEF:\\d+\\p\\w+\\p\\w+\\s\\w+\\p\\d+.\\d+.\\d+\\p\\d+\\p\\.+\\p(\\d+)\\p\n Severity\n`\n\nRegarding siblings:\nThey are typically used when the same type of log may appear with slightly different structures (e.g., a field is sometimes present and sometimes absent). Since a failed causes the decoder to fail entirely, separating variants into sibling decoders prevents one optional field from breaking the match.\n\nSo in short, yes this behavior is expected and by design. The parent is meant to provide a shared classification layer, and children are intended to handle structural variations rather than act as incremental field extensions.\n\nI understand it can feel unintuitive if you're coming from systems where parsing stages are additive, but within Wazuh's decoder model, this is the intended processing flow.\n\nHope this clarifies things",
+ "Closing this due to inactivity from the user."
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/62589",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "review/moderation",
+ "reviewer/team/community/oajani",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 62588,
+ "title": "llamothe_70672 https://discord.com/channels/1049711339578331186/1049711340316541004/1471847601639915533",
+ "body": "if i am not mistaken, in order for the manager to send active response commands you would need to have wazuh_command.remote_commands=1 in the file /var/ossec/etc/local_internal_options.conf on all agents?\nhttps://documentation.wazuh.com/current/user-manual/capabilities/command-monitoring/configuration.html\nhttps://discord.com/channels/1049711339578331186/1049711340316541004/1471847601639915533",
+ "comments": [
+ "Closing issue due to no user activity."
+ ],
+ "external_community": [
+ "[Linked Discord conversation \u2014 not fetchable without a bot token in that server.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/62588",
+ "labels": [
+ "Discord",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/oajani",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 62583,
+ "title": "patricio981 https://wazuh.slack.com/archives/C0A933R8E/p1770981922151389",
+ "body": "Hi Community. I'm running wazuh v4.14.2 on EKS. I'm getting this error log on\nthe manager pods: ```2026-02-13T09:48:10.453Z WARN [elasticsearch]\nelasticsearch/client.go:408 Cannot index event\npublisher.Event{Content:beat.Event{Timestamp:time.Time{wall:0xc25bdab6569454ba,\next:74672112678282, loc:(*time.Location)(0x42417a0)},\nMeta:{\"pipeline\":\"filebeat-7.10.2-wazuh-alerts-pipeline\"},\nFields:{\"agent\":{\"ephemeral_id\":\"0e68acf2-1cf3-4e90-bc97-4cfe9a10e2cf\",\"hostname\":\"wazuh-\nmanager-master-0\",\"id\":\"777fdd55-08f0-48a3-993a-dba8d4056526\",\"name\":\"wazuh-\nmanager-\nmaster-0\",\"type\":\"filebeat\",\"version\":\"7.10.2\"},\"ecs\":{\"version\":\"1.6.0\"},\"event\":{\"dataset\":\"wazuh.alerts\",\"module\":\"wazuh\"},\"fields\":{\"index_prefix\":\"wazuh-\nalerts-4.x-\"},\"fileset\":{\"name\":\"alerts\"},\"host\":{\"name\":\"wazuh-manager-\nmaster-0\"},\"input\":{\"type\":\"log\"},\"log\":{\"file\":{\"path\":\"/var/ossec/logs/alerts/alerts.json\"},\"offset\":345221136},\"message\":\"{\\\"timestamp\\\":\\\"2026-02-13T09:48:01.367+0000\\\",\\\"rule\\\":{\\\"level\\\":3,\\\"description\\\":\\\"Office\n365: Azure Active Directory\nevents.\\\",\\\"id\\\":\\\"91539\\\",\\\"firedtimes\\\":1,\\\"mail\\\":true,\\\"groups\\\":[\\\"office365\\\",\\\"AzureActiveDirectory\\\"],\\\"hipaa\\\":[\\\"164.312.b\\\"],\\\"pci_dss\\\":[\\\"10.6.2\\\"]},\\\"agent\\\":{\\\"id\\\":\\\"000\\\",\\\"name\\\":\\\"wazuh-\nmanager-master-0\\\"},\\\"manager\\\":{\\\"name\\\":\\\"wazuh-manager-\nmaster-0\\\"},\\\"id\\\":\\\"xxxxxx\\\",\\\"cluster\\\":{\\\"name\\\":\\\"wazuh\\\",\\\"node\\\":\\\"wazuh-\nmanager-\nmaster\\\"},\\\"decoder\\\":{\\\"name\\\":\\\"json\\\"},\\\"data\\\":{\\\"integration\\\":\\\"office365\\\",\\\"office365\\\":{\\\"CreationTime\\\":\\\"2026-02-13T09:44:16\\\",\\\"Id\\\":\\\"xxxxxxxxxx\\\",\\\"Operation\\\":\\\"Update\ndevice.\\\",\\\"OrganizationId\\\":\\\"xxxxx-xxx-\nxxxxxxxx\\\",\\\"RecordType\\\":\\\"8\\\",\\\"ResultStatus\\\":\\\"Success\\\",\\\"UserKey\\\":\\\"Not\nAvailable\\\",\\\"UserType\\\":\\\"4\\\",\\\"Version\\\":\\\"1\\\",\\\"Workload\\\":\\\"AzureActiveDirectory\\\",\\\"ObjectId\\\":\\\"Devicexxxxxxx\\\",\\\"UserId\\\":\\\"ServicePrincipal_xxxxxx-\nxxxxxx\\\",\\\"AzureActiveDirectoryEventType\\\":\\\"1\\\",\\\"ExtendedProperties\\\":[{\\\"Name\\\":\\\"additionalDetails\\\",\\\"Value\\\":\\\"{\\\\\\\\\\\"DeviceId\\\\\\\\\\\":\\\\\\\\\\\"xxxxxxxxx\\\\\\\\\\\",\\\\\\\\\\\"DeviceOSType\\\\\\\\\\\":\\\\\\\\\\\"Windows\\\\\\\\\\\",\\\\\\\\\\\"DeviceTrustType\\\\\\\\\\\":\\\\\\\\\\\"Workplace\\\\\\\\\\\"}\\\"},{\\\"Name\\\":\\\"extendedAuditEventCategory\\\",\\\"Value\\\":\\\"Device\\\"}],\\\"ModifiedProperties\\\":[{\\\"Name\\\":\\\"Included\nUpdated\nProperties\\\",\\\"NewValue\\\":\\\"\\\",\\\"OldValue\\\":\\\"\\\"},{\\\"Name\\\":\\\"TargetId.DeviceId\\\",\\\"NewValue\\\":\\\"57810602-d891-46ef-93d5-xxxxxx\\\",\\\"OldValue\\\":\\\"\\\"},{\\\"Name\\\":\\\"TargetId.DeviceOSType\\\",\\\"NewValue\\\":\\\"Windows\\\",\\\"OldValue\\\":\\\"\\\"},{\\\"Name\\\":\\\"TargetId.DeviceTrustType\\\",\\\"NewValue\\\":\\\"Workplace\\\",\\\"OldValue\\\":\\\"\\\"}],\\\"Actor\\\":[{\\\"ID\\\":\\\"Device\nRegistration\nService\\\",\\\"Type\\\":1},{\\\"ID\\\":\\\"xxxxxx\\\",\\\"Type\\\":2},{\\\"ID\\\":\\\"ServicePrincipal_xxxxxxxx\\\",\\\"Type\\\":2},{\\\"ID\\\":\\\"xxxxxxxx\\\",\\\"Type\\\":2},{\\\"ID\\\":\\\"ServicePrincipal\\\",\\\"Type\\\":2}],\\\"ActorContextId\\\":\\\"xxxxxxx\\\",\\\"InterSystemsId\\\":\\\"xxxxxxxx\\\",\\\"IntraSystemId\\\":\\\"xxxxxxx\\\",\\\"Target\\\":[{\\\"ID\\\":\\\"Device_xxxxxxx\\\",\\\"Type\\\":2},{\\\"ID\\\":\\\"xxxxxxx\\\",\\\"Type\\\":2},{\\\"ID\\\":\\\"Device\\\",\\\"Type\\\":2},{\\\"ID\\\":\\\"xxxxxxx\\\",\\\"Type\\\":1}],\\\"TargetContextId\\\":\\\"xxxxxxxxx\\\",\\\"Subscription\\\":\\\"Audit.AzureActiveDirectory\\\"}},\\\"location\\\":\\\"office365\\\"}\",\"service\":{\"type\":\"wazuh\"}},\nPrivate:file.State{Id:\"native::393481-66309\", PrevId:\"\", Finished:false,\nFileinfo:(*os.fileStat)(0xc00052a820),\nSource:\"/var/ossec/logs/alerts/alerts.json\", Offset:345223409,\nTimestamp:time.Time{wall:0xc25bb85db0401faf, ext:39501543367296,\nloc:(*time.Location)(0x42417a0)}, TTL:-1, Type:\"log\",\nMeta:map[string]string(nil), FileStateOS:file.StateOS{Inode:0x60109,\nDevice:0x10305}, IdentifierName:\"native\"}, TimeSeries:false}, Flags:0x1,\nCache:publisher.EventCache{m:common.MapStr(nil)}} (status=400):\n{\"type\":\"mapper_parsing_exception\",\"reason\":\"failed to parse field\n[data.office365.ModifiedProperties] of type [keyword] in document with id\n'6JhmVpwBm0xhefyXmQ8m'. Preview of field's value: '{OldValue=, NewValue=,\nName=Included Updated\nProperties}'\",\"caused_by\":{\"type\":\"illegal_state_exception\",\"reason\":\"Can't\nget text on a START_OBJECT at 1:995\"}}``` Any help?\nhttps://wazuh.slack.com/archives/C0A933R8E/p1770981922151389",
+ "comments": [
+ "An issue was created for this https://github.com/wazuh/wazuh/issues/34521 ",
+ "LGTM "
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/62583",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/Stuti3097",
+ "Open github issue",
+ "review/quality",
+ "escalated",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 62582,
+ "title": "loris.brunet787 https://wazuh.slack.com/archives/C0A933R8E/p1770981168065719",
+ "body": "Hello Wazuh community, I have been experiencing an issue since the last\nupdate; I am unable to log in, even though I have not made any changes to the\nLDAP files. Could you please assist me?\nhttps://wazuh.slack.com/archives/C0A933R8E/p1770981168065719",
+ "comments": [
+ "@kafleParash there is a response from the user here",
+ "So far is LGTM",
+ "Closing this due to inactivity. Please reopen it if needed"
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/62582",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/Stuti3097",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 62549,
+ "title": "oknivrech https://wazuh.slack.com/archives/C0A933R8E/p1770915750360009",
+ "body": "Is there a ready-made CIS Hardening implementation for Ubuntu2404+ that\nmatches SCAP from Wazuh?\nhttps://wazuh.slack.com/archives/C0A933R8E/p1770915750360009",
+ "comments": [
+ "Answered",
+ "LGTM ",
+ "LGTM"
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/62549",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/Stuti3097",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 62544,
+ "title": "patricio981 https://wazuh.slack.com/archives/C0A933R8E/p1770909442186209",
+ "body": "hi community, is there a guide for changing the dashboard password? I'm\nrunning wazuh 4.14.2 on EKS\nhttps://wazuh.slack.com/archives/C0A933R8E/p1770909442186209",
+ "comments": [
+ "LGTM ",
+ "Marking it as Resolved. Please reopen if needed"
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/62544",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "Resolved",
+ "review/moderation",
+ "reviewer/team/community/Stuti3097",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 62526,
+ "title": "artzylee0 https://wazuh.slack.com/archives/C0A933R8E/p1770890091082409",
+ "body": "Hello Community, im trying to upgrade wazuh from 4.7 to 4.14.2 which Filebeat\nversion is compatible because when i followed the documentation it upgraded\nfilebeat to the latest version which was not compatible so ihad to roll back\nto the version im working with which is 7.10.2 should i just keep this version\n?\nhttps://wazuh.slack.com/archives/C0A933R8E/p1770890091082409",
+ "comments": [
+ "Closing this issue as the query has been answered with no response from the user."
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/62526",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/oajani",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 62514,
+ "title": "Wazuh Debian 13 SCA policy lacking https://www.reddit.com/r/Wazuh/comments/1r2l4q6/wazuh_debian_13_sca_policy_lacking/",
+ "body": "Hello, few notes and a question on SCA policy/improvements.\n\nThe Debian 13 policy is, at time of writing, lacking in regards to accuracy and coverage. This observation should not be taken as a slight but every opportunity should be taken to improve.\n\nIn particular audit checks are either missing or not implemented correctly. CIS references are wrong and deprecated \"-w\" checks are used.\n\nThere are 36 audit items capable of being parsed through SCA checks. I have rewritten all of them to the correct CIS reference together with update rule structure. On a correctly configured machine I can either get the rules to pass or show as \"not applicable\" for most. Worst one is \"privileged commands\" but we know this is likely to be different across systems so I only included the commands from a minimal build.\n\nI have in the past put a few contributions into github for correction and improved accuracy of SCA files both for Windows 2025 and Debian Linux. Not one has been acknowledged or implemented. Closest I got was another Wazuh user taking my observations and producing a patch file but that too was not taken up.\n\nI'd be happy to put my findings up for improvement of the Debian 13 audit ruleset but don't want it to go to waste. Grateful for comment\n\n submitted by /u/ek54ljl\n\n[link] [comments]\nhttps://www.reddit.com/r/Wazuh/comments/1r2l4q6/wazuh_debian_13_sca_policy_lacking/",
+ "comments": [
+ "Good answer"
+ ],
+ "external_community": [],
+ "url": "https://github.com/wazuh/community/issues/62514",
+ "labels": [
+ "Reddit",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/oajani",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 62507,
+ "title": "cherouise https://github.com/wazuh/wazuh-docker/issues/2186",
+ "body": "Hello guys !!\n\nI encountered an issue where ze Wazuh Dashboard entrypoint erase/corrupt the **wazuh.yml** file when it is provided externally (in my case, injected via a hashicorp vault agent, because why not...).\n\nThe problem is that ze script **wazuh_app_config.sh** uses a hardcoded magic number to check if the configuration file exists : \n\n\n```\ngrep -q 1513629884013 $dashboard_config_file\n_config_exists=$?\n\nif [[ $_config_exists -ne 0 ]]; then\ncat << EOF >> $dashboard_config_file\nhosts:\n - 1513629884013:\n url: $wazuh_url\n ...\nEOF\n```\n\n\nHere, ze script grep for ze magic number **1513629884013** instead of checking for ze actual hosts key...\n\nEven if the wazuh.yml file is valid and exists it then appends a duplicated \"hosts:\" block into MY FILE !!!\n\nWhich leads to the following fatal error because yaml does not allow duplicated mapping keys :\n\n```\nFATAL Error: Error getting configuration: duplicated mapping key at line 7, column -140:\n hosts:\n ^\n```\n\nMy workaround at this moment is to protect this wazuh.yml by changing ze permissions : \n\n```\nchown root:root /usr/share/wazuh-dashboard/data/wazuh/config/wazuh.yml\nchmod 444 /usr/share/wazuh-dashboard/data/wazuh/config/wazuh.yml\nexec /entrypoint.sh\n```\n\nthen now if I do this : \n\ndocker exec -it wazuh-dashboard cat /usr/share/wazuh-dashboard/data/wazuh/config/wazuh.yml\nI got this : \n\n```\nhosts:\n - url: https://wazuh.manager\n port: 55000\n username: wazuh-wui\n password: \"ForSur3!xD\"\n run_as: false\n```\n\nIn my opinion, ze entrypoint should at least provide an environment variable to skip this step and maybe grep for the **hosts:** key instead of a magic number to detect an existing configuration file...\n\nIs there an existing workaround or a feature I might have missed that allows for external configuration management without triggering these script-driven modifications?\n\nThank you in advance for your replies !\n\nHave a good day !\n\n\nMicka\u00ebl Ch\u00e9rouise.\n\nhttps://github.com/wazuh/wazuh-docker/issues/2186",
+ "comments": [
+ "Hello\n\nIt's true that we need to find a better way to address the issue of assigning values \u200b\u200bto the parameters in the wazuh.yml file. We've been working on Wazuh version 5.0.0 and have modified the location of these parameters to have better control when assigning these values.\n\nCurrently, we have a wazuh.yml file that we mount in the container at wazuh-docker/(single/multi)-node/config/wazuh_dashboard/, which contains the number you mentioned. As a workaround, you can mount your file with that number appended. Another option would be to use environment variables directly and not mount any file. If you're generating your own images, you could remove the code that performs this variable substitution to avoid errors.\n\nWe are not currently working on new features for Docker deployments of version 4.x, so we will have a solution when version 5.0.0 is ready.",
+ "LGTM \n\nClosing this due to inactivity please reopen it if needed. \n\nThank you!"
+ ],
+ "external_community": [],
+ "url": "https://github.com/wazuh/community/issues/62507",
+ "labels": [
+ "GitHub",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "review/moderation",
+ "reviewer/team/community/Stuti3097",
+ "closed/inactivity",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 62499,
+ "title": "stevencyoung1991 https://wazuh.slack.com/archives/C0A933R8E/p1770827058023699",
+ "body": "Hey folks, I've recently managed to end up with a duplicate index pattern of\nwazuh-archives. I've deleted the duplicate, and made the other my default\nindex pattern. When I enter discovery view, it shows me my wazuh-archives\ndata, however, the front dashboard screen that shows you your last 24 hour\nalerts has a resounding 0 across all of them, due to it defaulting to wazuh-\nalerts, which we don't use. Anyone know how to make it so the front dashboard\nuses the correct index pattern?\nhttps://wazuh.slack.com/archives/C0A933R8E/p1770827058023699",
+ "comments": [
+ "As Kevin Branch correctly points out, the issue may be related to the Custom Index Pattern ID. The Wazuh dashboard expects to find an index pattern with the specific ID `wazuh-archives-*`. When you create it manually without setting this custom ID, it generates a random ID that Wazuh doesn't recognize.\n\nBefore proceeding, please share which Wazuh version you have installed. In older versions of Wazuh, the `wazuh-alerts-*` index pattern was hardcoded in the dashboard home KPI links, so we need to locate the version you have installed.\n\nTo recreate the Index Pattern with a Custom ID, you can follow these steps.s\n\nStep 1: Recreate Index Pattern\n\n1. Go to *Dashboard management* > *Index Patterns*\n2. Delete the existing `wazuh-archives-*` index pattern\n3. Click *Create index pattern*\n4. *Step 1 of 2: Define an index pattern*\n \u2022 Enter index pattern name: `wazuh-archives-*`\n \u2022 Click *Next step*\n5. *Step 2 of 2: Configure settings*\n \u2022 Select Time field: `timestamp`\n \u2022 Click \"*Show advanced settings*\"\n \u2022 Set *Custom index pattern ID*: `wazuh-archives-*`\n \u2022 Click *Create index pattern*\n6. Set it as the default index pattern\n\nStep 2: Configure wazuh.yml\n\nEdit `/usr/share/wazuh-dashboard/data/wazuh/config/wazuh.yml` and add/modify:\n\n`pattern: 'wazuh-archives-*'`\n\nStep 3: Restart the Dashboard\n\n`systemctl restart wazuh-dashboard`\n\nYou can check these steps in [Wazuh Indexer Indices](https://documentation.wazuh.com/current/user-manual/wazuh-indexer/wazuh-indexer-indices.html)\n",
+ "Looks good to me. Good response. \nThe user is back with the information. Can you please look into that when you have a moment. \n\nThank you!",
+ "Hi @fcaffieri \n\nCan you please look into this the user is back ",
+ "The user indicated that their issue has been resolved; i'm closing the issue."
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/62499",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "review/moderation",
+ "reviewer/team/community/Stuti3097",
+ "ambassador",
+ "review/quality",
+ "flag/no_follow-up",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 62497,
+ "title": "provensen https://wazuh.slack.com/archives/C0A933R8E/p1770826428961679",
+ "body": "I just upgraded my Wazuh server to v4.14.3, and now I have lost API permission\nfor my admin user. My admin user has the same permissions as the built in\ndefault admin user. What happened? I am getting this now:\nhttps://wazuh.slack.com/archives/C0A933R8E/p1770826428961679",
+ "comments": [
+ "Hi, could you tell me which version you upgraded from to 4.14.3 and what deployment method do you have (All-in-One or Distributed)?\n\nCould you also tell me which section you're trying to access in the Wazuh dashboard, or what query you're trying to make through the API?\n\nIf you could share the upgrade guide you followed so I can check for a possible error, that would be very helpful.",
+ "Everything seems to indicate a permissions issue with the user logged into the Wazuh dashboard.\n\nI ran a quick test: I deployed a Docker single-node environment with version 4.14.1\n\nand created a custom user with admin privileges.\n\n\n\n\n\nThen I updated to 4.14.3 and was able to connect without problems with the user I created.\n\n\n\n\n\nThis is the deployment process:\n\n```console\ncbordon@cbordon-HP-255-15-6-inch-G10-Notebook-PC:/tmp/wazuh-docker/single-node$ docker compose up -d\n[+] up 59/59\n \u2714 Image wazuh/wazuh-indexer:4.14.1 Pulled 292.0s\n \u2714 Image wazuh/wazuh-dashboard:4.14.1 Pulled 191.9s\n \u2714 Image wazuh/wazuh-manager:4.14.1 Pulled 345.1s\n \u2714 Volume single-node_filebeat_var Created 0.0s\n \u2714 Volume single-node_wazuh_etc Created 0.0s\n \u2714 Volume single-node_wazuh_logs Created 0.0s\n \u2714 Volume single-node_wazuh_var_multigroups Created 0.0s\n \u2714 Volume single-node_filebeat_etc Created 0.0s\n \u2714 Volume single-node_wazuh_api_configuration Created 0.0s\n \u2714 Volume single-node_wazuh-dashboard-config Created 0.0s\n \u2714 Volume single-node_wazuh_queue Created 0.0s\n \u2714 Volume single-node_wazuh-indexer-data Created 0.0s\n \u2714 Volume single-node_wazuh-dashboard-custom Created 0.0s\n \u2714 Volume single-node_wazuh_wodles Created 0.0s\n \u2714 Volume single-node_wazuh_integrations Created 0.0s\n \u2714 Volume single-node_wazuh_active_response Created 0.0s\n \u2714 Volume single-node_wazuh_agentless Created 0.0s\n \u2714 Container single-node-wazuh.manager-1 Created 0.9s\n \u2714 Container single-node-wazuh.indexer-1 Created 0.9s\n \u2714 Container single-node-wazuh.dashboard-1 Created 0.0s\ncbordon@cbordon-HP-255-15-6-inch-G10-Notebook-PC:/tmp/wazuh-docker/single-node$ docker composer down\ndocker: unknown command: docker composer\n\nRun 'docker --help' for more information\ncbordon@cbordon-HP-255-15-6-inch-G10-Notebook-PC:/tmp/wazuh-docker/single-node$ docker compose down\n[+] down 4/4\n \u2714 Container single-node-wazuh.dashboard-1 Removed 10.3s\n \u2714 Container single-node-wazuh.indexer-1 Removed 0.5s\n \u2714 Container single-node-wazuh.manager-1 Removed 3.8s\n \u2714 Network single-node_default Removed 0.1s\ncbordon@cbordon-HP-255-15-6-inch-G10-Notebook-PC:/tmp/wazuh-docker/single-node$ vim docker-compose.yml \ncbordon@cbordon-HP-255-15-6-inch-G10-Notebook-PC:/tmp/wazuh-docker/single-node$ docker compose up -d\n[+] up 46/46\n \u2714 Image wazuh/wazuh-manager:4.14.3 Pulled 334.7s\n \u2714 Image wazuh/wazuh-indexer:4.14.3 Pulled 327.0s\n \u2714 Image wazuh/wazuh-dashboard:4.14.3 Pulled 208.2s\n \u2714 Network single-node_default Created 0.0s\n \u2714 Container single-node-wazuh.manager-1 Created 0.9s\n \u2714 Container single-node-wazuh.indexer-1 Created 0.9s\n \u2714 Container single-node-wazuh.dashboard-1 Created\n```\n\n\nDo you have access with the user `admin` to check permissions?\n\nIf you don't have access, you can reset the password as follows: https://documentation.wazuh.com/current/deployment-options/docker/changing-default-password.html#wazuh-indexer-user\n\nOnce you log in as admin, check the permissions of your custom user in Indexer Management -> Security -> Internal Users",
+ "Wazuh docker, as a deployment method, requires internet access to Docker Hub. I understand that this is permitted on your host since you were able to download the images without issues.\n\nFurthermore, Wazuh Manager needs internet access because it requires updating its CVE databases and rulesets.. If you need to perform an offline deployment of Wazuh, we have a guide that may be helpful: https://documentation.wazuh.com/current/deployment-options/offline-installation/index.html",
+ "What role is your user assigned?\n\n\n\nAre you assigned the `admin` role to your `wazuh_admin` user, or do you have a custom role?\n\nHere are all the default roles and their respective policies: https://documentation.wazuh.com/current/user-manual/api/rbac/reference.html#api-rbac-reference-default-roles",
+ "We've made some changes related to role mapping. You can see the details in this issue: https://github.com/wazuh/wazuh-docker/issues/2156. This change affects all installation methods, not just Docker.\n\nThis change was made to improve Wazuh's security and prevent potential exploits by users with limited permissions.\n\nYou can see all the changes made in version 4.14.3 here: https://documentation.wazuh.com/current/release-notes/release-4-14-3.html",
+ "Great efforts and good response. LGTM me so far. The user is back with more queries. Can you please look into this when you have a moment? \n\nAdditionally, marking this community as resolved because the initial query is resolved by @c-bordon. ",
+ "Closing this due to no activity from the user. Please reopen it if needed"
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/62497",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "Resolved",
+ "review/moderation",
+ "reviewer/team/community/Stuti3097",
+ "closed/inactivity",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 62475,
+ "title": "indranil.kamulkar https://wazuh.slack.com/archives/C07CCCCGHHP/p1770785054992279",
+ "body": "I have a firewall (FortiGate) integrated with WAZUH ... (This is working superbly), I have installed an Ubuntu machine and the Firewall logs are forwarded to this Ubuntu machine, now I want to add another firewall on this Ubuntu machine for the logs, how do I go about this ???\nhttps://wazuh.slack.com/archives/C07CCCCGHHP/p1770785054992279",
+ "comments": [
+ "Hi @IsmailChemmala,\n\nYour response looks good to me. Please assist further if they need further assistance on this. Thanks!",
+ "Good answer",
+ "Hi @IsmailChemmala,\n\nClosing this issue due to inactivity. Please reopen if they need further assistance on this. Thanks!"
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/62475",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/Hasitha9796",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 62470,
+ "title": "d4rkm4gi https://discord.com/channels/1049711339578331186/1049711340316541004/1470880451198713877",
+ "body": "Hi Team.. i need help on my wazuh auditlog config that will log the user activities on wazuh-dashboard..\n\nI have working \"security-auditlog-*\" yet,, i didnt see any wazuh-dashboard user activities..\nhttps://discord.com/channels/1049711339578331186/1049711340316541004/1470880451198713877",
+ "comments": [
+ "About to end working day",
+ "LGTM",
+ "LGTM, it was resolved, so I closed it."
+ ],
+ "external_community": [
+ "[Linked Discord conversation \u2014 not fetchable without a bot token in that server.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/62470",
+ "labels": [
+ "Discord",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/Hasitha9796",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 62460,
+ "title": "joao.victor080807 https://wazuh.slack.com/archives/C0A933R8E/p1770736827140529",
+ "body": "Hello Everyone. I would like an advice related to the usage of an old server\nin my current Wazuh environment. In summary, was requested me to use an old\nserver (Xeon 3070, 4GB RAM and 14TB) as a cluster to my current Wazuh All-In-\nOn server (16CPUs, 32GB and 1.4TB SSD). I thought to use this old server as a\ncold indexer cluster to search for logs more than 90days only. The goal of\nusing this old server is to index firewall logs \"Connection Opened\" and\n\"Connection Closed\" events that generates at least 20 million per day, and we\ncan't increase the storage of the main cluster anymore (SSD), so that's why we\nneed to use the old server that have 14TB to store this kind of logs. What's\nthe recommendation? Use a separated cluster ou use some kind of graylog to\nonly store these logs? Any ideas?\nhttps://wazuh.slack.com/archives/C0A933R8E/p1770736827140529",
+ "comments": [
+ "Response time: 45 min\n\nLGTM."
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/62460",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/oajani",
+ "ambassador",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 62455,
+ "title": "victor_gyt https://discord.com/channels/1049711339578331186/1049711340316541004/1470769267640369324",
+ "body": "Hello! \nI have a request, is Debian 13 well supported for Wazuh? I got an alert during the configuration that it is not recommended. What are the risks?\nhttps://discord.com/channels/1049711339578331186/1049711340316541004/1470769267640369324",
+ "comments": [
+ "\n\nhttps://discord.com/channels/1049711339578331186/1470769267640369324/1470776416764297306\n\nAlready answered by a community user ",
+ "No further action is required here. The user seems satisfied with the responses.\nClosing this"
+ ],
+ "external_community": [
+ "[Linked Discord conversation \u2014 not fetchable without a bot token in that server.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/62455",
+ "labels": [
+ "Discord",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/oajani",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 62448,
+ "title": "How does Wazuh gets CVE (CPE?) ",
+ "body": "Hi Everyone, \r\nwhere from does Wazuh get those CVE? Does it consider small updates and \r\npatches? What about CPE?\r\n\r\n-- \r\nYou received this message because you are subscribed to the Google Groups \"Wazuh | Mailing List\" group.\r\nTo unsubscribe from this group and stop receiving emails from it, send an email to wazuh+unsubscribe@googlegroups.com.\r\nTo view this discussion visit https://groups.google.com/d/msgid/wazuh/f974685c-a0d3-4dc5-a05c-4b094700364bn%40googlegroups.com.\r\n",
+ "comments": [
+ "I am closing this due to inactivity from the user."
+ ],
+ "external_community": [
+ "window.WIZ_global_data = {\"AfY8Hf\":false,\"DpimGf\":false,\"EP1ykd\":[\"/_/*\"],\"FdrFJe\":\"8434532369397567047\",\"HiPsbb\":1,\"Im6cmf\":\"/_/GroupsFrontendUi\",\"LVIXXb\":1,\"LoQv7e\":false,\"MT7f9b\":[],\"MUE6Ne\":\"groups-frontend\",\"PLnRge\":\"https://docs.google.com/picker\",\"QrtxK\":\"\",\"S06Grb\":\"\",\"S6lZl\":112976253,\"TSDtV\":\"%.@.[[null,[[45447917,null,true,null,null,null,\\\"OeRc3d\\\"],[45447918,null,true,null,null,null,\\\"paZwJ\\\"],[45749384,null,true,null,null,null,\\\"ervCtb\\\"],[45709804,null,false,null,null,null,\\\"PoJR0\\\"],[45697011,null,true,null,null,null,\\\"xNsvyb\\\"],[45734189,null,true,null,null,null,\\\"XGnjIb\\\"],[45448406,null,false,null,null,null,\\\"jKfwq\\\"],[45780063,null,false,null,null,null,\\\"j3OYnd\\\"],[45447921,null,false,null,null,null,\\\"qAj4w\\\"],[45655177,null,true,null,null,null,\\\"VvKUq\\\"],[45447945,null,true,null,null,null,\\\"ckfnge\\\"],[45459555,null,false,null,null,null,\\\"Imeoqb\\\"],[45447936,null,false,null,null,null,\\\"MohPG\\\"],[45532875,null,true,null,null,null,\\\"udsJQe\\\"],[45722772,null,true,null,null,null,\\\"MVCE3b\\\"],[45646796,null,false,null,null,null,\\\"Q877Ab\\\"],[45753484,null,true,null,null,null,\\\"G9u5lb\\\"],[45699412,null,true,null,null,null,\\\"xLy3Ce\\\"],[45447950,null,true,null,null,null,\\\"lU0ald\\\"],[45447953,null,true,null,null,null,\\\"jbTsAe\\\"],[45447919,null,false,null,null,null,\\\"KMEQCe\\\"],[45699332,30,null,null,null,null,\\\"grENN\\\"],[45447930,null,null,null,\\\"signed_out_users\\\",null,\\\"JTFNhb\\\"],[45447928,null,true,null,null,null,\\\"Wh7on\\\"],[45447938,null,true,null,null,null,\\\"uVDGld\\\"],[45709766,null,true,null,null,null,\\\"sSVuJ\\\"],[45776470,null,true,null,null,null,\\\"A9MFwf\\\"],[45447926,null,true,null,null,null,\\\"IUIyxe\\\"],[45793915,null,true,null,null,null,\\\"Vwrzsf\\\"],[45721043,null,true,null,null,null,\\\"FcV2Ie\\\"],[45447924,null,true,null,null,null,\\\"hMPU3c\\\"],[45447934,null,null,null,\\\"https://forms.gle/DuQUYavHhwfEo4sp9\\\",null,\\\"gvGLK\\\"],[45447932,null,false,null,null,null,\\\"jlJZI\\\"]],\\\"CAMSOx0+/7riEsDRsgYOrrekEBYEFtqsNuYD3f+wBAq20g0Kuv0FCqMuFtjVAgqrrQAOppMFCpO2DQ6Jng4K\\\"]]]\",\"Tb2qJf\":4,\"UUFaWc\":\"%.@.null,1000,2]\",\"Vvafkd\":false,\"Yllh3e\":\"%.@.1783855376231855,122393272,1644206838]\",\"YlwcZe\":\"%.@.3,[1],[3600],2,[15,4,13,14,12,2]]\",\"ZZZ7Uc\":\"\",\"b5W2zf\":\"default_GroupsFrontendUi\",\"cfb2h\":\"boq_groupsfrontendserver_20260706.01_p0\",\"eNnkwf\":\"1783855375\",\"eptZe\":\"/_/GroupsFrontendUi/\",\"fPDxwd\":[97493638,97493660,105739272],\"gGcLoe\":false,\"gSs3jc\":1,\"hpRnh\":1,\"hsFLT\":\"%.@.null,10,3]\",\"iCzhFc\":false,\"nQyAE\":{\"G9u5lb\":\"true\",\"lU0ald\":\"true\",\"grENN\":\"30\",\"uVDGld\":\"true\",\"Vwrzsf\":\"true\",\"XGnjIb\":\"true\",\"FcV2Ie\":\"true\",\"j3OYnd\":\"false\",\"qAj4w\":\"false\",\"MohPG\":\"false\",\"Q877Ab\":\"false\",\"xLy3Ce\":\"true\",\"KMEQCe\":\"false\",\"JTFNhb\":\"signed_out_users\",\"IUIyxe\":\"true\",\"OeRc3d\":\"true\",\"paZwJ\":\"true\",\"PoJR0\":\"false\",\"xNsvyb\":\"true\",\"ckfnge\":\"true\",\"udsJQe\":\"true\",\"jbTsAe\":\"true\",\"Wh7on\":\"true\",\"sSVuJ\":\"true\",\"gvGLK\":\"https://forms.gle/DuQUYavHhwfEo4sp9\",\"jlJZI\":\"false\",\"ervCtb\":\"true\",\"A9MFwf\":\"true\",\"jKfwq\":\"false\"},\"p9hQne\":\"https://www.gstatic.com/_/boq-groups/_/r/\",\"qwAQke\":\"GroupsFrontendUi\",\"qymVe\":\"kl9PXz6z1CI4Eq-ch93h5hVd3nc\",\"rtQCxc\":-330,\"u4g7r\":\"%.@.null,1,3]\",\"w2btAe\":\"%.@.null,null,\\\"\\\",false,null,null,null,false]\",\"xn5OId\":false,\"xnI9P\":false,\"xwAfE\":true,\"y2FhP\":\"prod\",\"yFnxrf\":1884,\"zChJod\":\"%.@.]\"}; window[\"_F_toggles_default_GroupsFrontendUi\"] = [0x30081086, 0x30b0b301, 0x100c5ad, ]; (function(){'use strict';var a=window,d=a.performance,l=k();a.cc_latency_start_time=d&&d.now?0:d&&d.timing&&d.timing.navigationStart?d.timing.navigationStart:l;function k(){return d&&d.now?d.now():(new Date).getTime()}function n(e){if(d&&d.now&&d.mark){var g=d.mark(e);if(g)return g.startTime;if(d.getEntriesByName&&(e=d.getEntriesByName(e).pop()))return e.startTime}return k()}a.onaft=function(){n(\"aft\")};a._isLazyImage=function(e){return e.hasAttribute(\"data-src\")||e.hasAttribute(\"data-ils\")||e.getAttribute(\"loading\")===\"lazy\"}; a.l=function(e){function g(b){var c={};c[b]=k();a.cc_latency.push(c)}function m(b){var c=n(\"iml\");b.setAttribute(\"data-iml\",c);ret"
+ ],
+ "url": "https://github.com/wazuh/community/issues/62448",
+ "labels": [
+ "Google Groups",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/oajani",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 62435,
+ "title": "@cctvsystem3481 https://www.youtube.com/watch?v=YWCpXdqj1wU&lc=UgyP6Gk5HhqyGrGaPkt4AaABAg",
+ "body": "I want to setup same thing for my windows agent that will be great, it worked perfect for me, as I am using this for couple of months, now wanted for windows client machines, if you suggest link that be perfect.Thanks\n\nhttps://www.youtube.com/watch?v=YWCpXdqj1wU&lc=UgyP6Gk5HhqyGrGaPkt4AaABAg\n",
+ "comments": [
+ "answer: https://www.youtube.com/watch?v=YWCpXdqj1wU&lc=UgyP6Gk5HhqyGrGaPkt4AaABAg.ASiE3Hj-zFsAT22Df5z8vK",
+ "LGTM",
+ "Closed due to inactivity. Please reopen it if the user replies "
+ ],
+ "external_community": [],
+ "url": "https://github.com/wazuh/community/issues/62435",
+ "labels": [
+ "YouTube",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/Stuti3097",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 62423,
+ "title": "driftwood8891 https://discord.com/channels/1049711339578331186/1049711340316541004/1470524291622895730",
+ "body": "So I am brand new to Wazuh and I am currently testing out Wazuh for a possible SEIM at the place I work at. As far as Wazuh's Regulatory Compliance modules go, can SCAP scanning be intergrated in with Wazuh? I have read about intergrating OpenSCAP with Wazuh but then had also read that the OpenSCAP module had been removed and essentially replaced with SCA scans. \n\nWhat I am asking is if a company needed a SCAP compliant vulnerability scanner, could they use Wazuh or would they have to use something else such as Nessus ACAS?\nhttps://discord.com/channels/1049711339578331186/1049711340316541004/1470524291622895730",
+ "comments": [
+ "LGTM",
+ "Closing this due to inactivity. Please reopen it if the user replies "
+ ],
+ "external_community": [
+ "[Linked Discord conversation \u2014 not fetchable without a bot token in that server.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/62423",
+ "labels": [
+ "Discord",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "review/moderation",
+ "reviewer/team/community/Stuti3097",
+ "closed/inactivity",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 62400,
+ "title": "ak3238539 https://wazuh.slack.com/archives/C0A933R8E/p1770640531574989",
+ "body": "Hi <#C0A933R8E> I recently deployed wazuh manual installation in *SAME HOST*\nlike wazuh indexer, wazuh server & so on, I am trying to reset the default\nwazuh indexer and wazuh server api users passwords using *\"wazuh-passwords-\ntool.sh\"* I need to know whats the exact and correct command to *reset the all\nusers passwords* and also i need to know in which components i need to update\nthe passwords - if anyone know realted to this *PLS provide a solution to\nthis* :smiling_face_with_tear:\nhttps://wazuh.slack.com/archives/C0A933R8E/p1770640531574989",
+ "comments": [],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/62400",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/sakib789",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 62349,
+ "title": "maxschlagern https://discord.com/channels/1049711339578331186/1049711340316541004/1469404343492149311",
+ "body": "Will the Wazuh Agent run on a ARM based Windows 11 laptop? - perhaps it works fine via emulation? - does anyone know? (We have a few Surface Laptops with snapdragon CPUs)\nhttps://discord.com/channels/1049711339578331186/1049711340316541004/1469404343492149311",
+ "comments": [
+ "At this time, Wazuh does not officially support running the Windows agent on ARM64-based Windows systems (such as Surface laptops with Snapdragon CPUs).\n \nDocumentation to support this\nThe official Windows agent installation guide only provides MSI packages intended for x86/x64 Windows architectures. There is no Windows ARM64 agent package listed or documented:\n \no\tWazuh documentation \u2192 Installation guide \u2192 Wazuh agent for Windows\nWhile Wazuh documentation mentions ARM/ARM64 support in other contexts, this applies to Linux builds or central components (manager, indexer, dashboard), not the Windows agent:\n \nhttps://documentation.wazuh.com/current/installation-guide/wazuh-agent/wazuh-agent-package-windows.html?utm_source=chatgpt.com#deploying-wazuh-agents-on-windows-endpoints\n \no\tWazuh documentation \u2192 Development / Packaging \u2192 Agent package generation\n \nhttps://documentation.wazuh.com/current/development/packaging/generate-agent-package.html?utm_source=chatgpt.com#creating-the-agent-package\n \no\tWazuh release notes discussing ARM support\n \nhttps://wazuh.com/blog/introducing-wazuh-4-12-0/\n \nWhat about Windows 11 ARM emulation?\n\u2022\tWindows 11 on ARM can emulate x64 applications, so the Wazuh Windows agent may install and run under emulation.\n\u2022\tHowever, this setup is not tested, documented, or supported by Wazuh, and issues related to stability, performance, upgrades, or missing functionality would be out of scope for official support.\n \nRecommendation\n\u2022\tFor production environments, use the Wazuh agent on native x86_64 Windows systems.\n\u2022\tARM-based Windows devices can be evaluated only in lab or PoC scenarios, with the understanding that behavior may vary.\n \nIf official Windows ARM64 support is added in the future, it would be explicitly documented in the installation guides and release notes.\n",
+ "Hi @luissalaz11,\n\nYour response looks good to me. ",
+ "Hi @luissalaz11,\n\nClosing this issue due to inactivity. Please reopen if they need further assistance on this. Thanks!"
+ ],
+ "external_community": [
+ "[Linked Discord conversation \u2014 not fetchable without a bot token in that server.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/62349",
+ "labels": [
+ "Discord",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/Hasitha9796",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 62330,
+ "title": "patricio981 https://wazuh.slack.com/archives/C0A933R8E/p1770380029801929",
+ "body": "Hi community. I'm running wazuh v4.14.2 on EKS. I'm trying to mount my custom\nsentinelone decoders and rules. First I tried to mount the decoders I'm doing\nit like this: `kustomization.yaml`: ```configMapGenerator: \\- name: indexer-\nconf files: \\- indexer_stack/wazuh-indexer/indexer_conf/opensearch.yml \\-\nindexer_stack/wazuh-indexer/indexer_conf/internal_users.yml \\- name: wazuh-\nconf files: \\- wazuh_managers/wazuh_conf/master.conf \\-\nwazuh_managers/wazuh_conf/worker.conf \\- name: dashboard-conf files: \\-\nindexer_stack/wazuh-dashboard/dashboard_conf/opensearch_dashboards.yml \\-\nname: wazuh-filebeat-conf ##############My config files: \\-\nwazuh_managers/wazuh_conf/filebeat.yml # Custom Filebeat config # Add any\nother custom Filebeat files here \\- name: custom-rules-and-decoders files: \\-\nwazuh_managers/wazuh_conf/sentinelone_decoders.xml \\-\nwazuh_managers/wazuh_conf/sentinelone_rules.xml \\-\nwazuh_managers/wazuh_conf/local_rules.xml # Add any other custom Filebeat\nfiles here \\- name: integrations-cm files: \\- wazuh_managers/wazuh-\nintegrations/n8n-cm \\- wazuh_managers/wazuh-integrations/n8n-cm.py ``` My\n`sentinelone_decoders.xml` looks like this: ``` sentinel -\nCEF:\\d\\|SentinelOne\\|\nsentinelone\\|SentinelOne\\|\\w+\\|\\S+\\|\\d+\\|(.*)\n-log_message1\nsentinelone\n\\|SentinelOne\\|\\w+\\|\\S+\\|\\d+\\|(.*)\\|\\d+\nlog_message2\nsentinelone\\|SentinelOne\\|\\w+\\|\\S+\\|\\d+\\|.* machine\n(.*)\\|\\d+endpointsentinelone\nosName=(\\w*\\s*\\w*\\s*\\w*\\s*)operating_system\nsentinelone\nfileHash=(\\S+)\\sfile_hashsentinelone\nfilePath=(\\S+)\\sfile_pathsentinelone\nactivityID=(\\S+)activity_idsentinelonesuser=(\\S+)\nsuser\nsentinelonesiteId=(\\d+)\nsite_id\nsentinelonesiteName=(\\S+)\nsite_name\nsentineloneaccountId=(\\d+)\naccount_id\nsentineloneaccountName=(\\w*\\s\\w*\\s)\naccount_name\nsentinelonegroupId=(\\d+)\ngroup_id\nsentinelonegroupName=(\\S+\\s\\S+)\ngroup_name\nsentineloneagentId=(\\d+)\nagent_id\nsentineloneuserId=(\\d+)\nuser_id\nsentinelonethreadId=(\\d+)\nthread_id\nsentineloneip_address=(\\d+\\\\.\\d+\\\\.\\d+\\\\.\\d+)\nip_address\nsentineloneconfidence_level=(\\w+)\nconfidence_level\nsentinelonestoryline=(\\S+)\nstoryline\ntesthost``` And I'm mounting them in `wazuh-\nmanager-master/worker` like this: ``` volumes: \\- name: custom-rules-and-\ndecoders-cm configMap: name: custom-rules-and-decoders volumeMounts: \\- name:\ncustom-rules-and-decoders-cm mountPath:\n/var/ossec/etc/decoders/sentinelone_decoders.xml subPath:\nsentinelone_decoders.xml readOnly: true ``` Thing is, if I do not mount it,\nthe `/var/ossec/etc/decoders` folder looks like this: ```$ kubectl exec -it\nwazuh-manager-master-0 -n wazuh -- /bin/bash bash-5.2# ls -l\nvar/ossec/etc/decoders/ total 4 -rw-rw----. 1 wazuh wazuh 815 Jan 8 19:06\nlocal_decoder.xml``` But when I mount my customs sentinelone decoders, it\nlooks like this: ```$ kubectl exec -it wazuh-manager-master-0 -n wazuh --\n/bin/bash bash-5.2# ls -l var/ossec/etc/decoders/ total 4 -rw-r--r--. 1 root\n101 2983 Feb 5 14:06 sentinelone_decoders.xml``` Somehow I'm overwriting all\nthe folder. And the same old error appears: ```2026/02/05 14:06:42 wazuh-\nanalysisd: ERROR: (1103): Could not open file 'etc/shared/ar.conf' due to\n[(2)-(No such file or directory)]. 2026/02/05 14:06:42 wazuh-analysisd:\nCRITICAL: (1202): Configuration error at 'etc/ossec.conf'.``` (edited) [11:16\nAM] But for example, I'm using the same config for the\n`/var/ossec/integrations/n8n-cm.py` and it does not overwrite: ```bash-5.2# ls\n-l var/ossec/integrations/ total 92 -rwxr-x---. 1 root wazuh 1045 Jan 8 19:07\nmaltiverse -rwxr-x---. 1 root wazuh 20926 Jan 8 19:06 maltiverse.py -rw-r--\nr--. 1 root wazuh 1046 Feb 5 14:06 n8n-cm -rw-r--r--. 1 root wazuh 7198 Feb 5\n14:06 n8n-cm.py -rwxr-x---. 1 root wazuh 1045 Jan 8 19:07 pagerduty\n-rwxr-x---. 1 root wazuh 6449 Jan 8 19:06 pagerduty.py -rwxr-x---. 1 root\nwazuh 1045 Jan 8 19:07 shuffle -rwxr-x---. 1 root wazuh 7249 Jan 8 19:06\nshuffle.py -rwxr-x---. 1 root wazuh 1045 Jan 8 19:07 slack -rwxr-x---. 1 root\nwazuh 6835 Jan 8 19:06 slack.py -rwxr-x---. 1 root wazuh 1045 Jan 8 19:07\nvirustotal -rwxr-x---. 1 root wazuh 10691 Jan 8 19:06 virustotal.py``` The\nconfig is: `kustomization.yaml`: ```configMapGenerator: \\- name: indexer-conf\nfiles: \\- indexer_stack/wazuh-indexer/indexer_conf/opensearch.yml \\-\nindexer_stack/wazuh-indexer/indexer_conf/internal_users.yml \\- name: wazuh-\nconf files: \\- wazuh_managers/wazuh_conf/master.conf \\-\nwazuh_managers/wazuh_conf/worker.conf \\- name: dashboard-conf files: \\-\nindexer_stack/wazuh-dashboard/dashboard_conf/opensearch_dashboards.yml \\-\nname: wazuh-filebeat-conf ##############My config files: \\-\nwazuh_managers/wazuh_conf/filebeat.yml # Custom Filebeat config # Add any\nother custom Filebeat files here \\- name: custom-rules-and-decoders files: \\-\nwazuh_managers/wazuh_conf/sentinelone_decoders.xml \\-\nwazuh_managers/wazuh_conf/sentinelone_rules.xml \\-\nwazuh_managers/wazuh_conf/local_rules.xml # Add any other custom Filebeat\nfiles here \\- name: integrations-cm files: \\- wazuh_managers/wazuh-\nintegrations/n8n-cm \\- wazuh_managers/wazuh-integrations/n8n-cm.py ``` And in\nthe sts: ``` volumes: \\- name: integrations-cm-volume configMap: name:\nintegrations-cm volumeMounts: \\- name: integrations-cm-volume mountPath:\n/wazuh-config-mount/integrations/n8n-cm.py subPath: n8n-cm.py \\- name:\nintegrations-cm-volume mountPath: /wazuh-config-mount/integrations/n8n-cm\nsubPath: n8n-cm ``` [11:17 AM] I ran `cat\n/var/ossec/etc/decoders/sentinelone_decoders.xml` inside the pod and the\ncontent is correct, the same with `cat /var/ossec/integrations/n8n-cm.py`\n*Patricio Roig* [11:23 AM] The same happens with the `filebeat.yml` when I\nmount my custom filebeat using the same tactic, the directory is being\noverwritten but It does not break anything, the wazuh-template.json is still\nthere somehow: ```bash-5.2# ls -l etc/filebeat/ total 88 -rw-r--r--. 1 root\n101 715 Feb 5 14:06 filebeat.yml -rw-------. 1 root root 84275 Jan 1 1970\nwazuh-template.json``` But by default it has more archives, looks like this by\ndefault: ```$ kubectl exec -it wazuh-manager-master-0 -n wazuh -- /bin/bash\nbash-5.2# ls -l etc/filebeat/ total 476 -rw-r--r--. 1 root root 297349 Oct 17\n12:04 fields.yml -rw-r--r--. 1 root root 91838 Oct 17 12:04\nfilebeat.reference.yml -rw-r--r--. 1 root root 718 Feb 6 12:08 filebeat.yml\ndrwxr-sr-x. 2 root root 4096 Feb 5 11:16 modules.d -rw-------. 1 root root\n84275 Jan 1 1970 wazuh-template.json``` I don't understand, any help?\nhttps://wazuh.slack.com/archives/C0A933R8E/p1770380029801929",
+ "comments": [
+ "# Update\n\nThe user was asked some questions. Meanwhile, I'm replicating their configuration.",
+ "# Update\n\nMy disk filled up and I couldn't log into Ubuntu; it took me a while to delete files, snaps, and logs. I'll now respond to the user.",
+ "Good response. Great efforts ",
+ "# Update\n\nThe user was helped to generate the decoders necessary for what he required.",
+ "Marking as Resolved. "
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/62330",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/Stuti3097",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 62325,
+ "title": "Does Wazuh support vulnerability detection for CentOS Stream https://www.reddit.com/r/Wazuh/comments/1qxe8e8/does_wazuh_support_vulnerability_detection_for/",
+ "body": "Does Wazuh 4.14.2 correctly detect vulnerabilities on all CentOS Stream distributions?\n\nThere are plenty of other vulnerability detection tools that work fine for CentOS, but fail on CentOS Stream because of the different package naming convention on the Stream distributions.\n\nThe result of this failure is the false reporting of almost ALL packages as vulnerable.\n\nThanks in advance.\n\n submitted by /u/Few-Ferret1767\n\n[link] [comments]\nhttps://www.reddit.com/r/Wazuh/comments/1qxe8e8/does_wazuh_support_vulnerability_detection_for/",
+ "comments": [
+ "Hi @wazuh/community , why has been my comment removed?\n\n\n\n",
+ "Hello @rauldpm This has been checked and approved. The issue is with Reddit. automated filters.",
+ "Great effort. Good response. ",
+ "Closing this due to inactivity from the user side. Please reopen it if the user responds."
+ ],
+ "external_community": [],
+ "url": "https://github.com/wazuh/community/issues/62325",
+ "labels": [
+ "Reddit",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/Stuti3097",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 62322,
+ "title": "m.bhat https://wazuh.slack.com/archives/C0A933R8E/p1770373465687999",
+ "body": "Hi <@U01HXEV6SL8> need to create a detection rule that triggers an alert when\nthere are 10 consecutive failed SSH login attempts on a device followed by a\nsuccessful login on the 11th attempt from the same source IP address. kindly\nhelp\nhttps://wazuh.slack.com/archives/C0A933R8E/p1770373465687999",
+ "comments": [
+ "Still working on this.",
+ "The user has failed to respond. I will close this issue and monitor the thread."
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/62322",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/oajani",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 62308,
+ "title": "shobhit.goel https://wazuh.slack.com/archives/C0A933R8E/p1770345081877019",
+ "body": "Hello <@U01HXEV6SL8> In Wazuh, we use the vulnerability detection module to\nfind vulnerabilities on all our servers. When we export the report, it shows\n4\u20135 lakh vulnerabilities, but many of them are already resolved. Is there any\nway to add filter for only those which are relevant and export them only?\nhttps://wazuh.slack.com/archives/C0A933R8E/p1770345081877019",
+ "comments": [
+ "Great efforts and Good response ",
+ "Good answer"
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/62308",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/Stuti3097",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 62286,
+ "title": "muhiko68 https://wazuh.slack.com/archives/C0A933R8E/p1770294637545329",
+ "body": "Hello <@U01HXEV6SL8> , The vulnerability are not getting fetched --> I edited\nthe proxy. That shouldnt be the problem. Not a single error from that side.\nThe only thing i get is following: `2026/02/05 13:23:55 indexer-connector:\nWARNING: IndexerConnector initialization failed for index 'wazuh-states-\nvulnerabilities-wazuh', retrying until the connection is successful.`\n2026/02/05 13:23:55 indexer-connector: WARNING: IndexerConnector\ninitialization failed for index 'wazuh-states-inventory-processes-wazuh',\nretrying until the connection is successful. 2026/02/05 13:23:55 indexer-\nconnector: WARNING: IndexerConnector initialization failed for index 'wazuh-\nstates-inventory-ports-wazuh', retrying until the connection is successful.\n2026/02/05 13:23:55 indexer-connector: WARNING: IndexerConnector\ninitialization failed for index 'wazuh-states-inventory-hotfixes-wazuh',\nretrying until the connection is successful. 2026/02/05 13:23:55 indexer-\nconnector: WARNING: IndexerConnector initialization failed for index 'wazuh-\nstates-inventory-hardware-wazuh', retrying until the connection is successful.\n2026/02/05 13:23:56 indexer-connector: WARNING: IndexerConnector\ninitialization failed for index 'wazuh-states-inventory-protocols-wazuh',\nretrying until the connection is successful. 2026/02/05 13:23:56 indexer-\nconnector: WARNING: IndexerConnector initialization failed for index 'wazuh-\nstates-inventory-interfaces-wazuh', retrying until the connection is\nsuccessful. 2026/02/05 13:23:56 indexer-connector: WARNING: IndexerConnector\ninitialization failed for index 'wazuh-states-inventory-networks-wazuh',\nretrying until the connection is successful. 2026/02/05 13:23:56 indexer-\nconnector: WARNING: IndexerConnector initialization failed for index 'wazuh-\nstates-inventory-users-wazuh', retrying until the connection is successful.\n2026/02/05 13:23:56 indexer-connector: WARNING: IndexerConnector\ninitialization failed for index 'wazuh-states-inventory-groups-wazuh',\nretrying until the connection is successful. 2026/02/05 13:23:56 indexer-\nconnector: WARNING: IndexerConnector initialization failed for index 'wazuh-\nstates-inventory-browser-extensions-wazuh', retrying until the connection is\nsuccessful. 2026/02/05 13:23:56 indexer-connector: WARNING: IndexerConnector\ninitialization failed for index 'wazuh-states-inventory-services-wazuh',\nretrying until the connection is successful.\nhttps://wazuh.slack.com/archives/C0A933R8E/p1770294637545329",
+ "comments": [
+ "The `IndexerConnector initialization failed` warnings indicate that your Wazuh Manager cannot establish a connection with the Wazuh Indexer. To help you diagnose the problem, I would need more information:\n\n- We need some information about the surroundings, such as:\n\n1. What version of Wazuh do you have installed?\n2. Deployment type: All-in-One or Distributed?\n3. Recent upgrade? If yes, from which version?\n\n- Next, we will validate the status of Wazuh Indexer. Execute these commands on your Indexer node:\n\n```bash\n# 1. Verify that the service is running\nsystemctl status wazuh-indexer\n\n# 2. Verify cluster health\ncurl -k -u admin:PASSWORD https://INDEXER_IP:9200/_cluster/health?pretty\n```\n\n- You could share your indexer configuration block from `/var/ossec/etc/ossec.conf` on the Manager. Please avoid sharing sensitive information.\n\nThis will validate:\n1. If Indexer is on the same host as Manager \u2192 use `127.0.0.1` (NOT `0.0.0.0`)\n2. Only ONE `` tag inside ``\n3. Certificate paths must match files in `/etc/filebeat/certs/`\n\n\n- Finally, we're going to test the connectivity between Wazuh Manager and Wazuh Indexer. Are they on the same cluster?\n\nRun on your Manager node:\n\n```bash\n# Please edit the certificates according to your configuration.\ncurl --cacert /etc/filebeat/certs/root-ca.pem \\\n --cert /etc/filebeat/certs/wazuh-server.pem \\\n --key /etc/filebeat/certs/wazuh-server-key.pem \\\n -u INDEXER_USER:INDEXER_PASSWORD \\\n https://INDEXER_IP:9200/_cluster/health?pretty\n\n# If it fails, test WITHOUT certificate verification\ncurl -k -u INDEXER_USER:INDEXER_PASSWORD https://INDEXER_IP:9200/_cluster/health?pretty\n```\n\nIf the second command (without certificates) works correctly, the issue might be related to incorrect or missing credentials in the Wazuh Manager keystore. The credentials may have changed or were not properly configured. You can update them with:\n\n\n```bash\necho 'INDEXER_USERNAME' | /var/ossec/bin/wazuh-keystore -f indexer -k username\necho 'INDEXER_PASSWORD' | /var/ossec/bin/wazuh-keystore -f indexer -k password\nsystemctl restart wazuh-manager\n```\n\nFor more details, see: https://documentation.wazuh.com/current/installation-guide/wazuh-server/step-by-step.html#configuring-the-wazuh-indexer-connection\n",
+ "\nI'm glad you were able to solve the problem by fixing the CA.\n\nRegarding the \"3,522 Pending - Evaluation\" you're seeing:\n\nThis is the Vulnerability Detection module showing pending software packages that need to be evaluated against vulnerability databases. This is completely normal after re-establishing the Indexer connection, especially if:\n\n- The vulnerability module was recently enabled\n- You have multiple agents reporting their software inventory\n- There was a backlog due to the previous connection issues\n\nWhat to expect:\n- This number should gradually decrease as the system processes the evaluations\n- Processing time depends on: number of agents, packages per agent, and server resources\n\nTo monitor progress:\n\n```bash\n# Watch vulnerability module activity\ntail -f /var/ossec/logs/ossec.log | grep -i vulnerability\n\n# Check if vulnerabilities are being indexed\ncurl -k -u admin:PASSWORD https://127.0.0.1:9200/_cat/indices/wazuh-states-vulnerabilities*?v\n```\n\n",
+ "Initial response resolved the issue and answered the second query as well. This LGTM ",
+ "Hi @fcaffieri \n\nCan you please look into this? The user is back with a response.\n\nThank you",
+ "The increasing count after 13 hours indicates the Vulnerability Detection module is NOT processing evaluations.\n\n\nVerify module configuration\n\nShare your complete `` block from `/var/ossec/etc/ossec.conf`:\n\n```xml\n\n yes\n yes\n 60m\n\n```\n\n\nLook for errors\n\n```bash\n# Check for errors/warnings\ncat /var/ossec/logs/ossec.log | grep -i -E \"vulnerability|vuln|error|warn\" | tail -50\n\n# Check if feeds are downloading\ncat /var/ossec/logs/ossec.log | grep -i \"feed\" | tail -20\n\n# Verify vulnerability database exists\nls -lh /var/ossec/queue/vulnerabilities/\ndu -sh /var/ossec/queue/vulnerabilities/\n```\n\nIf the directory is empty, check the errors if the feed download:\n\n```bash\ncat /var/ossec/logs/ossec.log | grep -i -E \"feed|content-updater|CTI|download\" | tail -50\n```\n\nTo force-feed download, restart the Wazuh manager and monitor logs:\n\n```bash\n# Enable debug logging\necho \"wazuh_modules.debug=2\" >> /var/ossec/etc/local_internal_options.conf\n\n# Restart manager\nsystemctl restart wazuh-manager\n\n# Monitor feed download (wait 5-10 minutes)\ntail -f /var/ossec/logs/ossec.log | grep -i -E \"content-updater|feed|download|CTI\"\n```\n\nVerify Syscollector is running.\n\n```bash\ncat /var/ossec/logs/ossec.log | grep -i \"syscollector\" | tail -20\n```\n\nWithout Syscollector collecting software inventory from agents, there's nothing to evaluate.\n\n\nEnable debug logging\n\n```bash\necho \"wazuh_modules.debug=2\" >> /var/ossec/etc/local_internal_options.conf\nsystemctl restart wazuh-manager\n# Wait 2-3 minutes\ntail -50 /var/ossec/logs/ossec.log\n```\n\nAdditionally, here is some documentation on VD:\n- https://documentation.wazuh.com/current/user-manual/capabilities/vulnerability-detection/how-it-works.html\n- https://documentation.wazuh.com/current/user-manual/capabilities/vulnerability-detection/configuring-scans.html\n",
+ "Closing this as there is no activity from the user side. Please resopn it if the user replies "
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/62286",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "Resolved",
+ "review/moderation",
+ "reviewer/team/community/Stuti3097",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 62264,
+ "title": "Assylbek2002 https://github.com/wazuh/wazuh-docker/pull/2000",
+ "body": "Some integrations like Slack, Telegram, Jira require the python3-requests module to send notifications.\nHowever, this module is not included in the wazuh-manager image.\n\nhttps://github.com/wazuh/wazuh-docker/pull/2000",
+ "comments": [
+ "Hello\n\nThank you very much for your contribution!\n\nWe are aware that the official images do not include the dependencies needed by all the features. The reason is that the images are designed to be as lightweight as possible and avoid potential vulnerabilities.\n\nIf you need this dependency in your image, I encourage you to fork our repository and add it there, as you have all the necessary tools within our repository to generate your own images without problems. I regret not being able to include this requirement in our base image, but we try to avoid introducing vulnerabilities that we cannot manage and that not everyone needs.\n\nWe have our guide for building the images, which is quite simple:\nhttps://documentation.wazuh.com/current/deployment-options/docker/build-docker-images-locally.html",
+ "This issue is stale because it has been open for 15 days with no activity.",
+ "Closing this as there is no activity from the user side. Please reopen it if needed"
+ ],
+ "external_community": [],
+ "url": "https://github.com/wazuh/community/issues/62264",
+ "labels": [
+ "GitHub",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "stale",
+ "review/moderation",
+ "reviewer/team/community/Stuti3097",
+ "closed/inactivity",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 62245,
+ "title": "stefano.pierini573 https://wazuh.slack.com/archives/C0A933R8E/p1770200274987709",
+ "body": "Hi community, I\u2019d like to get some information about monitoring VM resources\nwith the Wazuh agent installed. Previously, to monitor resources such as RAM,\nCPU, and STORAGE, it was necessary to use custom rules by following the guide\nin the blog . Now, part of those checks has been integrated into the\nIT_HYGIENE module, which is great. I only have one issue: in that module it\u2019s\npossible to see the status of both RAM and CPU, but there is no information\nabout disk usage, for example for partitions like `/root`, `/var`, and so on.\nAm I missing something, or has this not been implemented yet? And if it\nhasn\u2019t, how can I implement monitoring for a specific partition? Should I\nstill follow the guide I mentioned earlier? Thanks everyone.\nhttps://wazuh.slack.com/archives/C0A933R8E/p1770200274987709",
+ "comments": [
+ "Answered. Waiting for user response. ",
+ "Update #1: So far, no response from user. ",
+ "Update #2. No response from client. I'll proceed and mark this request as done. ",
+ "I am closing this due to inactivity from the user."
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/62245",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/oajani",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 62223,
+ "title": "llopez870407 https://wazuh.slack.com/archives/C0A933R8E/p1770145089679959",
+ "body": "Hello, I'm looking for an MCP-Wazuh project that isn't just tied to Claude,\nbut is open to connecting to other platforms. Anyone?\nhttps://wazuh.slack.com/archives/C0A933R8E/p1770145089679959",
+ "comments": [
+ "LGTM\n\nMarking as resolved "
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/62223",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "Resolved",
+ "review/moderation",
+ "reviewer/team/community/Stuti3097",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 62209,
+ "title": "Wazuh shutdown order ",
+ "body": "Hi team, I'd like to restart my server this night. Is there any specific \r\norder to shutdown wazuh components? I'm on wazuh 4.9 if that helps. Thanks \r\nin advance. \r\n\r\n-- \r\nYou received this message because you are subscribed to the Google Groups \"Wazuh | Mailing List\" group.\r\nTo unsubscribe from this group and stop receiving emails from it, send an email to wazuh+unsubscribe@googlegroups.com.\r\nTo view this discussion visit https://groups.google.com/d/msgid/wazuh/17e374d6-7aab-4b11-aa93-ad28fd21c41fn%40googlegroups.com.\r\n",
+ "comments": [
+ "The user has not responded in a reasonable amount of time",
+ "LGTM!\n\n"
+ ],
+ "external_community": [
+ "window.WIZ_global_data = {\"AfY8Hf\":false,\"DpimGf\":false,\"EP1ykd\":[\"/_/*\"],\"FdrFJe\":\"-7326367203974494548\",\"HiPsbb\":1,\"Im6cmf\":\"/_/GroupsFrontendUi\",\"LVIXXb\":1,\"LoQv7e\":false,\"MT7f9b\":[],\"MUE6Ne\":\"groups-frontend\",\"PLnRge\":\"https://docs.google.com/picker\",\"QrtxK\":\"\",\"S06Grb\":\"\",\"S6lZl\":112976253,\"TSDtV\":\"%.@.[[null,[[45447917,null,true,null,null,null,\\\"OeRc3d\\\"],[45447918,null,true,null,null,null,\\\"paZwJ\\\"],[45749384,null,true,null,null,null,\\\"ervCtb\\\"],[45709804,null,false,null,null,null,\\\"PoJR0\\\"],[45697011,null,true,null,null,null,\\\"xNsvyb\\\"],[45734189,null,true,null,null,null,\\\"XGnjIb\\\"],[45448406,null,false,null,null,null,\\\"jKfwq\\\"],[45780063,null,false,null,null,null,\\\"j3OYnd\\\"],[45447921,null,false,null,null,null,\\\"qAj4w\\\"],[45655177,null,true,null,null,null,\\\"VvKUq\\\"],[45447945,null,true,null,null,null,\\\"ckfnge\\\"],[45459555,null,false,null,null,null,\\\"Imeoqb\\\"],[45447936,null,false,null,null,null,\\\"MohPG\\\"],[45532875,null,true,null,null,null,\\\"udsJQe\\\"],[45722772,null,true,null,null,null,\\\"MVCE3b\\\"],[45646796,null,false,null,null,null,\\\"Q877Ab\\\"],[45753484,null,true,null,null,null,\\\"G9u5lb\\\"],[45699412,null,true,null,null,null,\\\"xLy3Ce\\\"],[45447950,null,true,null,null,null,\\\"lU0ald\\\"],[45447953,null,true,null,null,null,\\\"jbTsAe\\\"],[45447919,null,false,null,null,null,\\\"KMEQCe\\\"],[45699332,30,null,null,null,null,\\\"grENN\\\"],[45447930,null,null,null,\\\"signed_out_users\\\",null,\\\"JTFNhb\\\"],[45447928,null,true,null,null,null,\\\"Wh7on\\\"],[45447938,null,true,null,null,null,\\\"uVDGld\\\"],[45709766,null,true,null,null,null,\\\"sSVuJ\\\"],[45776470,null,true,null,null,null,\\\"A9MFwf\\\"],[45447926,null,true,null,null,null,\\\"IUIyxe\\\"],[45793915,null,true,null,null,null,\\\"Vwrzsf\\\"],[45721043,null,true,null,null,null,\\\"FcV2Ie\\\"],[45447924,null,true,null,null,null,\\\"hMPU3c\\\"],[45447934,null,null,null,\\\"https://forms.gle/DuQUYavHhwfEo4sp9\\\",null,\\\"gvGLK\\\"],[45447932,null,false,null,null,null,\\\"jlJZI\\\"]],\\\"CAMSOx0+/7riEsDRsgYOrrekEBYEFtqsNuYD3f+wBAq20g0Kuv0FCqMuFtjVAgqrrQAOppMFCpO2DQ6Jng4K\\\"]]]\",\"Tb2qJf\":4,\"UUFaWc\":\"%.@.null,1000,2]\",\"Vvafkd\":false,\"Yllh3e\":\"%.@.1783855395160222,122392613,3222757752]\",\"YlwcZe\":\"%.@.3,[1],[3600],2,[15,4,13,14,12,2]]\",\"ZZZ7Uc\":\"\",\"b5W2zf\":\"default_GroupsFrontendUi\",\"cfb2h\":\"boq_groupsfrontendserver_20260706.01_p0\",\"eNnkwf\":\"1783855394\",\"eptZe\":\"/_/GroupsFrontendUi/\",\"fPDxwd\":[97493638,97493660,105739272],\"gGcLoe\":false,\"gSs3jc\":1,\"hpRnh\":1,\"hsFLT\":\"%.@.null,10,3]\",\"iCzhFc\":false,\"nQyAE\":{\"G9u5lb\":\"true\",\"lU0ald\":\"true\",\"grENN\":\"30\",\"uVDGld\":\"true\",\"Vwrzsf\":\"true\",\"XGnjIb\":\"true\",\"FcV2Ie\":\"true\",\"j3OYnd\":\"false\",\"qAj4w\":\"false\",\"MohPG\":\"false\",\"Q877Ab\":\"false\",\"xLy3Ce\":\"true\",\"KMEQCe\":\"false\",\"JTFNhb\":\"signed_out_users\",\"IUIyxe\":\"true\",\"OeRc3d\":\"true\",\"paZwJ\":\"true\",\"PoJR0\":\"false\",\"xNsvyb\":\"true\",\"ckfnge\":\"true\",\"udsJQe\":\"true\",\"jbTsAe\":\"true\",\"Wh7on\":\"true\",\"sSVuJ\":\"true\",\"gvGLK\":\"https://forms.gle/DuQUYavHhwfEo4sp9\",\"jlJZI\":\"false\",\"ervCtb\":\"true\",\"A9MFwf\":\"true\",\"jKfwq\":\"false\"},\"p9hQne\":\"https://www.gstatic.com/_/boq-groups/_/r/\",\"qwAQke\":\"GroupsFrontendUi\",\"qymVe\":\"xVIsgHoSyfF7DIrYKkX0b6v1g6Y\",\"rtQCxc\":-330,\"u4g7r\":\"%.@.null,1,3]\",\"w2btAe\":\"%.@.null,null,\\\"\\\",false,null,null,null,false]\",\"xn5OId\":false,\"xnI9P\":false,\"xwAfE\":true,\"y2FhP\":\"prod\",\"yFnxrf\":1884,\"zChJod\":\"%.@.]\"}; window[\"_F_toggles_default_GroupsFrontendUi\"] = [0x30081086, 0x30b0b301, 0x100c5ad, ]; (function(){'use strict';var a=window,d=a.performance,l=k();a.cc_latency_start_time=d&&d.now?0:d&&d.timing&&d.timing.navigationStart?d.timing.navigationStart:l;function k(){return d&&d.now?d.now():(new Date).getTime()}function n(e){if(d&&d.now&&d.mark){var g=d.mark(e);if(g)return g.startTime;if(d.getEntriesByName&&(e=d.getEntriesByName(e).pop()))return e.startTime}return k()}a.onaft=function(){n(\"aft\")};a._isLazyImage=function(e){return e.hasAttribute(\"data-src\")||e.hasAttribute(\"data-ils\")||e.getAttribute(\"loading\")===\"lazy\"}; a.l=function(e){function g(b){var c={};c[b]=k();a.cc_latency.push(c)}function m(b){var c=n(\"iml\");b.setAttribute(\"data-iml\",c);re"
+ ],
+ "url": "https://github.com/wazuh/community/issues/62209",
+ "labels": [
+ "Google Groups",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/oajani",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 62208,
+ "title": "Wazuh_Terraform_Setup ",
+ "body": "I want to install a wazuh docker setup with persistent storage.\r\nHere I have attached the script file for your analysis. \r\n\r\n*Problem:*\r\n\r\nEvery first time terraform apply it works but when I destroy the EC2 \r\ninstance and re-apply Terraform, the Wazuh dashboard starts showing API \r\nconnection errors.\r\n\r\n\r\n\r\n*AxiosError: Error getting the authorization token3000 - Error getting the \r\nauthorization token: API host with host ID [1513629884013] could not check \r\nthe ability to use the run as. Ensure the API host is accesible and the \r\ninternal user has the minimal permissions to check this capability.*\r\n\r\nregars\r\nshifat\r\n\r\n-- \r\nYou received this message because you are subscribed to the Google Groups \"Wazuh | Mailing List\" group.\r\nTo unsubscribe from this group and stop receiving emails from it, send an email to wazuh+unsubscribe@googlegroups.com.\r\nTo view this discussion visit https://groups.google.com/d/msgid/wazuh/1da330e9-52f6-4655-9192-033c7a7ecd6an%40googlegroups.com.\r\n",
+ "comments": [
+ "Unable to work on this because of documentation testing https://github.com/wazuh/internal-documentation-requests/issues/550",
+ "no new response from user"
+ ],
+ "external_community": [
+ "window.WIZ_global_data = {\"AfY8Hf\":false,\"DpimGf\":false,\"EP1ykd\":[\"/_/*\"],\"FdrFJe\":\"-1924632486170959059\",\"HiPsbb\":1,\"Im6cmf\":\"/_/GroupsFrontendUi\",\"LVIXXb\":1,\"LoQv7e\":false,\"MT7f9b\":[],\"MUE6Ne\":\"groups-frontend\",\"PLnRge\":\"https://docs.google.com/picker\",\"QrtxK\":\"\",\"S06Grb\":\"\",\"S6lZl\":112976253,\"TSDtV\":\"%.@.[[null,[[45447917,null,true,null,null,null,\\\"OeRc3d\\\"],[45447918,null,true,null,null,null,\\\"paZwJ\\\"],[45749384,null,true,null,null,null,\\\"ervCtb\\\"],[45709804,null,false,null,null,null,\\\"PoJR0\\\"],[45697011,null,true,null,null,null,\\\"xNsvyb\\\"],[45734189,null,true,null,null,null,\\\"XGnjIb\\\"],[45448406,null,false,null,null,null,\\\"jKfwq\\\"],[45780063,null,false,null,null,null,\\\"j3OYnd\\\"],[45447921,null,false,null,null,null,\\\"qAj4w\\\"],[45655177,null,true,null,null,null,\\\"VvKUq\\\"],[45447945,null,true,null,null,null,\\\"ckfnge\\\"],[45459555,null,false,null,null,null,\\\"Imeoqb\\\"],[45447936,null,false,null,null,null,\\\"MohPG\\\"],[45532875,null,true,null,null,null,\\\"udsJQe\\\"],[45722772,null,true,null,null,null,\\\"MVCE3b\\\"],[45646796,null,false,null,null,null,\\\"Q877Ab\\\"],[45753484,null,true,null,null,null,\\\"G9u5lb\\\"],[45699412,null,true,null,null,null,\\\"xLy3Ce\\\"],[45447950,null,true,null,null,null,\\\"lU0ald\\\"],[45447953,null,true,null,null,null,\\\"jbTsAe\\\"],[45447919,null,false,null,null,null,\\\"KMEQCe\\\"],[45699332,30,null,null,null,null,\\\"grENN\\\"],[45447930,null,null,null,\\\"signed_out_users\\\",null,\\\"JTFNhb\\\"],[45447928,null,true,null,null,null,\\\"Wh7on\\\"],[45447938,null,true,null,null,null,\\\"uVDGld\\\"],[45709766,null,true,null,null,null,\\\"sSVuJ\\\"],[45776470,null,true,null,null,null,\\\"A9MFwf\\\"],[45447926,null,true,null,null,null,\\\"IUIyxe\\\"],[45793915,null,true,null,null,null,\\\"Vwrzsf\\\"],[45721043,null,true,null,null,null,\\\"FcV2Ie\\\"],[45447924,null,true,null,null,null,\\\"hMPU3c\\\"],[45447934,null,null,null,\\\"https://forms.gle/DuQUYavHhwfEo4sp9\\\",null,\\\"gvGLK\\\"],[45447932,null,false,null,null,null,\\\"jlJZI\\\"]],\\\"CAMSOx0+/7riEsDRsgYOrrekEBYEFtqsNuYD3f+wBAq20g0Kuv0FCqMuFtjVAgqrrQAOppMFCpO2DQ6Jng4K\\\"]]]\",\"Tb2qJf\":4,\"UUFaWc\":\"%.@.null,1000,2]\",\"Vvafkd\":false,\"Yllh3e\":\"%.@.1783855398229228,122392629,1275810696]\",\"YlwcZe\":\"%.@.3,[1],[3600],2,[15,4,13,14,12,2]]\",\"ZZZ7Uc\":\"\",\"b5W2zf\":\"default_GroupsFrontendUi\",\"cfb2h\":\"boq_groupsfrontendserver_20260706.01_p0\",\"eNnkwf\":\"1783855397\",\"eptZe\":\"/_/GroupsFrontendUi/\",\"fPDxwd\":[97493638,97493660,105739272],\"gGcLoe\":false,\"gSs3jc\":1,\"hpRnh\":1,\"hsFLT\":\"%.@.null,10,3]\",\"iCzhFc\":false,\"nQyAE\":{\"G9u5lb\":\"true\",\"lU0ald\":\"true\",\"grENN\":\"30\",\"uVDGld\":\"true\",\"Vwrzsf\":\"true\",\"XGnjIb\":\"true\",\"FcV2Ie\":\"true\",\"j3OYnd\":\"false\",\"qAj4w\":\"false\",\"MohPG\":\"false\",\"Q877Ab\":\"false\",\"xLy3Ce\":\"true\",\"KMEQCe\":\"false\",\"JTFNhb\":\"signed_out_users\",\"IUIyxe\":\"true\",\"OeRc3d\":\"true\",\"paZwJ\":\"true\",\"PoJR0\":\"false\",\"xNsvyb\":\"true\",\"ckfnge\":\"true\",\"udsJQe\":\"true\",\"jbTsAe\":\"true\",\"Wh7on\":\"true\",\"sSVuJ\":\"true\",\"gvGLK\":\"https://forms.gle/DuQUYavHhwfEo4sp9\",\"jlJZI\":\"false\",\"ervCtb\":\"true\",\"A9MFwf\":\"true\",\"jKfwq\":\"false\"},\"p9hQne\":\"https://www.gstatic.com/_/boq-groups/_/r/\",\"qwAQke\":\"GroupsFrontendUi\",\"qymVe\":\"T_4MOcazhcwfMghw8uTZ1_u4HRM\",\"rtQCxc\":-330,\"u4g7r\":\"%.@.null,1,3]\",\"w2btAe\":\"%.@.null,null,\\\"\\\",false,null,null,null,false]\",\"xn5OId\":false,\"xnI9P\":false,\"xwAfE\":true,\"y2FhP\":\"prod\",\"yFnxrf\":1884,\"zChJod\":\"%.@.]\"}; window[\"_F_toggles_default_GroupsFrontendUi\"] = [0x30081086, 0x30b0b301, 0x100c5ad, ]; (function(){'use strict';var a=window,d=a.performance,l=k();a.cc_latency_start_time=d&&d.now?0:d&&d.timing&&d.timing.navigationStart?d.timing.navigationStart:l;function k(){return d&&d.now?d.now():(new Date).getTime()}function n(e){if(d&&d.now&&d.mark){var g=d.mark(e);if(g)return g.startTime;if(d.getEntriesByName&&(e=d.getEntriesByName(e).pop()))return e.startTime}return k()}a.onaft=function(){n(\"aft\")};a._isLazyImage=function(e){return e.hasAttribute(\"data-src\")||e.hasAttribute(\"data-ils\")||e.getAttribute(\"loading\")===\"lazy\"}; a.l=function(e){function g(b){var c={};c[b]=k();a.cc_latency.push(c)}function m(b){var c=n(\"iml\");b.setAttribute(\"data-iml\",c);re"
+ ],
+ "url": "https://github.com/wazuh/community/issues/62208",
+ "labels": [
+ "Google Groups",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/oajani",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 62049,
+ "title": "a.g.efremov92 https://wazuh.slack.com/archives/C07CCCCGHHP/p1769701161291139",
+ "body": "hi, how to find if wazuh-manager/wazuh-worker can't download vulnerability databases from http://cti.wazuh.comcti.wazuh.com> API?\nhttps://wazuh.slack.com/archives/C07CCCCGHHP/p1769701161291139",
+ "comments": [
+ "Hello Andrey, \n\nTo verify whether the Wazuh manager or worker is unable to download vulnerability databases from cti.wazuh.com, you can check the following points:\n\n1. Review Wazuh logs\n\nOn the affected node, inspect /var/ossec/logs/ossec.log for vulnerability-related messages. \n\nErrors such as connection timeouts, SSL failures, or HTTP 401/403 responses indicate that the vulnerability databases cannot be downloaded.\n\ngrep -Ei \"vulnerability\" /var/ossec/logs/ossec.log\n\n\n\n \n2. Validate network connectivity\n\nConfirm the node has outbound HTTPS access to the CTI service:\n\ncurl -v https://cti.wazuh.com/ 443 && timedatectl\n\n\n\nnc -zv cti.wazuh.com 443\n\nIf this fails, the issue is usually related to firewall rules, proxy restrictions, or DNS resolution.\n\n\n\n3. Check vulnerability detector configuration\nVerify that vulnerability detection is enabled in /var/ossec/etc/ossec.conf and that no custom configuration is overriding the default CTI behavior. After any change, restart the manager.\n\n\n\nFor reference, the following documentation explains how vulnerability detection and CTI work and how to troubleshoot update issues:\n\nhttps://documentation.wazuh.com/current/user-manual/capabilities/vulnerability-detection/configuring-scans.html#configuration\n\n",
+ "Hello @luissalaz11 LGTM, However, you could have also shared the screenshots with the user to serve as reference and aid their troubleshooting.\n\nThank you",
+ "closing this one as user stopped answering"
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/62049",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/oajani",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 62030,
+ "title": "damien.ramsamy8 https://wazuh.slack.com/archives/C0A933R8E/p1769686825805909",
+ "body": "Hello, *Is it possible to create a rule for FIM so that .lock or .crypt files\nappear on the dashboard and trigger an email to the administrators?*\nhttps://wazuh.slack.com/archives/C0A933R8E/p1769686825805909",
+ "comments": [
+ "## Update\nProvided the user with a [response](https://wazuh.slack.com/archives/C0A933R8E/p1769695776590389?thread_ts=1769686825.805909&cid=C0A933R8E) stating that It is possible and how to do It and narrow down the configuration for specific file extensions rather than using default configuration. Also mentioned that email notification can be configured in many ways.",
+ "This issue is stale because it has been open for 15 days with no activity."
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/62030",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "stale",
+ "reviewer/team/community/oajani",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 62029,
+ "title": "logitechflames https://wazuh.slack.com/archives/C0A933R8E/p1769680406795549",
+ "body": "*Hello <@U01HXEV6SL8>,* I\u2019m looking for guidance and real-world experience on how teams are using *LLMs with Wazuh* to address: \u2022 Alert fatigue \u2022 False positives \u2022 Automated alert triage and handling Specifically, I\u2019m interested in *agentic AI approaches* using *LLMs (e.g., via Ollama)* for automatic analysis and decision-making on Wazuh alerts. I\u2019d appreciate insights on: \u2022 How Wazuh alerts are integrated with LLMs (API, Indexer, Kafka, Filebeat, etc.) \u2022 The overall *end-to-end flow* for handling alerts using LLMs \u2022 Agentic AI designs (single agent vs multi-agent workflows) \u2022 Techniques used for alert deduplication, suppression, enrichment, and severity re-scoring \u2022 Models you\u2019ve successfully used with *Ollama* (LLaMA, Mistral, DeepSeek, etc.) and why \u2022 How false positives are reduced and how analyst feedback is incorporated \u2022 Safeguards and human-in-the-loop controls for automated actions The goal is to keep *Wazuh as the detection engine*, while using LLMs as an *intelligent analyst layer* to reduce noise and improve response quality. Any shared architectures, tools, or best practices from production environments would be greatly appreciated. Thank you.\nhttps://wazuh.slack.com/archives/C0A933R8E/p1769680406795549",
+ "comments": [
+ "This issue is stale because it has been open for 15 days with no activity.",
+ "Closing due to inactivity from the user"
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/62029",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "stale",
+ "reviewer/team/community/oajani",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 62007,
+ "title": "anthony.delossantos https://wazuh.slack.com/archives/C07CCCCGHHP/p1769631979254659",
+ "body": "any issue why sometimes my wazuh server getting issue on this\nhttps://wazuh.slack.com/archives/C07CCCCGHHP/p1769631979254659",
+ "comments": [
+ "Hi @matiasmercado-ar \n\nYour initial response is great. The user has returned with a response. Can you please look into this?\n\nThank you! ",
+ "Dm'ed @matiasmercado-ar ",
+ "Closed due to inactivity. Please reopen it if the user replies ."
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/62007",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "Resolved without feedback",
+ "review/moderation",
+ "reviewer/team/community/Stuti3097",
+ "closed/inactivity",
+ "review/quality",
+ "flag/no_follow-up",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 61992,
+ "title": "yasminebenchekroun238 https://wazuh.slack.com/archives/C0A933R8E/p1769605776766819",
+ "body": "Hello, I\u2019m trying to determine the most appropriate deployment method for my\nuse case and would appreciate some guidance. My goal is to monitor around 200\nservers and run Wazuh in a distributed environment. At this stage, I\u2019m\nconsidering starting with three virtual machines: \u2022 one VM for the Wazuh\nmanager \u2022 one VM for the Wazuh indexer \u2022 one VM for the Wazuh dashboard I\u2019m\nunsure which installation approach would be the most suitable in this context:\n\u2022 Can Docker be used to properly deploy these components on three separate\nVMs, or is Docker mainly intended for single-host / single-VM deployments? \u2022\nWould it be better to use Kubernetes to achieve this kind of separation across\nmultiple VMs? \u2022 Or is there another more appropriate/recommended deployment\noption for this scale\nhttps://wazuh.slack.com/archives/C0A933R8E/p1769605776766819",
+ "comments": [
+ "LGTM \n\nClosing this as resolved "
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/61992",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "review/moderation",
+ "reviewer/team/community/Stuti3097",
+ "ambassador",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 61984,
+ "title": "hamza.the.integer https://discord.com/channels/1049711339578331186/1049711340316541004/1466039950154666111",
+ "body": "@everyone \nHi , wazuh team i am working on highly critical production project. i have installed a 3 node cluster for both manager and indexer.\nI want to collect the vulnerability related software or hardware related relavent data from wazuh and want to generate the cpe string myself.\nNow the question is if i turn the vd module to off , then how i can get the data to calculate the cpe string.\nplease help ASAP.\nhttps://discord.com/channels/1049711339578331186/1049711340316541004/1466039950154666111",
+ "comments": [
+ "Hi @Jorgesnchz\n\nLGTM\n\nThe user looks happy with your answer. So I'm closing this as resolved. Please reopen it if the user comes back.\n\nThank you\n\n"
+ ],
+ "external_community": [
+ "[Linked Discord conversation \u2014 not fetchable without a bot token in that server.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/61984",
+ "labels": [
+ "Discord",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "Resolved",
+ "review/moderation",
+ "reviewer/team/community/Stuti3097",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 61968,
+ "title": "murali.m https://wazuh.slack.com/archives/C0A933R8E/p1769577177048449",
+ "body": "Hello <#C0A933R8E>, i have a major doubt on the scaling, i want to add another\nworker node and indexer. for, the existing production environment. 1\\. Is that\npossible to do on the existing production environment. why because of the\nexisting production enviroment was build with 1-indexer,manager-node&worker-\nnode with dashboard through the config.yml. 2\\. If it is possible, how can i\ndo this. Could you help me in this topic.\nhttps://wazuh.slack.com/archives/C0A933R8E/p1769577177048449",
+ "comments": [
+ "No response from the user"
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/61968",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/oajani",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 61956,
+ "title": "thaynarasoarescampos https://wazuh.slack.com/archives/C0A933R8E/p1769538010779489",
+ "body": "Hey <@U01HXEV6SL8>, I recently noticed that not all of my agents are showing\nvulnerabilities. I'd like to check if there's a problem or if it's simply\nbecause the vulnerability isn't present. Currently, my environment has 1,059\nactive agents running version 4.14.2. My environment is clustered: 2 Index\nVirtual Machines, 1 Virtual Machine that acts as Master and Dashboard, and 2\nWorker Virtual Machines.\nhttps://wazuh.slack.com/archives/C0A933R8E/p1769538010779489",
+ "comments": [
+ "Community issue resolved",
+ "Closed due to inactivity "
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/61956",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/sakib789",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 61946,
+ "title": "Custom Audiocodes SBC decoder ",
+ "body": " Hey,\r\n\r\nI've desperately tried to build a custom decoder for Audiocodes Session \r\nBorder Controlles but I can't get them to work.\r\nThey're sending via syslog but the log format is just _weird_.\r\n\r\nI built RegExes with Regex101 which work on _some_ strings but never get a \r\nchild decoder working. \r\n\r\nLogs look something like\r\n\r\n2026-01-23T03:05:54.032649+01:00 192.168.180.15 [S=17183] [BID=bc2577:83] \r\nRAISE-ALARM:acProxyConnectionLost: [HA-Main] Proxy Set Alarm Proxy Set 1 \r\n(OXE): Proxy lost. looking for another proxy; Severity:major; \r\nSource:Board#1/ProxyConnection#1; Unique ID:9; [Time:23-01@03:05:53.371] \r\n[19508657]\r\n2026-01-23T03:05:54.032649+01:00 192.168.180.15 [S=17184] [BID=bc2577:83] \r\nRAISE-ALARM:acIpGroupNoRouteAlarm: [HA-Main] IP Group is temporarily \r\nblocked. IP Group (OXE Vodafone Default) Blocked Reason: No Working Proxy; \r\nSeverity:major; Source:Board#1/IPGroup#3; Unique ID:10; \r\n[Time:23-01@03:05:53.372] [19508660]\r\n2026-01-23T03:05:54.178838+01:00 192.168.180.15 [S=17185] \r\n[SID=bc2577:83:159074] (N 18040269)?? [WARNING] Can't find matching \r\ntransaction for response 408 to OPTIONS. Call-ID: \r\n18768185823120263537@192.168.180.31 [Time:23-01@03:05:53.519] [19508665]\r\n2026-01-23T03:06:11.345569+01:00 192.168.180.15 [S=17186] \r\n[SID=bc2577:83:159076] (N 18040299)?? [WARNING] Can't find matching \r\ntransaction for response 408 to OPTIONS. Call-ID: \r\n174294617623120263554@192.168.180.31 [Time:23-01@03:06:10.686] [19508697]\r\n2026-01-23T03:06:21.147926+01:00 192.168.180.15 [S=17187] [BID=bc2577:83] \r\n(N 18040317)!! [ERROR] AcSIPParser [SIP Message Headers] Parse error: \r\n\"Unexpected symbol ' ' in scheme.\". (L:1,C:18)Parsed line: Cirpack \r\nKeepAlive Packet [Time:23-01@03:06:20.488] [19508717]\r\n2026-01-23T03:06:21.148347+01:00 192.168.180.15 [S=17188] [BID=bc2577:83] \r\n(N 18040319)!! [ERROR] SIPStackEngine::HandleReceivedMessage - Basic error \r\nin Message [Time:23-01@03:06:20.488] [19508719]\r\n2026-01-23T03:06:28.151753+01:00 192.168.180.15 [S=17189] \r\n[SID=bc2577:83:159078] (N 18040336)?? [WARNING] Can't find matching \r\ntransaction for response 408 to OPTIONS. Call-ID: \r\n58472385523120263611@192.168.180.31 [Time:23-01@03:06:27.492] [19508737]\r\n2026-01-23T03:06:45.151462+01:00 192.168.180.15 [S=17190] \r\n[SID=bc2577:83:159081] (N 18040387)?? [WARNING] Can't find matching \r\ntransaction for response 408 to OPTIONS. Call-ID: \r\n199255425723120263628@192.168.180.31 [Time:23-01@03:06:44.492] [19508792]\r\n2026-01-23T03:06:48.771166+01:00 192.168.180.15 [S=17191] [BID=bc2577:83] \r\n(N 18040401)!! [ERROR] AcSIPParser [SIP Message Headers] Parse error: \r\n\"Unexpected symbol ' ' in scheme.\". (L:1,C:18)Parsed line: Cirpack \r\nKeepAlive Packet [Time:23-01@03:06:48.111] [19508808]\r\n2026-01-23T03:06:48.771166+01:00 192.168.180.15 [S=17192] [BID=bc2577:83] \r\n(N 18040403)!! [ERROR] SIPStackEngine::HandleReceivedMessage - Basic error \r\nin Message [Time:23-01@03:06:48.111] [19508810]\r\n2026-01-23T03:07:02.168722+01:00 192.168.180.15 [S=17193] \r\n[SID=bc2577:83:159084] (N 18040640)?? [WARNING] Can't find matching \r\ntransaction for response 408 to OPTIONS. Call-ID: \r\n170397740123120263645@192.168.180.31 [Time:23-01@03:07:01.509] [19509065]\r\n2026-01-23T03:07:07.518192+01:00 192.168.180.15 [S=17194] \r\n[SID=bc2577:83:159086] (N 18040667)?? [WARNING] Route Failed! IPGroup 3 is \r\nnot alive [Time:23-01@03:07:06.858] [19509094]\r\n2026-01-23T03:07:07.518192+01:00 192.168.180.15 [S=17195] \r\n[SID=bc2577:83:159086] (N 18040669)?? [WARNING] Route Failed! IPGroup 3 is \r\nnot alive [Time:23-01@03:07:06.858] [19509096]\r\n2026-01-23T03:07:07.533423+01:00 192.168.180.15 [S=17196] \r\n[SID=bc2577:83:159087] (N 18040684)?? [WARNING] Can't find matching dialog \r\nfor ACK request. Call-ID: voQlJjc4XdCd:xvA [Time:23-01@03:07:06.874] \r\n[19509114]\r\n2026-01-23T03:07:07.579022+01:00 192.168.180.15 [S=17197] \r\n[SID=bc2577:83:159088] (N 18040699)?? [WARNING] Route Failed! IPGroup 3 is \r\nnot alive [Time:23-01@03:07:06.919] [19509130]\r\n2026-01-23T03:07:07.579022+01:00 192.168.180.15 [S=17198] \r\n[SID=bc2577:83:159088] (N 18040701)?? [WARNING] Route Failed! IPGroup 3 is \r\nnot alive [Time:23-01@03:07:06.919] [19509132]\r\n2026-01-23T03:07:07.594375+01:00 192.168.180.15 [S=17199] \r\n[SID=bc2577:83:159089] (N 18040716)?? [WARNING] Can't find matching dialog \r\nfor ACK request. Call-ID: 8ZTmm8oEgg4X6AUV [Time:23-01@03:07:06.935] \r\n[19509150]\r\n\r\n2026-01-27T14:32:55.503714+01:00 192.168.180.15 [S=46920] [BID=bc2577:83] \r\n (N 20026931)!! [ERROR] AcSIPParser [SIP Message Headers] Parse error: \r\n\"Unexpected symbol ' ' in scheme.\". (L:1,C:18)Parsed line: Cirpack \r\nKeepAlive Packet [Time:27-01@14:32:52.208] [21657916]\r\n2026-01-27T14:32:55.503714+01:00 192.168.180.15 [S=46921] [BID=bc2577:83] \r\n (N 20026933)!! [ERROR] SIPStackEngine::HandleReceivedMessage - Basic error \r\nin Message [Time:27-01@14:32:52.208] [21657918]\r\n2026-01-27T14:33:23.644433+01:00 192.168.180.15 [S=46922] [BID=bc2577:83] \r\n (N 20027396)!! [ERROR] AcSIPParser [SIP Message Headers] Parse error: \r\n\"Unexpected symbol ' ' in scheme.\". (L:1,C:18)Parsed line: Cirpack \r\nKeepAlive Packet [Time:27-01@14:33:20.348] [21658414]\r\n2026-01-27T14:33:23.645236+01:00 192.168.180.15 [S=46923] [BID=bc2577:83] \r\n (N 20027398)!! [ERROR] SIPStackEngine::HandleReceivedMessage - Basic error \r\nin Message [Time:27-01@14:33:20.349] [21658416]\r\n2026-01-27T14:33:51.515995+01:00 192.168.180.15 [S=46924] [BID=bc2577:83] \r\n (N 20027557)!! [ERROR] AcSIPParser [SIP Message Headers] Parse error: \r\n\"Unexpected symbol ' ' in scheme.\". (L:1,C:18)Parsed line: Cirpack \r\nKeepAlive Packet [Time:27-01@14:33:48.220] [21658584]\r\n2026-01-27T14:33:51.515995+01:00 192.168.180.15 [S=46925] [BID=bc2577:83] \r\n (N 20027559)!! [ERROR] SIPStackEngine::HandleReceivedMessage - Basic error \r\nin Message [Time:27-01@14:33:48.220] [21658586]\r\n2026-01-27T14:34:19.627964+01:00 192.168.180.15 [S=46926] [BID=bc2577:83] \r\n (N 20027598)!! [ERROR] AcSIPParser [SIP Message Headers] Parse error: \r\n\"Unexpected symbol ' ' in scheme.\". (L:1,C:18)Parsed line: Cirpack \r\nKeepAlive Packet [Time:27-01@14:34:16.332] [21658630]\r\n2026-01-27T14:34:19.627964+01:00 192.168.180.15 [S=46927] [BID=bc2577:83] \r\n (N 20027600)!! [ERROR] SIPStackEngine::HandleReceivedMessage - Basic error \r\nin Message [Time:27-01@14:34:16.332] [21658632]\r\n2026-01-27T14:34:47.514271+01:00 192.168.180.15 [S=46928] [BID=bc2577:83] \r\n (N 20027929)!! [ERROR] AcSIPParser [SIP Message Headers] Parse error: \r\n\"Unexpected symbol ' ' in scheme.\". (L:1,C:18)Parsed line: Cirpack \r\nKeepAlive Packet [Time:27-01@14:34:44.219] [21658986]\r\n2026-01-27T14:34:47.514271+01:00 192.168.180.15 [S=46929] [BID=bc2577:83] \r\n (N 20027931)!! [ERROR] SIPStackEngine::HandleReceivedMessage - Basic error \r\nin Message [Time:27-01@14:34:44.219] [21658988]\r\n2026-01-27T14:35:15.638334+01:00 192.168.180.15 [S=46930] [BID=bc2577:83] \r\n (N 20028408)!! [ERROR] AcSIPParser [SIP Message Headers] Parse error: \r\n\"Unexpected symbol ' ' in scheme.\". (L:1,C:18)Parsed line: Cirpack \r\nKeepAlive Packet [Time:27-01@14:35:12.343] [21659498]\r\n2026-01-27T14:35:15.638334+01:00 192.168.180.15 [S=46931] [BID=bc2577:83] \r\n (N 20028410)!! [ERROR] SIPStackEngine::HandleReceivedMessage - Basic error \r\nin Message [Time:27-01@14:35:12.343] [21659500]\r\n\r\nAt first it doesn't send a hostname. So with multiple devices I think I \r\nneed to match it via the IP? Next is that different types of error messages \r\nseem to be available. I tried some RegExes like \\[S=(\\d++)] \\[BID=(\\S+) \r\nRAISE-ALARM:(\\S+ )\\[(HA-Main)](\\s+)(.*?)\\s+\\(([^)]+)\\):\\s+([^;]+); \r\nSeverity:([^;]+);\\s+Source:([^;]+);\\s+Unique \r\nID:([^;]+);\\s+\\[Time:([^\\]]+)\\]\\s+\\[(\\d+)\\] or\r\n\\[S=(\\d+)\\]\\s+\\[BID=([^\\]]+)\\]\\s+RAISE-ALARM:([^:]+):\\s+\\[HA-Main]\\s+(.*?)\\s+\\(([^)]+)\\):\\s+([^;]+);\\s+Severity:([^;]+);\\s+Source:([^;]+);\\s+Unique \r\nID:([^;]+);\\s+\\[Time:([^\\]]+)\\]\\s+\\[(\\d+)\\] but none match the child \r\ndecoder.\r\n\r\nMy current attempt looks like:\r\n\r\n\r\n [S=\r\n\r\n\r\n \r\n\r\n SBC\r\n (\\d+)\\]\\s+\\[BID=([^\\]]+)]\\s+RAISE-ALARM:([^:]+):\\s+\\[HA-Main]\\s+(.*?)\\s+\\(([^)]+)\\):\\s+([^;]+);\\s+Severity:([^;]+);\\s+Source:([^;]+);\\s+Unique \r\nID:([^;]+);\\s+\\[Time:([^\\]]+)]\\s+\\[(\\d+)]\r\n s_id bid alarm_code component site alarm_message severity source \r\nunique_id event_time event_id\r\n\r\n\r\nThis at least matches the s_id but nothing more. I am running out of ideas \r\nhow to get a working decoder...\r\n\r\n**Phase 1: Completed pre-decoding.\r\n full event: '2026-01-23T03:05:54.032649+01:00 192.168.180.15 \r\n[S=17183] [BID=bc2577:83] RAISE-ALARM:acProxyConnectionLost: [HA-Main] \r\nProxy Set Alarm Proxy Set 1 (OXE): Proxy lost. looking for another proxy; \r\nSeverity:major; Source:Board#1/ProxyConnection#1; Unique ID:9; \r\n[Time:23-01@03:05:53.371] [19508657]'\r\n timestamp: '2026-01-23T03:05:54.032649+01:00'\r\n\r\n**Phase 2: Completed decoding.\r\n name: 'SBC'\r\n s_id: '17183'\r\n\r\nIf anyone has ideas I would be very happy...\r\n\r\ncheers chic\r\n\r\n-- \r\nYou received this message because you are subscribed to the Google Groups \"Wazuh | Mailing List\" group.\r\nTo unsubscribe from this group and stop receiving emails from it, send an email to wazuh+unsubscribe@googlegroups.com.\r\nTo view this discussion visit https://groups.google.com/d/msgid/wazuh/057aa0bb-7b6b-4586-823e-65027a644554n%40googlegroups.com.\r\n",
+ "comments": [
+ "Answer: https://groups.google.com/g/wazuh/c/SXyv9Fc0g14/m/TSih8CXFAAAJ",
+ "LGTM. For the future, please try to attach the logtest/ ruleset test output for user reference.\n\nThank you!",
+ "This issue is stale because it has been open for 15 days with no activity.",
+ "Hi @jr0me \n\nThe user is back with a query. Can you please take a look? \n\nThank you!",
+ "Dm'ed @jr0me ",
+ "The user is back can you please look into this ",
+ "Mgs'ed @jr0me on Slack",
+ "Hi @jr0me \n\nI'm closing this issue because there is no activity from the user side. Please try to respond to the user in a timely manner, so we can help them to resolve the issue faster if possible. Feel free to open the issue if the user is back."
+ ],
+ "external_community": [
+ "window.WIZ_global_data = {\"AfY8Hf\":false,\"DpimGf\":false,\"EP1ykd\":[\"/_/*\"],\"FdrFJe\":\"4242657562368542060\",\"HiPsbb\":1,\"Im6cmf\":\"/_/GroupsFrontendUi\",\"LVIXXb\":1,\"LoQv7e\":false,\"MT7f9b\":[],\"MUE6Ne\":\"groups-frontend\",\"PLnRge\":\"https://docs.google.com/picker\",\"QrtxK\":\"\",\"S06Grb\":\"\",\"S6lZl\":112976253,\"TSDtV\":\"%.@.[[null,[[45447917,null,true,null,null,null,\\\"OeRc3d\\\"],[45447918,null,true,null,null,null,\\\"paZwJ\\\"],[45749384,null,true,null,null,null,\\\"ervCtb\\\"],[45709804,null,false,null,null,null,\\\"PoJR0\\\"],[45697011,null,true,null,null,null,\\\"xNsvyb\\\"],[45734189,null,true,null,null,null,\\\"XGnjIb\\\"],[45448406,null,false,null,null,null,\\\"jKfwq\\\"],[45780063,null,false,null,null,null,\\\"j3OYnd\\\"],[45447921,null,false,null,null,null,\\\"qAj4w\\\"],[45655177,null,true,null,null,null,\\\"VvKUq\\\"],[45447945,null,true,null,null,null,\\\"ckfnge\\\"],[45459555,null,false,null,null,null,\\\"Imeoqb\\\"],[45447936,null,false,null,null,null,\\\"MohPG\\\"],[45532875,null,true,null,null,null,\\\"udsJQe\\\"],[45722772,null,true,null,null,null,\\\"MVCE3b\\\"],[45646796,null,false,null,null,null,\\\"Q877Ab\\\"],[45753484,null,true,null,null,null,\\\"G9u5lb\\\"],[45699412,null,true,null,null,null,\\\"xLy3Ce\\\"],[45447950,null,true,null,null,null,\\\"lU0ald\\\"],[45447953,null,true,null,null,null,\\\"jbTsAe\\\"],[45447919,null,false,null,null,null,\\\"KMEQCe\\\"],[45699332,30,null,null,null,null,\\\"grENN\\\"],[45447930,null,null,null,\\\"signed_out_users\\\",null,\\\"JTFNhb\\\"],[45447928,null,true,null,null,null,\\\"Wh7on\\\"],[45447938,null,true,null,null,null,\\\"uVDGld\\\"],[45709766,null,true,null,null,null,\\\"sSVuJ\\\"],[45776470,null,true,null,null,null,\\\"A9MFwf\\\"],[45447926,null,true,null,null,null,\\\"IUIyxe\\\"],[45793915,null,true,null,null,null,\\\"Vwrzsf\\\"],[45721043,null,true,null,null,null,\\\"FcV2Ie\\\"],[45447924,null,true,null,null,null,\\\"hMPU3c\\\"],[45447934,null,null,null,\\\"https://forms.gle/DuQUYavHhwfEo4sp9\\\",null,\\\"gvGLK\\\"],[45447932,null,false,null,null,null,\\\"jlJZI\\\"]],\\\"CAMSOx0+/7riEsDRsgYOrrekEBYEFtqsNuYD3f+wBAq20g0Kuv0FCqMuFtjVAgqrrQAOppMFCpO2DQ6Jng4K\\\"]]]\",\"Tb2qJf\":4,\"UUFaWc\":\"%.@.null,1000,2]\",\"Vvafkd\":false,\"Yllh3e\":\"%.@.1783855410435947,122393272,1644206838]\",\"YlwcZe\":\"%.@.3,[1],[3600],2,[15,4,13,14,12,2]]\",\"ZZZ7Uc\":\"\",\"b5W2zf\":\"default_GroupsFrontendUi\",\"cfb2h\":\"boq_groupsfrontendserver_20260706.01_p0\",\"eNnkwf\":\"1783855409\",\"eptZe\":\"/_/GroupsFrontendUi/\",\"fPDxwd\":[97493638,97493660,105739272],\"gGcLoe\":false,\"gSs3jc\":1,\"hpRnh\":1,\"hsFLT\":\"%.@.null,10,3]\",\"iCzhFc\":false,\"nQyAE\":{\"G9u5lb\":\"true\",\"lU0ald\":\"true\",\"grENN\":\"30\",\"uVDGld\":\"true\",\"Vwrzsf\":\"true\",\"XGnjIb\":\"true\",\"FcV2Ie\":\"true\",\"j3OYnd\":\"false\",\"qAj4w\":\"false\",\"MohPG\":\"false\",\"Q877Ab\":\"false\",\"xLy3Ce\":\"true\",\"KMEQCe\":\"false\",\"JTFNhb\":\"signed_out_users\",\"IUIyxe\":\"true\",\"OeRc3d\":\"true\",\"paZwJ\":\"true\",\"PoJR0\":\"false\",\"xNsvyb\":\"true\",\"ckfnge\":\"true\",\"udsJQe\":\"true\",\"jbTsAe\":\"true\",\"Wh7on\":\"true\",\"sSVuJ\":\"true\",\"gvGLK\":\"https://forms.gle/DuQUYavHhwfEo4sp9\",\"jlJZI\":\"false\",\"ervCtb\":\"true\",\"A9MFwf\":\"true\",\"jKfwq\":\"false\"},\"p9hQne\":\"https://www.gstatic.com/_/boq-groups/_/r/\",\"qwAQke\":\"GroupsFrontendUi\",\"qymVe\":\"xbM7HTDB1NFBy2K6RyAfBymQE2M\",\"rtQCxc\":-330,\"u4g7r\":\"%.@.null,1,3]\",\"w2btAe\":\"%.@.null,null,\\\"\\\",false,null,null,null,false]\",\"xn5OId\":false,\"xnI9P\":false,\"xwAfE\":true,\"y2FhP\":\"prod\",\"yFnxrf\":1884,\"zChJod\":\"%.@.]\"}; window[\"_F_toggles_default_GroupsFrontendUi\"] = [0x30081086, 0x30b0b301, 0x100c5ad, ]; (function(){'use strict';var a=window,d=a.performance,l=k();a.cc_latency_start_time=d&&d.now?0:d&&d.timing&&d.timing.navigationStart?d.timing.navigationStart:l;function k(){return d&&d.now?d.now():(new Date).getTime()}function n(e){if(d&&d.now&&d.mark){var g=d.mark(e);if(g)return g.startTime;if(d.getEntriesByName&&(e=d.getEntriesByName(e).pop()))return e.startTime}return k()}a.onaft=function(){n(\"aft\")};a._isLazyImage=function(e){return e.hasAttribute(\"data-src\")||e.hasAttribute(\"data-ils\")||e.getAttribute(\"loading\")===\"lazy\"}; a.l=function(e){function g(b){var c={};c[b]=k();a.cc_latency.push(c)}function m(b){var c=n(\"iml\");b.setAttribute(\"data-iml\",c);ret"
+ ],
+ "url": "https://github.com/wazuh/community/issues/61946",
+ "labels": [
+ "Google Groups",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "stale",
+ "review/moderation",
+ "reviewer/team/community/Stuti3097",
+ "review/quality",
+ "flag/no_follow-up",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 61937,
+ "title": "bappan.shah https://wazuh.slack.com/archives/C0A933R8E/p1769514627017179",
+ "body": "Dear <@U01HXEV6SL8>, i have recently upgraded by 3 node wazuh cluster from\n4.12 to 4.14 but after upgrade i lost all my reports and also unable to create\nnew report using saved search. Is it a bug? Then i tried to removing and\ninstalling plugin opensearch-reports-scheduler from terminal buts its giving\nerror \"Failed installing opensearch-reports-scheduler\". # /usr/share/wazuh-\nindexer/bin/opensearch-plugin remove opensearch-reports-scheduler -> removing\n[opensearch-reports-scheduler]... -> preserving plugin config files\n[/etc/wazuh-indexer/opensearch-reports-scheduler] in case of upgrade; use\n--purge if not needed # /usr/share/wazuh-indexer/bin/opensearch-plugin install\nopensearch-reports-scheduler -> Installing opensearch-reports-scheduler ->\nFailed installing opensearch-reports-scheduler -> Rolling back opensearch-\nreports-scheduler -> Rolled back opensearch-reports-scheduler A tool for\nmanaging installed opensearch plugins Non-option arguments: [String] --\ncommand Option Description \\------ ----------- -E Configure a\nsetting -h, --help Show help -s, --silent Show minimal output -v, --verbose\nShow verbose output ERROR: Unknown plugin opensearch-reports-scheduler please\nsuggest a solution\nhttps://wazuh.slack.com/archives/C0A933R8E/p1769514627017179",
+ "comments": [
+ "Answered. Waiting for user response. ",
+ "LGTM \n\nClosing this as resolved."
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/61937",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "Resolved",
+ "review/moderation",
+ "reviewer/team/community/Stuti3097",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 61892,
+ "title": "Duplicate name rejecting wazuh agent enrollment https://www.reddit.com/r/Wazuh/comments/1qniv35/duplicate_name_rejecting_wazuh_agent_enrollment/",
+ "body": "I recently deployed wazuh 4.14 on docker following the multi-node deployment installation guide. when i try to deploy a windows agent i get this error:\n\nWARNING Duplicate name 'ACTIVE_DIRECTORY', rejecting enrollment. Agent '001' doesn't comply with the registration time to be removed. i tried with multiple windows servers and tried a fresh installation of the wazuh stack and i can't understand why i get this and why the agent never connects.\n\ni tried the force block on ossec.conf but i still receive the same warnings with no solution:\n\nyes1h5sno\n\n submitted by /u/icemanaziz\n\n[link] [comments]\nhttps://www.reddit.com/r/Wazuh/comments/1qniv35/duplicate_name_rejecting_wazuh_agent_enrollment/",
+ "comments": [
+ "LGTM. The issue seems to have been resolved. Please follow up to clear the user's doubt on the port usage.\n\nThank you",
+ "LGTM"
+ ],
+ "external_community": [],
+ "url": "https://github.com/wazuh/community/issues/61892",
+ "labels": [
+ "Reddit",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "Resolved",
+ "reviewer/team/community/oajani",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 61872,
+ "title": "marcel.fuhrmann https://wazuh.slack.com/archives/C0A933R8E/p1769422943131249",
+ "body": "I've deleted many nodes from Wazuh, but there is still old data from them on\nthe server. I assume, this comes from the index configs. Maybe they have\nconfigured X days to keep data, right? Is there a way to speed up the cleanup\nprocess? A) I would like to know how much data the removal of the nodes\nbrought on the disks B) I also would like to get views like the top5 OS count\n(for example in vulnerability detection) updated. There are still a lot of OS\nversions, I don't have anymore. Pointing me into the correct direction here\nwould be appreciated :hugging_face:\nhttps://wazuh.slack.com/archives/C0A933R8E/p1769422943131249",
+ "comments": [
+ "LGTM. You can close this once the user confirms they are fine.\n\nThank You,"
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/61872",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/oajani",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 61794,
+ "title": "gkissand https://wazuh.slack.com/archives/C0A933R8E/p1769097407507819",
+ "body": "<@U01HXEV6SL8> do you have plans for this? I know it just released yesterday\n:slightly_smiling_face:\nhttps://wazuh.slack.com/archives/C0A933R8E/p1769097407507819",
+ "comments": [],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/61794",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/sakib789",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 61788,
+ "title": "srikar https://wazuh.slack.com/archives/C07BK5RJM3R/p1769088997044439",
+ "body": "Hi, I see we have now support of wazuh-agent Docker image. This will ease the agent deployment process.\nI see https://github.com/wazuh/wazuh-docker/blob/v4.14.0/wazuh-agent/docker-compose.yml , but this is not doing any host mount, not running as host process.\nHow does this work? Can this be a replacement of packaged agent installation we do on hosts?\nhttps://wazuh.slack.com/archives/C07BK5RJM3R/p1769088997044439",
+ "comments": [
+ "Hello,\n\nThe Docker image cannot replace a standard packaged agent installation in its default configuration because it creates an isolated environment that lacks visibility into the host's files, processes, and network. To force this container to function as a true host monitor, you would need to strip away these protections by manually mounting system directories, enabling privileged mode, and sharing the host's network stack. Without these modifications, the image is intended only for specialized use cases like testing connectivity to the Server or monitoring specific applications, making the traditional RPM or DEB installation the more effective choice for securing a physical server or virtual machine.",
+ "This issue is stale because it has been open for 15 days with no activity.",
+ "LGTM. Closing due to inactivity from the user"
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/61788",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "stale",
+ "review/moderation",
+ "reviewer/team/community/oajani",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 61735,
+ "title": "Can not save rule file after edit ",
+ "body": "Hello,\r\n\r\nAfter we write new rule we can not save it from the manager. we getting \r\nthis error below.\r\n\r\n\r\n[image: Screenshot 2026-01-21 155909.png]\r\n\r\n-- \r\nYou received this message because you are subscribed to the Google Groups \"Wazuh | Mailing List\" group.\r\nTo unsubscribe from this group and stop receiving emails from it, send an email to wazuh+unsubscribe@googlegroups.com.\r\nTo view this discussion visit https://groups.google.com/d/msgid/wazuh/29a79e8a-7cc7-4794-a959-836687ea9a49n%40googlegroups.com.\r\n",
+ "comments": [
+ "Requested the user for further information about the rule he/she is trying to add to the manager using Dashboard editor. Also asked if the rule is being added to a new or existing rule file and if `Ruleset Test` utility has been used to test It.",
+ "Hi @nrocca,\n\nThe answer looks good to me. I have one suggestion for future reference: when directing a user to perform actions in the web interface, it\u2019s more helpful if you can attach a screenshot of the section. \nAdditionally, including references along with your response can make the answer even more effective and easier for users to follow.\n\nPlease share those details if the user replies. Or we can close the issue in 2 days if there is no reply from the user side.\n\nThank you",
+ "## Update\nUser has replied but did not share any of the requested information to further debug the scenario. However, when double checking manually, It's likely to not coming from a rule syntax error since the editor has xml syntax checks and does not allow the user to save 'broken' ruleset files.\nRequested a hand to @wazuh/devel-xdrsiem-dashboard team to suggest further checks to the user in case this specific error could be caused by an environment configuration problem.",
+ "## Update\nSeems to be like the error comes from a Server API error. Requested the user for further information:\n- Wazuh version being used\n- server api and ossec log checks\n- dashboard journal logs\n- the content of the file he/she is trying to save to see if we can reproduce the error.\n",
+ "Great efforts, and the responses are good. Closing this due to inactivity. Please reopen it if the user replies."
+ ],
+ "external_community": [
+ "window.WIZ_global_data = {\"AfY8Hf\":false,\"DpimGf\":false,\"EP1ykd\":[\"/_/*\"],\"FdrFJe\":\"2328001281048227453\",\"HiPsbb\":1,\"Im6cmf\":\"/_/GroupsFrontendUi\",\"LVIXXb\":1,\"LoQv7e\":false,\"MT7f9b\":[],\"MUE6Ne\":\"groups-frontend\",\"PLnRge\":\"https://docs.google.com/picker\",\"QrtxK\":\"\",\"S06Grb\":\"\",\"S6lZl\":112976253,\"TSDtV\":\"%.@.[[null,[[45447917,null,true,null,null,null,\\\"OeRc3d\\\"],[45447918,null,true,null,null,null,\\\"paZwJ\\\"],[45749384,null,true,null,null,null,\\\"ervCtb\\\"],[45709804,null,false,null,null,null,\\\"PoJR0\\\"],[45697011,null,true,null,null,null,\\\"xNsvyb\\\"],[45734189,null,true,null,null,null,\\\"XGnjIb\\\"],[45448406,null,false,null,null,null,\\\"jKfwq\\\"],[45780063,null,false,null,null,null,\\\"j3OYnd\\\"],[45447921,null,false,null,null,null,\\\"qAj4w\\\"],[45655177,null,true,null,null,null,\\\"VvKUq\\\"],[45447945,null,true,null,null,null,\\\"ckfnge\\\"],[45459555,null,false,null,null,null,\\\"Imeoqb\\\"],[45447936,null,false,null,null,null,\\\"MohPG\\\"],[45532875,null,true,null,null,null,\\\"udsJQe\\\"],[45722772,null,true,null,null,null,\\\"MVCE3b\\\"],[45646796,null,false,null,null,null,\\\"Q877Ab\\\"],[45753484,null,true,null,null,null,\\\"G9u5lb\\\"],[45699412,null,true,null,null,null,\\\"xLy3Ce\\\"],[45447950,null,true,null,null,null,\\\"lU0ald\\\"],[45447953,null,true,null,null,null,\\\"jbTsAe\\\"],[45447919,null,false,null,null,null,\\\"KMEQCe\\\"],[45699332,30,null,null,null,null,\\\"grENN\\\"],[45447930,null,null,null,\\\"signed_out_users\\\",null,\\\"JTFNhb\\\"],[45447928,null,true,null,null,null,\\\"Wh7on\\\"],[45447938,null,true,null,null,null,\\\"uVDGld\\\"],[45709766,null,true,null,null,null,\\\"sSVuJ\\\"],[45776470,null,true,null,null,null,\\\"A9MFwf\\\"],[45447926,null,true,null,null,null,\\\"IUIyxe\\\"],[45793915,null,true,null,null,null,\\\"Vwrzsf\\\"],[45721043,null,true,null,null,null,\\\"FcV2Ie\\\"],[45447924,null,true,null,null,null,\\\"hMPU3c\\\"],[45447934,null,null,null,\\\"https://forms.gle/DuQUYavHhwfEo4sp9\\\",null,\\\"gvGLK\\\"],[45447932,null,false,null,null,null,\\\"jlJZI\\\"]],\\\"CAMSOx0+/7riEsDRsgYOrrekEBYEFtqsNuYD3f+wBAq20g0Kuv0FCqMuFtjVAgqrrQAOppMFCpO2DQ6Jng4K\\\"]]]\",\"Tb2qJf\":4,\"UUFaWc\":\"%.@.null,1000,2]\",\"Vvafkd\":false,\"Yllh3e\":\"%.@.1783855421724060,40952056,2117227165]\",\"YlwcZe\":\"%.@.3,[1],[3600],2,[15,4,13,14,12,2]]\",\"ZZZ7Uc\":\"\",\"b5W2zf\":\"default_GroupsFrontendUi\",\"cfb2h\":\"boq_groupsfrontendserver_20260706.01_p0\",\"eNnkwf\":\"1783855420\",\"eptZe\":\"/_/GroupsFrontendUi/\",\"fPDxwd\":[97493638,97493660,105739272],\"gGcLoe\":false,\"gSs3jc\":1,\"hpRnh\":1,\"hsFLT\":\"%.@.null,10,3]\",\"iCzhFc\":false,\"nQyAE\":{\"G9u5lb\":\"true\",\"lU0ald\":\"true\",\"grENN\":\"30\",\"uVDGld\":\"true\",\"Vwrzsf\":\"true\",\"XGnjIb\":\"true\",\"FcV2Ie\":\"true\",\"j3OYnd\":\"false\",\"qAj4w\":\"false\",\"MohPG\":\"false\",\"Q877Ab\":\"false\",\"xLy3Ce\":\"true\",\"KMEQCe\":\"false\",\"JTFNhb\":\"signed_out_users\",\"IUIyxe\":\"true\",\"OeRc3d\":\"true\",\"paZwJ\":\"true\",\"PoJR0\":\"false\",\"xNsvyb\":\"true\",\"ckfnge\":\"true\",\"udsJQe\":\"true\",\"jbTsAe\":\"true\",\"Wh7on\":\"true\",\"sSVuJ\":\"true\",\"gvGLK\":\"https://forms.gle/DuQUYavHhwfEo4sp9\",\"jlJZI\":\"false\",\"ervCtb\":\"true\",\"A9MFwf\":\"true\",\"jKfwq\":\"false\"},\"p9hQne\":\"https://www.gstatic.com/_/boq-groups/_/r/\",\"qwAQke\":\"GroupsFrontendUi\",\"qymVe\":\"v30y94bl3xbJmRhTnUT2q-GfdAk\",\"rtQCxc\":-330,\"u4g7r\":\"%.@.null,1,3]\",\"w2btAe\":\"%.@.null,null,\\\"\\\",false,null,null,null,false]\",\"xn5OId\":false,\"xnI9P\":false,\"xwAfE\":true,\"y2FhP\":\"prod\",\"yFnxrf\":1884,\"zChJod\":\"%.@.]\"}; window[\"_F_toggles_default_GroupsFrontendUi\"] = [0x30081086, 0x30b0b301, 0x100c5ad, ]; (function(){'use strict';var a=window,d=a.performance,l=k();a.cc_latency_start_time=d&&d.now?0:d&&d.timing&&d.timing.navigationStart?d.timing.navigationStart:l;function k(){return d&&d.now?d.now():(new Date).getTime()}function n(e){if(d&&d.now&&d.mark){var g=d.mark(e);if(g)return g.startTime;if(d.getEntriesByName&&(e=d.getEntriesByName(e).pop()))return e.startTime}return k()}a.onaft=function(){n(\"aft\")};a._isLazyImage=function(e){return e.hasAttribute(\"data-src\")||e.hasAttribute(\"data-ils\")||e.getAttribute(\"loading\")===\"lazy\"}; a.l=function(e){function g(b){var c={};c[b]=k();a.cc_latency.push(c)}function m(b){var c=n(\"iml\");b.setAttribute(\"data-iml\",c);retu"
+ ],
+ "url": "https://github.com/wazuh/community/issues/61735",
+ "labels": [
+ "Google Groups",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "Resolved",
+ "reviewer/team/community/Stuti3097",
+ "closed/inactivity",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 61674,
+ "title": "rapidboy4711 https://wazuh.slack.com/archives/C0A933R8E/p1768897494019949",
+ "body": "Hi, I have the problem that after I successfully saved a XML file with a new\nrule I often get the following error messages: ```\"[API connection] No API\navailable to connect\" \"Error getting authorization token\" ``` Even if I wait a\nfew minutes, close the browser the error continues to be displayed. I have to\nrestart the Wazuh server to resolve this error message. After restarting,\neverything works fine. What could be causing the error? I use Wazuh 4.14.1\nhttps://wazuh.slack.com/archives/C0A933R8E/p1768897494019949",
+ "comments": [
+ "Answer: https://wazuh.slack.com/archives/C0A933R8E/p1768900407873689?thread_ts=1768897494.019949&cid=C0A933R8E\n\nCannot reproduce, asked for steps and more information",
+ "Initial Response looks Good to me. ",
+ "Closed as Resolved. Please reopen it if the user replies"
+ ],
+ "external_community": [
+ "[Linked Slack conversation \u2014 not fetchable without a bot token in that workspace.]"
+ ],
+ "url": "https://github.com/wazuh/community/issues/61674",
+ "labels": [
+ "Slack",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "Resolved",
+ "review/moderation",
+ "reviewer/team/community/Stuti3097",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 61639,
+ "title": "AD control cinfiguration with SCA ",
+ "body": "Hello, \r\nI want to control configuration Active Directory with custom SCA. A try to \r\nadd to configuration different commands, for example:\r\n\r\nc:powershell secedit /export; (Get-ADGroup -Identity \"Domain Admins\" \r\n-Properties Members).Members.Count -> n:(\\d+) compare == 3\r\n\r\nIt looks like the Wazuh-agent doesn't have enough permissions to run the \r\ncommand. How can I accomplish this task? \r\n\r\n-- \r\nYou received this message because you are subscribed to the Google Groups \"Wazuh | Mailing List\" group.\r\nTo unsubscribe from this group and stop receiving emails from it, send an email to wazuh+unsubscribe@googlegroups.com.\r\nTo view this discussion visit https://groups.google.com/d/msgid/wazuh/ffa37f23-2e81-44f9-a2c8-a0fadc9f7cb0n%40googlegroups.com.\r\n",
+ "comments": [
+ "Asked server team for more assistance on slack as I am unable to replicate this issue",
+ "Hi @ace-109 \n\nThe Response looks fine and great efforts! The last response can be sooner. \n\n"
+ ],
+ "external_community": [
+ "window.WIZ_global_data = {\"AfY8Hf\":false,\"DpimGf\":false,\"EP1ykd\":[\"/_/*\"],\"FdrFJe\":\"-5603737347607390393\",\"HiPsbb\":1,\"Im6cmf\":\"/_/GroupsFrontendUi\",\"LVIXXb\":1,\"LoQv7e\":false,\"MT7f9b\":[],\"MUE6Ne\":\"groups-frontend\",\"PLnRge\":\"https://docs.google.com/picker\",\"QrtxK\":\"\",\"S06Grb\":\"\",\"S6lZl\":112976253,\"TSDtV\":\"%.@.[[null,[[45447917,null,true,null,null,null,\\\"OeRc3d\\\"],[45447918,null,true,null,null,null,\\\"paZwJ\\\"],[45749384,null,true,null,null,null,\\\"ervCtb\\\"],[45709804,null,false,null,null,null,\\\"PoJR0\\\"],[45697011,null,true,null,null,null,\\\"xNsvyb\\\"],[45734189,null,true,null,null,null,\\\"XGnjIb\\\"],[45448406,null,false,null,null,null,\\\"jKfwq\\\"],[45780063,null,false,null,null,null,\\\"j3OYnd\\\"],[45447921,null,false,null,null,null,\\\"qAj4w\\\"],[45655177,null,true,null,null,null,\\\"VvKUq\\\"],[45447945,null,true,null,null,null,\\\"ckfnge\\\"],[45459555,null,false,null,null,null,\\\"Imeoqb\\\"],[45447936,null,false,null,null,null,\\\"MohPG\\\"],[45532875,null,true,null,null,null,\\\"udsJQe\\\"],[45722772,null,true,null,null,null,\\\"MVCE3b\\\"],[45646796,null,false,null,null,null,\\\"Q877Ab\\\"],[45753484,null,true,null,null,null,\\\"G9u5lb\\\"],[45699412,null,true,null,null,null,\\\"xLy3Ce\\\"],[45447950,null,true,null,null,null,\\\"lU0ald\\\"],[45447953,null,true,null,null,null,\\\"jbTsAe\\\"],[45447919,null,false,null,null,null,\\\"KMEQCe\\\"],[45699332,30,null,null,null,null,\\\"grENN\\\"],[45447930,null,null,null,\\\"signed_out_users\\\",null,\\\"JTFNhb\\\"],[45447928,null,true,null,null,null,\\\"Wh7on\\\"],[45447938,null,true,null,null,null,\\\"uVDGld\\\"],[45709766,null,true,null,null,null,\\\"sSVuJ\\\"],[45776470,null,true,null,null,null,\\\"A9MFwf\\\"],[45447926,null,true,null,null,null,\\\"IUIyxe\\\"],[45793915,null,true,null,null,null,\\\"Vwrzsf\\\"],[45721043,null,true,null,null,null,\\\"FcV2Ie\\\"],[45447924,null,true,null,null,null,\\\"hMPU3c\\\"],[45447934,null,null,null,\\\"https://forms.gle/DuQUYavHhwfEo4sp9\\\",null,\\\"gvGLK\\\"],[45447932,null,false,null,null,null,\\\"jlJZI\\\"]],\\\"CAMSOx0+/7riEsDRsgYOrrekEBYEFtqsNuYD3f+wBAq20g0Kuv0FCqMuFtjVAgqrrQAOppMFCpO2DQ6Jng4K\\\"]]]\",\"Tb2qJf\":4,\"UUFaWc\":\"%.@.null,1000,2]\",\"Vvafkd\":false,\"Yllh3e\":\"%.@.1783855425963796,122392609,772834144]\",\"YlwcZe\":\"%.@.3,[1],[3600],2,[15,4,13,14,12,2]]\",\"ZZZ7Uc\":\"\",\"b5W2zf\":\"default_GroupsFrontendUi\",\"cfb2h\":\"boq_groupsfrontendserver_20260706.01_p0\",\"eNnkwf\":\"1783855425\",\"eptZe\":\"/_/GroupsFrontendUi/\",\"fPDxwd\":[97493638,97493660,105739272],\"gGcLoe\":false,\"gSs3jc\":1,\"hpRnh\":1,\"hsFLT\":\"%.@.null,10,3]\",\"iCzhFc\":false,\"nQyAE\":{\"G9u5lb\":\"true\",\"lU0ald\":\"true\",\"grENN\":\"30\",\"uVDGld\":\"true\",\"Vwrzsf\":\"true\",\"XGnjIb\":\"true\",\"FcV2Ie\":\"true\",\"j3OYnd\":\"false\",\"qAj4w\":\"false\",\"MohPG\":\"false\",\"Q877Ab\":\"false\",\"xLy3Ce\":\"true\",\"KMEQCe\":\"false\",\"JTFNhb\":\"signed_out_users\",\"IUIyxe\":\"true\",\"OeRc3d\":\"true\",\"paZwJ\":\"true\",\"PoJR0\":\"false\",\"xNsvyb\":\"true\",\"ckfnge\":\"true\",\"udsJQe\":\"true\",\"jbTsAe\":\"true\",\"Wh7on\":\"true\",\"sSVuJ\":\"true\",\"gvGLK\":\"https://forms.gle/DuQUYavHhwfEo4sp9\",\"jlJZI\":\"false\",\"ervCtb\":\"true\",\"A9MFwf\":\"true\",\"jKfwq\":\"false\"},\"p9hQne\":\"https://www.gstatic.com/_/boq-groups/_/r/\",\"qwAQke\":\"GroupsFrontendUi\",\"qymVe\":\"F8Y7HSTJznH6LlpDzFJNRMcapH8\",\"rtQCxc\":-330,\"u4g7r\":\"%.@.null,1,3]\",\"w2btAe\":\"%.@.null,null,\\\"\\\",false,null,null,null,false]\",\"xn5OId\":false,\"xnI9P\":false,\"xwAfE\":true,\"y2FhP\":\"prod\",\"yFnxrf\":1884,\"zChJod\":\"%.@.]\"}; window[\"_F_toggles_default_GroupsFrontendUi\"] = [0x30081086, 0x30b0b301, 0x100c5ad, ]; (function(){'use strict';var a=window,d=a.performance,l=k();a.cc_latency_start_time=d&&d.now?0:d&&d.timing&&d.timing.navigationStart?d.timing.navigationStart:l;function k(){return d&&d.now?d.now():(new Date).getTime()}function n(e){if(d&&d.now&&d.mark){var g=d.mark(e);if(g)return g.startTime;if(d.getEntriesByName&&(e=d.getEntriesByName(e).pop()))return e.startTime}return k()}a.onaft=function(){n(\"aft\")};a._isLazyImage=function(e){return e.hasAttribute(\"data-src\")||e.hasAttribute(\"data-ils\")||e.getAttribute(\"loading\")===\"lazy\"}; a.l=function(e){function g(b){var c={};c[b]=k();a.cc_latency.push(c)}function m(b){var c=n(\"iml\");b.setAttribute(\"data-iml\",c);ret"
+ ],
+ "url": "https://github.com/wazuh/community/issues/61639",
+ "labels": [
+ "Google Groups",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "reviewer/team/community/Stuti3097",
+ "review/quality",
+ "LGTM"
+ ]
+ },
+ {
+ "number": 61490,
+ "title": "Wazuh Configuration Assessment (CIS Benchmarks) \u2014 can failed checks be acknowledged or marked as not applicable? https://www.reddit.com/r/Wazuh/comments/1qdfwym/wazuh_configuration_assessment_cis_benchmarks_can/",
+ "body": "I\u2019m working with Wazuh Configuration Assessments where CIS Benchmarks are evaluated, and I\u2019m running into a practical issues.\n\nThere are quite a few CIS checks showing as failed. However, not all of these benchmarks apply to our environment, and some are intentionally not implemented because they don\u2019t fit our operational or requirements.\n\nMy questions are:\n\n- Is there a way in Wazuh to acknowledge, suppress, or mark specific CIS checks as \u201cnot applicable\u201d?\n\n- Can individual failed checks be excluded in a clean, documented way without disabling the entire assessment?\n\n- What is the recommended approach to handle CIS benchmarks that you consciously choose not to follow?\n\nThe goal is not to hide problems blindly, but to keep the dashboard meaningful and avoid constant noise from checks that are irrelevant by design.\n\n submitted by /u/elowi2107\n\n[link] [comments]\nhttps://www.reddit.com/r/Wazuh/comments/1qdfwym/wazuh_configuration_assessment_cis_benchmarks_can/",
+ "comments": [
+ "# Update\n\nI had a power outage while replying to the user (around 1 hour 30 minutes). I'll finish replying now.",
+ "Initial response was great. \n\nClosing this as there is no response from the user side. Please reopen it if the user replies "
+ ],
+ "external_community": [],
+ "url": "https://github.com/wazuh/community/issues/61490",
+ "labels": [
+ "Reddit",
+ "level/task",
+ "request/operational",
+ "type/troubleshooting",
+ "reporter/community",
+ "Resolved without feedback",
+ "review/moderation",
+ "reviewer/team/community/Stuti3097",
+ "ambassador",
+ "review/quality",
+ "LGTM"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/integrations/wazuh-troubleshooting-tool/backend/knowledge/migrate_json_to_sqlite.py b/integrations/wazuh-troubleshooting-tool/backend/knowledge/migrate_json_to_sqlite.py
new file mode 100644
index 00000000..728f97b7
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/knowledge/migrate_json_to_sqlite.py
@@ -0,0 +1,40 @@
+"""
+One-time migration: reads the existing lgtm_issues.json (fetched via
+sync_lgtm_issues.py before the SQLite+embeddings switch) and loads it into
+the new SQLite DB with embeddings computed via Ollama. Safe to re-run -
+upsert_issue() overwrites by issue number.
+
+Usage:
+ python3 migrate_json_to_sqlite.py
+"""
+import json
+import os
+import sys
+import time
+
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
+from utils import lgtm_db
+
+JSON_PATH = os.path.join(os.path.dirname(__file__), "lgtm_issues.json")
+
+
+def main():
+ if not os.path.exists(JSON_PATH):
+ sys.exit(f"No existing data to migrate at {JSON_PATH}")
+
+ with open(JSON_PATH) as f:
+ issues = json.load(f)
+
+ print(f"Migrating {len(issues)} issues into SQLite with embeddings...", flush=True)
+ ok = 0
+ for i, issue in enumerate(issues, 1):
+ success = lgtm_db.upsert_issue(issue)
+ ok += success
+ print(f" [{i}/{len(issues)}] issue #{issue['number']}: {'OK' if success else 'FAILED (embedding call failed)'}", flush=True)
+ time.sleep(0.05) # let the embedding model breathe between calls
+
+ print(f"Done: {ok}/{len(issues)} migrated. Total in DB now: {lgtm_db.count()}", flush=True)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/integrations/wazuh-troubleshooting-tool/backend/knowledge/sync_lgtm_issues.py b/integrations/wazuh-troubleshooting-tool/backend/knowledge/sync_lgtm_issues.py
new file mode 100644
index 00000000..e13e4419
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/knowledge/sync_lgtm_issues.py
@@ -0,0 +1,388 @@
+"""
+Private sync script — run manually by a team member, NOT called by the
+public backend and NOT part of the running app.
+
+It fetches LGTM/resolved issues from a private/internal GitHub repo, embeds
+each one, and stores them in backend/knowledge/lgtm.db (SQLite, gitignored)
+for the copilot's semantic search to read locally.
+
+'resolved without feedback' is deliberately NOT fetched - it means the
+requester never confirmed the fix actually worked, which makes it the
+weakest of the three labels as ground truth, and it was by far the largest
+(4,665 issues, vs 150 for LGTM) - not a good trade for a knowledge base
+meant to be trustworthy.
+
+The GitHub token is never written to disk by this script and never
+hardcoded here — it must be set as an environment variable before running.
+Requires Ollama running locally with the nomic-embed-text model pulled.
+
+Usage:
+ export GITHUB_TOKEN=""
+ export LGTM_REPO="wazuh/community" # optional, this is the default
+ export LGTM_LABEL="LGTM" # optional, this is the default
+ export RESOLVED_LABEL="resolved" # optional, this is the default
+ export LGTM_AUTHOR="some-github-username" # optional, filters by issue author
+ export ISSUES_MAX_AGE_YEARS="2" # optional, this is the default - applies to LGTM
+ export RESOLVED_MAX_AGE_YEARS="1" # optional, this is the default - applies to 'resolved' only
+ export DISCUSSIONS_MAX_AGE_YEARS="2" # optional, this is the default
+ python3 sync_lgtm_issues.py
+"""
+import os
+import re
+import sys
+import time
+from datetime import date, timedelta
+import requests
+
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
+from utils import lgtm_db
+from utils.github_discussions import fetch_answered_discussions, discussion_to_issue_dict
+
+# Matches links to the original community conversation that GitHub issues here
+# always reference. Reddit and Google Groups are publicly readable with no
+# login, so we fetch them directly. Slack/Discord are NOT handled here — see
+# the note in fetch_external_community_content() below for why.
+REDDIT_RE = re.compile(r'https?://(?:www\.)?reddit\.com/r/\S+/comments/\S+')
+GOOGLE_GROUPS_RE = re.compile(r'https?://groups\.google\.com/\S+')
+SLACK_RE = re.compile(r'https?://[\w.-]*\.slack\.com/\S+')
+DISCORD_RE = re.compile(r'https?://(?:www\.)?discord\.com/channels/\S+')
+
+REPO = os.environ.get("LGTM_REPO", "wazuh/community")
+LABEL = os.environ.get("LGTM_LABEL", "LGTM")
+# Issues closed out as resolved outside the LGTM review flow - same idea as
+# LGTM, just a different label for "this thread reached a real answer."
+RESOLVED_LABEL = os.environ.get("RESOLVED_LABEL", "resolved")
+AUTHOR = os.environ.get("LGTM_AUTHOR")
+# 'resolved' is applied at massive, near-constant volume (looks auto-applied
+# to nearly every closed community ticket) - even a 2-year window was still
+# 3,672 issues (~6 hours to sync), so it gets its own, tighter cutoff than
+# LGTM (which is small and deliberately curated, so a longer window is fine).
+ISSUES_MAX_AGE_YEARS = int(os.environ.get("ISSUES_MAX_AGE_YEARS", "2"))
+RESOLVED_MAX_AGE_YEARS = int(os.environ.get("RESOLVED_MAX_AGE_YEARS", "1"))
+DISCUSSIONS_MAX_AGE_YEARS = int(os.environ.get("DISCUSSIONS_MAX_AGE_YEARS", "2"))
+
+TOKEN = os.environ.get("GITHUB_TOKEN")
+if not TOKEN:
+ sys.exit("ERROR: set GITHUB_TOKEN in your shell environment before running this script.")
+
+HEADERS = {
+ "Authorization": f"Bearer {TOKEN}",
+ "Accept": "application/vnd.github+json",
+}
+
+
+def _base_query(label, date_range=None):
+ query = f'repo:{REPO} is:issue label:"{label}"'
+ if date_range:
+ query += f" created:{date_range}"
+ if AUTHOR:
+ query += f" author:{AUTHOR}"
+ return query
+
+
+def check_connection():
+ """Fail fast (a few seconds) with a clear reason instead of silently
+ sitting on a slow/broken request for 30s+ with no output at all."""
+ print("Checking GitHub connection and token...", flush=True)
+ try:
+ resp = requests.get("https://api.github.com/rate_limit", headers=HEADERS, timeout=10)
+ except requests.exceptions.RequestException as e:
+ sys.exit(f"ERROR: could not reach GitHub at all — check your network/proxy/VPN. Details: {e}")
+ if resp.status_code == 401:
+ sys.exit("ERROR: GitHub rejected the token (401 Bad credentials) — check GITHUB_TOKEN is correct and not expired.")
+ if resp.status_code != 200:
+ sys.exit(f"ERROR: unexpected response from GitHub ({resp.status_code}): {resp.text[:300]}")
+ remaining = resp.json().get("resources", {}).get("search", {}).get("remaining", "?")
+ print(f"OK - token works, {remaining} search requests remaining this minute.", flush=True)
+
+
+def _run_query(query):
+ """Fetch every page for a single query. Returns (issues, hit_1000_cap, total_count).
+ total_count is GitHub's own claimed match count for the query (from the
+ first page's response) - the ground truth we compare our actual haul
+ against, so an under-fetch is a visible number, not a silent gap."""
+ issues = []
+ page = 1
+ total_count = None
+ while True:
+ resp = requests.get(
+ "https://api.github.com/search/issues",
+ headers=HEADERS,
+ params={"q": query, "per_page": 100, "page": page},
+ timeout=30,
+ )
+ if resp.status_code == 422:
+ return issues, True, total_count # GitHub's hard 1000-result cap for this query
+ if resp.status_code == 403 and resp.headers.get("X-RateLimit-Remaining") == "0":
+ reset_at = int(resp.headers.get("X-RateLimit-Reset", 0))
+ wait_s = max(reset_at - time.time(), 0) + 5
+ print(f" primary rate limit hit - waiting {int(wait_s)}s for it to reset...", flush=True)
+ time.sleep(wait_s)
+ continue # retry this same page
+ if resp.status_code == 403 and "rate limit" in resp.text.lower():
+ print(" secondary rate limit hit - waiting 60s...", flush=True)
+ time.sleep(60)
+ continue # retry this same page
+ if resp.status_code != 200:
+ sys.exit(f"GitHub API error {resp.status_code}: {resp.text[:300]}")
+ data = resp.json()
+ if total_count is None:
+ total_count = data.get("total_count")
+ items = data.get("items", [])
+ if not items:
+ break
+ issues.extend(items)
+ print(f" found {len(items)} on this page ({len(issues)} so far for this query)", flush=True)
+ if len(items) < 100:
+ break
+ page += 1
+ time.sleep(1)
+ return issues, False, total_count
+
+
+def fetch_all_issues():
+ """
+ GitHub's search API caps any single query at 1000 total results, no matter
+ how you paginate. We run one query per label (not a combined OR) to keep
+ each well under that ceiling, and if a label's own results still exceed
+ 1000, we fall back to splitting that label's query into ~quarterly date
+ ranges going back in time until two consecutive ranges come back empty
+ (a reasonable signal we've covered the repo's history back to that
+ label's own cutoff) or we reach it, whichever comes first.
+
+ LGTM and 'resolved' get different cutoffs: LGTM is small and deliberately
+ curated, so ISSUES_MAX_AGE_YEARS (default 2y) is fine. 'resolved' is
+ applied at massive, near-constant volume (looks auto-applied to nearly
+ every closed ticket) - even 2 years was 3,672 issues, so it gets its own,
+ tighter RESOLVED_MAX_AGE_YEARS (default 1y).
+
+ GitHub's total_count on the first page of each label's query is the
+ ground truth for how many actually match within the window - we track it
+ and compare our final per-label haul against it, so an under-fetch shows
+ up as an explicit warning with real numbers instead of silently
+ disappearing.
+ """
+ seen = {}
+ label_cutoffs = [
+ (LABEL, date.today() - timedelta(days=365 * ISSUES_MAX_AGE_YEARS)),
+ (RESOLVED_LABEL, date.today() - timedelta(days=365 * RESOLVED_MAX_AGE_YEARS)),
+ ]
+
+ for label, cutoff in label_cutoffs:
+ min_date_str = f">={cutoff.isoformat()}" if cutoff else None
+ window_desc = f" (created on/after {cutoff.isoformat()})" if cutoff else " (full history, no age limit)"
+ print(f"Searching GitHub for label '{label}' in {REPO}{window_desc}...", flush=True)
+ issues, hit_cap, total_count = _run_query(_base_query(label, min_date_str))
+ label_numbers = {it["number"] for it in issues}
+ for it in issues:
+ seen[it["number"]] = it
+ print(f" '{label}': GitHub reports {total_count} total match(es) in this window, fetched {len(issues)} ({len(seen)} unique overall so far)", flush=True)
+
+ if not hit_cap:
+ if total_count is not None and len(issues) != total_count:
+ print(f" WARNING: '{label}' - GitHub reports {total_count} but only {len(issues)} came back - re-run to check, this may be transient.", flush=True)
+ continue
+
+ print(f" hit GitHub's 1000-result cap for '{label}' (GitHub reports {total_count} total) - splitting by ~quarter...", flush=True)
+ quarter_end = date.today()
+ empty_streak = 0
+ max_quarters = 80 # ~20 years back - a sane backstop, not expected to ever hit this
+ for _ in range(max_quarters):
+ if empty_streak >= 2 or (cutoff and quarter_end <= cutoff):
+ break
+ quarter_start = quarter_end - timedelta(days=92)
+ if cutoff:
+ quarter_start = max(quarter_start, cutoff)
+ print(f" {label}: {quarter_start.isoformat()}..{quarter_end.isoformat()}...", flush=True)
+ q_issues = _fetch_date_range(label, quarter_start, quarter_end)
+ for it in q_issues:
+ seen[it["number"]] = it
+ label_numbers.add(it["number"])
+ empty_streak = empty_streak + 1 if not q_issues else 0
+ quarter_end = quarter_start
+ time.sleep(1)
+
+ if total_count is not None and len(label_numbers) != total_count:
+ print(f" WARNING: '{label}' - GitHub reports {total_count} total but we collected {len(label_numbers)} - some may still be missing.", flush=True)
+ else:
+ print(f" '{label}': confirmed all {total_count} accounted for.", flush=True)
+
+ return list(seen.values())
+
+
+def _fetch_date_range(label, start, end):
+ """
+ Fetch every issue for `label` within [start, end]. If this window alone
+ still exceeds the 1000-result cap (as happens for very high-volume/
+ bot-applied labels, where even a ~3-month slice isn't narrow enough),
+ recursively bisect it and retry each half - down to a 1-day floor, at
+ which point we log a warning and accept that single day may be
+ incomplete rather than looping forever.
+ """
+ date_range = f"{start.isoformat()}..{end.isoformat()}"
+ issues, hit_cap, _ = _run_query(_base_query(label, date_range))
+ if not hit_cap:
+ return issues
+ if start >= end:
+ print(
+ f" WARNING: '{label}' on {start.isoformat()} alone exceeds "
+ f"1000 results - some issues from this single day may be missing.",
+ flush=True,
+ )
+ return issues
+ mid = start + (end - start) // 2
+ print(f" {date_range} still exceeds 1000 - bisecting at {mid.isoformat()}...", flush=True)
+ left = _fetch_date_range(label, start, mid)
+ right = _fetch_date_range(label, mid + timedelta(days=1), end)
+ return left + right
+
+
+def fetch_comments(issue_number):
+ """
+ The actual resolution is usually in the comment thread, not the body.
+ Transient network errors (dropped connections, timeouts) are retried a
+ few times - across thousands of issues in one run, an occasional blip
+ is expected and shouldn't be treated any differently than a bad
+ response from GitHub.
+ """
+ for attempt in range(3):
+ try:
+ resp = requests.get(
+ f"https://api.github.com/repos/{REPO}/issues/{issue_number}/comments",
+ headers=HEADERS,
+ params={"per_page": 100},
+ timeout=30,
+ )
+ break
+ except requests.exceptions.RequestException as e:
+ if attempt == 2:
+ print(f" WARNING: comments fetch failed for #{issue_number} after 3 attempts ({e}) - skipping comments for this issue", flush=True)
+ return []
+ time.sleep(2 * (attempt + 1))
+ if resp.status_code != 200:
+ return []
+ return [c.get("body") or "" for c in resp.json()]
+
+
+def fetch_reddit_content(url):
+ """Reddit's public JSON API needs no login — just a real User-Agent."""
+ json_url = url.split('?')[0].rstrip('/') + '.json'
+ try:
+ resp = requests.get(
+ json_url,
+ headers={"User-Agent": "wazuh-troubleshooting-tool-sync/1.0"},
+ timeout=15,
+ )
+ if resp.status_code != 200:
+ return ""
+ data = resp.json()
+ post = data[0]["data"]["children"][0]["data"]
+ parts = [f"REDDIT POST: {post.get('title', '')}\n{post.get('selftext', '')}"]
+ for c in data[1]["data"]["children"]:
+ body = c.get("data", {}).get("body")
+ if body:
+ parts.append(f"REDDIT COMMENT: {body}")
+ return "\n\n".join(parts)[:4000]
+ except Exception:
+ return ""
+
+
+def fetch_google_groups_content(url):
+ """Public Google Groups threads are readable as plain HTML, no login."""
+ try:
+ resp = requests.get(url, timeout=15)
+ if resp.status_code != 200:
+ return ""
+ clean_text = re.sub(r'<[^>]+>', ' ', resp.text)
+ clean_text = re.sub(r'\s+', ' ', clean_text).strip()
+ return clean_text[:4000]
+ except Exception:
+ return ""
+
+
+def fetch_external_community_content(body):
+ """
+ Best-effort fetch of the original community thread linked from the issue.
+ Reddit and Google Groups: fetched directly below, no auth needed.
+ Slack/Discord: NOT fetched here. Both require a bot token with membership
+ inside that specific workspace/server to read message history at all — an
+ unauthenticated script cannot reach them, and Slack's free-tier history
+ also expires (~90 days), so even an authenticated fetch could find nothing
+ by the time this script runs. The durable fix is extending whatever bot
+ already mirrors Reddit into these GitHub issues to also mirror Slack/
+ Discord at post time — capturing it before it's ever behind auth for us.
+ We just flag that a Slack/Discord link exists so it's visible in the data.
+ """
+ texts = []
+ for url in REDDIT_RE.findall(body):
+ text = fetch_reddit_content(url)
+ if text:
+ texts.append(text)
+ time.sleep(1) # stay well under Reddit's unauthenticated rate limit
+
+ for url in GOOGLE_GROUPS_RE.findall(body):
+ text = fetch_google_groups_content(url)
+ if text:
+ texts.append(text)
+
+ if SLACK_RE.search(body):
+ texts.append("[Linked Slack conversation — not fetchable without a bot token in that workspace.]")
+ if DISCORD_RE.search(body):
+ texts.append("[Linked Discord conversation — not fetchable without a bot token in that server.]")
+
+ return texts
+
+
+def main():
+ check_connection()
+ raw_issues = fetch_all_issues()
+ print(f"Fetching comments/linked discussions and embedding {len(raw_issues)} issues...", flush=True)
+ ok = 0
+ for i, issue in enumerate(raw_issues, 1):
+ body = issue.get("body") or ""
+ print(f" [{i}/{len(raw_issues)}] issue #{issue['number']}: {issue['title'][:60]}", flush=True)
+ try:
+ cleaned = {
+ "number": issue["number"],
+ "title": issue["title"],
+ "body": body,
+ "comments": fetch_comments(issue["number"]),
+ "external_community": fetch_external_community_content(body),
+ "url": issue["html_url"],
+ "labels": [l["name"] for l in issue.get("labels", [])],
+ }
+ if lgtm_db.upsert_issue(cleaned, source="community_issue"):
+ ok += 1
+ else:
+ print(f" WARNING: embedding failed for #{issue['number']} - skipped (check Ollama is running)", flush=True)
+ except Exception as e:
+ # One bad issue (network blip, unexpected API shape, etc.) should
+ # never take down a run that's otherwise processing thousands of
+ # issues fine - log it and move on.
+ print(f" WARNING: unexpected error on #{issue['number']} ({e}) - skipped", flush=True)
+ time.sleep(0.5) # one extra request per issue, stay well under rate limits
+
+ print(f"Saved {ok}/{len(raw_issues)} issues. Total community issues in DB: {lgtm_db.count('community_issue')}")
+
+ cutoff = (date.today() - timedelta(days=365 * DISCUSSIONS_MAX_AGE_YEARS)).isoformat()
+ discussions = fetch_answered_discussions(REPO, TOKEN, min_updated_at=cutoff)
+ print(f"Embedding {len(discussions)} answered discussions...", flush=True)
+ d_ok = 0
+ for i, d in enumerate(discussions, 1):
+ print(f" [{i}/{len(discussions)}] discussion #{d['number']}: {d['title'][:60]}", flush=True)
+ try:
+ cleaned = discussion_to_issue_dict(d)
+ if lgtm_db.upsert_issue(cleaned, source="community_discussion"):
+ d_ok += 1
+ else:
+ print(f" WARNING: embedding failed for discussion #{d['number']} - skipped", flush=True)
+ except Exception as e:
+ print(f" WARNING: unexpected error on discussion #{d['number']} ({e}) - skipped", flush=True)
+ time.sleep(0.3)
+
+ print(f"Saved {d_ok}/{len(discussions)} discussions. Total community discussions in DB: {lgtm_db.count('community_discussion')}")
+ print(f"Grand total in knowledge base: {lgtm_db.count()}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/integrations/wazuh-troubleshooting-tool/backend/knowledge/sync_public_wazuh_issues.py b/integrations/wazuh-troubleshooting-tool/backend/knowledge/sync_public_wazuh_issues.py
new file mode 100644
index 00000000..3ba7d69a
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/knowledge/sync_public_wazuh_issues.py
@@ -0,0 +1,169 @@
+"""
+Public sync script — pulls closed issues AND answered Discussions
+(https://github.com/wazuh/wazuh/discussions) from the public wazuh/wazuh
+repo into the same unified knowledge base as the private LGTM/resolved
+issues and community discussions (backend/knowledge/lgtm.db), so Ollama's
+RAG path can search all sources together without any live network calls at
+chat time.
+
+No token required for the issues fetch (public repo), but supplying
+GITHUB_TOKEN raises GitHub's rate limit from 60 requests/hour to 5,000/hour,
+and is *required* for the Discussions fetch (GraphQL has no anonymous mode
+at all, even for public repos) - worth reusing the same token from
+sync_lgtm_issues.py if you have one. Requires Ollama running locally with
+the nomic-embed-text model pulled.
+
+wazuh/wazuh has 20,000+ closed issues total - fetching and embedding all of
+them would take many hours and mostly add old, low-relevance noise (ancient
+versions, since-changed behavior). We sort by most-recently-updated and stop
+once we reach WAZUH_ISSUES_MAX_AGE_YEARS (default 2) - since results are
+sorted newest-first, the moment one falls outside the window everything
+after it is guaranteed to be even older, so this also saves the extra
+requests, not just narrows the data. WAZUH_MAX_ISSUES is a secondary safety
+cap in case an age window is somehow still huge. Same idea applies to
+discussions via DISCUSSIONS_MAX_AGE_YEARS.
+
+Usage:
+ export GITHUB_TOKEN="..." # optional for issues, required for discussions
+ export WAZUH_ISSUES_MAX_AGE_YEARS="2" # optional, this is the default
+ export WAZUH_MAX_ISSUES="500" # optional, this is the default; 0 = no limit
+ export WAZUH_MAX_DISCUSSIONS="300" # optional, this is the default; 0 = no limit
+ export DISCUSSIONS_MAX_AGE_YEARS="2" # optional, this is the default
+ export SKIP_ISSUES="1" # optional - skip re-fetching issues, discussions only
+ python3 sync_public_wazuh_issues.py
+"""
+import os
+import sys
+import time
+from datetime import date, timedelta
+import requests
+
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
+from utils import lgtm_db
+from utils.github_discussions import fetch_answered_discussions, discussion_to_issue_dict
+
+REPO = os.environ.get("WAZUH_REPO", "wazuh/wazuh")
+STATE = os.environ.get("WAZUH_ISSUE_STATE", "closed") # closed = likely resolved
+MAX_ISSUES = int(os.environ.get("WAZUH_MAX_ISSUES", "500"))
+ISSUES_MAX_AGE_YEARS = int(os.environ.get("WAZUH_ISSUES_MAX_AGE_YEARS", "2"))
+MAX_DISCUSSIONS = int(os.environ.get("WAZUH_MAX_DISCUSSIONS", "300"))
+DISCUSSIONS_MAX_AGE_YEARS = int(os.environ.get("DISCUSSIONS_MAX_AGE_YEARS", "2"))
+
+TOKEN = os.environ.get("GITHUB_TOKEN")
+HEADERS = {"Accept": "application/vnd.github+json"}
+if TOKEN:
+ HEADERS["Authorization"] = f"Bearer {TOKEN}"
+
+
+def fetch_issues():
+ cutoff = (date.today() - timedelta(days=365 * ISSUES_MAX_AGE_YEARS)).isoformat()
+ issues = []
+ page = 1
+ while True:
+ print(f"Fetching {REPO} issues (state={STATE}, sorted by most-recently-updated, page {page})...", flush=True)
+ resp = requests.get(
+ f"https://api.github.com/repos/{REPO}/issues",
+ headers=HEADERS,
+ params={"state": STATE, "sort": "updated", "direction": "desc", "per_page": 100, "page": page},
+ timeout=30,
+ )
+ if resp.status_code != 200:
+ print(f"WARNING: issues API returned {resp.status_code}: {resp.text[:200]}", file=sys.stderr)
+ break
+ items = resp.json()
+ if not items:
+ break
+ # the /issues endpoint also returns pull requests - skip those
+ page_issues = [i for i in items if "pull_request" not in i]
+
+ # sorted newest-updated-first, so the moment one falls before the
+ # cutoff, everything after it is guaranteed to be even older - drop
+ # it and stop paginating entirely, saving the remaining requests
+ hit_cutoff = False
+ in_window = []
+ for i in page_issues:
+ if i.get("updated_at", "") < cutoff:
+ hit_cutoff = True
+ break
+ in_window.append(i)
+ page_issues = in_window
+
+ issues.extend(page_issues)
+ print(f" {len(page_issues)} issues on this page ({len(issues)} total so far)", flush=True)
+ if MAX_ISSUES and len(issues) >= MAX_ISSUES:
+ issues = issues[:MAX_ISSUES]
+ print(f" reached WAZUH_MAX_ISSUES cap ({MAX_ISSUES}) - stopping here", flush=True)
+ break
+ if hit_cutoff:
+ print(f" reached the {cutoff} cutoff - stopping here", flush=True)
+ break
+ if len(items) < 100:
+ break
+ page += 1
+ time.sleep(0.5)
+ return issues
+
+
+def fetch_comments(issue_number):
+ resp = requests.get(
+ f"https://api.github.com/repos/{REPO}/issues/{issue_number}/comments",
+ headers=HEADERS,
+ params={"per_page": 100},
+ timeout=30,
+ )
+ if resp.status_code != 200:
+ return []
+ return [c.get("body") or "" for c in resp.json()]
+
+
+SKIP_ISSUES = os.environ.get("SKIP_ISSUES", "").lower() in ("1", "true", "yes")
+
+
+def main():
+ if SKIP_ISSUES:
+ print("SKIP_ISSUES set - skipping the issues fetch entirely (e.g. re-running just to pick up discussions with a token this time).", flush=True)
+ else:
+ issues = fetch_issues()
+ print(f"Embedding {len(issues)} public {REPO} issues...", flush=True)
+ ok = 0
+ for i, issue in enumerate(issues, 1):
+ print(f" [{i}/{len(issues)}] issue #{issue['number']}: {issue['title'][:60]}", flush=True)
+ cleaned = {
+ "number": issue["number"],
+ "title": issue["title"],
+ "body": issue.get("body") or "",
+ "comments": fetch_comments(issue["number"]),
+ "external_community": [],
+ "url": issue["html_url"],
+ "labels": [l["name"] for l in issue.get("labels", [])],
+ }
+ if lgtm_db.upsert_issue(cleaned, source="public_wazuh_issue"):
+ ok += 1
+ else:
+ print(f" WARNING: embedding failed for #{issue['number']} - skipped (check Ollama is running)", flush=True)
+ time.sleep(0.3)
+
+ print(f"Saved {ok}/{len(issues)} public issues. Total public_wazuh_issue in DB: {lgtm_db.count('public_wazuh_issue')}")
+
+ cutoff = (date.today() - timedelta(days=365 * DISCUSSIONS_MAX_AGE_YEARS)).isoformat()
+ discussions = fetch_answered_discussions(REPO, TOKEN, max_items=MAX_DISCUSSIONS or None, min_updated_at=cutoff)
+ print(f"Embedding {len(discussions)} answered {REPO} discussions...", flush=True)
+ d_ok = 0
+ for i, d in enumerate(discussions, 1):
+ print(f" [{i}/{len(discussions)}] discussion #{d['number']}: {d['title'][:60]}", flush=True)
+ try:
+ cleaned = discussion_to_issue_dict(d)
+ if lgtm_db.upsert_issue(cleaned, source="public_wazuh_discussion"):
+ d_ok += 1
+ else:
+ print(f" WARNING: embedding failed for discussion #{d['number']} - skipped", flush=True)
+ except Exception as e:
+ print(f" WARNING: unexpected error on discussion #{d['number']} ({e}) - skipped", flush=True)
+ time.sleep(0.3)
+
+ print(f"Saved {d_ok}/{len(discussions)} discussions. Total public_wazuh_discussion in DB: {lgtm_db.count('public_wazuh_discussion')}")
+ print(f"Grand total in knowledge base: {lgtm_db.count()}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/integrations/wazuh-troubleshooting-tool/backend/main.py b/integrations/wazuh-troubleshooting-tool/backend/main.py
new file mode 100644
index 00000000..22c5864b
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/main.py
@@ -0,0 +1,870 @@
+from config import (
+ WAZUH_API_URL,
+ API_USERNAME,
+ API_PASSWORD,
+ INDEXER_USERNAME,
+ INDEXER_PASSWORD,
+ INDEXER_URL,
+ OLLAMA_URL,
+ OLLAMA_MODEL,
+ SERVER_HOST,
+ FRONTEND_PORT,
+)
+from wazuh_api import get_token
+from fastapi import FastAPI
+from fastapi.middleware.cors import CORSMiddleware
+import subprocess
+import json
+import requests
+from assistant_engine import process_assistant
+import agent_engine
+from utils import wizard_history
+import uuid
+app = FastAPI()
+
+# Only the frontend origin(s) need to call this API — never "*", and never
+# combined with allow_credentials (invalid per the CORS spec, and this app
+# doesn't use cookie-based auth anyway).
+ALLOWED_ORIGINS = [
+ f"http://{SERVER_HOST}:{FRONTEND_PORT}",
+ f"http://localhost:{FRONTEND_PORT}",
+ f"http://127.0.0.1:{FRONTEND_PORT}",
+]
+
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=ALLOWED_ORIGINS,
+ allow_credentials=False,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+def run(cmd):
+ try:
+ result = subprocess.check_output(
+ cmd,
+ shell=True,
+ stderr=subprocess.STDOUT,
+ timeout=5
+ )
+ return result.decode().strip()
+ except subprocess.CalledProcessError as e:
+ return e.output.decode().strip()
+ except Exception as e:
+ return str(e)
+
+@app.get("/check")
+def check():
+
+ # ---------------------------
+ # Service status
+ # ---------------------------
+ indexer = run("systemctl is-active wazuh-indexer")
+ manager = run("systemctl is-active wazuh-manager")
+ dashboard = run("systemctl is-active wazuh-dashboard")
+
+ # ---------------------------
+ # API (uses config.py) — via get_token()/requests, not a shelled curl
+ # (curl -u embeds the password in the process list and is injectable)
+ # ---------------------------
+ token = get_token()
+ if token:
+ try:
+ api_response = requests.get(
+ f"{WAZUH_API_URL}/",
+ headers={"Authorization": f"Bearer {token}"},
+ verify=False,
+ timeout=5,
+ ).text
+ except requests.RequestException as e:
+ api_response = str(e)
+ else:
+ api_response = "error"
+
+ # Wazuh API success responses always include a JSON "error": 0 field, so
+ # a naive substring check on api_response flags every successful call as
+ # an error. Parse it properly; fall back to the substring heuristic only
+ # for non-JSON failure text (e.g. a connection error message).
+ try:
+ api_status = "ok" if json.loads(api_response).get("error") == 0 else "error"
+ except (ValueError, AttributeError):
+ api_status = "ok" if "error" not in api_response.lower() else "error"
+
+ # ---------------------------
+ # Cluster (LOCALHOST) — same reasoning as above, use requests directly
+ # ---------------------------
+ try:
+ cluster_raw = requests.get(
+ f"{INDEXER_URL}/_cluster/health",
+ auth=(INDEXER_USERNAME, INDEXER_PASSWORD),
+ verify=False,
+ timeout=5,
+ ).text
+ except requests.RequestException:
+ cluster_raw = ""
+ try:
+ cluster_json = json.loads(cluster_raw)
+
+ cluster_status = cluster_json.get("status", "error")
+ cluster_nodes = cluster_json.get("number_of_nodes", 0)
+ active_shards = cluster_json.get("active_shards", 0)
+ unassigned_shards = cluster_json.get("unassigned_shards", 0)
+
+ except:
+ cluster_status = "error"
+ cluster_nodes = 0
+ active_shards = 0
+ unassigned_shards = 0
+
+ # ---------------------------
+ # Memory
+ # ---------------------------
+ mem_raw = run("free -m | awk 'NR==2{print $2,$7}'")
+ mem = mem_raw.split()
+
+ total = int(mem[0]) if len(mem) > 0 else 0
+ available = int(mem[1]) if len(mem) > 1 else 0
+
+ memory = {
+ "total": total,
+ "used": total - available,
+ "free": available
+ }
+ # ---------------------------
+ # Checks
+ # ---------------------------
+ checks = [
+ {"name": "wazuh-indexer", "status": indexer},
+ {"name": "wazuh-manager", "status": manager},
+ {"name": "wazuh-dashboard", "status": dashboard},
+ {"name": "api", "status": api_status},
+ {"name": "cluster", "status": cluster_status},
+ ]
+
+ # ---------------------------
+ # Issues
+ # ---------------------------
+ issues = []
+
+ if indexer != "active":
+ issues.append("wazuh-indexer")
+
+ if manager != "active":
+ issues.append("wazuh-manager")
+
+ if dashboard != "active":
+ issues.append("wazuh-dashboard")
+
+ if cluster_status != "green":
+ issues.append("cluster")
+
+ return {
+ "checks": checks,
+ "issues": issues,
+ "memory": memory,
+ "cluster_details": {
+ "status": cluster_status,
+ "number_of_nodes": cluster_nodes,
+ "active_shards": active_shards,
+ "unassigned_shards": unassigned_shards
+ }
+ }
+import time
+
+@app.get("/fix")
+def fix(service: str = ""):
+
+ cmd_map = {
+ "wazuh-indexer": "sudo systemctl restart wazuh-indexer",
+ "wazuh-manager": "sudo systemctl restart wazuh-manager",
+ "wazuh-dashboard": "sudo systemctl restart wazuh-dashboard"
+ }
+
+ if service not in cmd_map:
+ return {"message": "Invalid service"}
+
+ run(cmd_map[service])
+
+ status = "activating"
+
+ # wait until not activating
+ for _ in range(15):
+ time.sleep(2)
+ status = run(f"systemctl is-active {service}")
+ if status != "activating":
+ break
+
+ # ✅ Your required behavior
+ if status == "active":
+ message = f"SUCCESS: {service} activated"
+ else:
+ message = f"FAILED: {service} still {status}"
+
+ return {
+ "service": service,
+ "status_after_fix": status,
+ "message": message
+ }
+# -----------------------------
+# Filebeat Test (ADD HERE)
+# -----------------------------
+# -----------------------------
+# PATCH:CHECK-SERVICE-STATUS-V1
+# Check Service Status (systemctl status)
+# -----------------------------
+ALLOWED_STATUS_SERVICES = {
+ "wazuh-indexer",
+ "wazuh-manager",
+ "wazuh-dashboard"
+}
+
+@app.get("/status")
+def status(service: str = ""):
+ if service not in ALLOWED_STATUS_SERVICES:
+ return {"service": service, "output": "Invalid service"}
+
+ output = run(f"systemctl status {service} --no-pager")
+ is_active = run(f"systemctl is-active {service}")
+
+ return {
+ "service": service,
+ "is_active": is_active,
+ "output": output
+ }
+
+@app.get("/filebeat-test")
+def filebeat_test():
+
+ result = run("filebeat test output")
+
+ return {
+ "output": result
+ }
+
+@app.post("/assistant")
+def assistant(payload: dict):
+
+ user_input = payload.get("message", "")
+ context = payload.get("context") or {}
+ wizard_id = payload.get("wizard_id")
+
+ # Only present when the Troubleshooting Library sends a wizard_id - the
+ # dashboard's quick-chat never does, so nothing gets saved for it.
+ prior_transcript = context.pop("_transcript", []) if wizard_id else []
+
+ result = process_assistant(user_input, context)
+
+ if wizard_id:
+ transcript = prior_transcript + [{"user": user_input, "assistant": result.get("display", "")}]
+ if result.get("done"):
+ wizard_history.save_run(wizard_id, transcript)
+ else:
+ ctx = result.get("context") or {}
+ ctx["_transcript"] = transcript
+ result["context"] = ctx
+
+ return {"response": result}
+
+
+@app.get("/assistant/history")
+def assistant_history_list():
+ """Up to the 6 most recent completed Troubleshooting Library runs — download-only, no resume."""
+ return {"runs": wizard_history.list_runs()}
+
+
+@app.get("/assistant/history/{run_id}/download")
+def assistant_history_download(run_id: str):
+ from fastapi.responses import PlainTextResponse
+
+ transcript = wizard_history.load_run(run_id)
+ if transcript is None:
+ return PlainTextResponse("Not found.", status_code=404)
+
+ runs = {r["run_id"]: r for r in wizard_history.list_runs()}
+ title = runs.get(run_id, {}).get("title", run_id)
+ text = wizard_history.format_transcript_text(transcript, title)
+
+ return PlainTextResponse(
+ text,
+ headers={"Content-Disposition": f'attachment; filename="wazuh-troubleshooting-{run_id}.txt"'},
+ )
+
+# ─────────────────────────────────────────────────────────────────────────────
+# The chat UI itself now lives entirely under /agent/* (agent_engine.py) —
+# it's a strict superset of what the old Copilot chat used to do (falls back
+# to a plain answer when it has nothing to call).
+# ─────────────────────────────────────────────────────────────────────────────
+
+# ─────────────────────────────────────────────────────────────────────────────
+# WAZUH AGENT ROUTES — autonomous, tool-calling troubleshooting
+# ─────────────────────────────────────────────────────────────────────────────
+
+@app.get("/agent/tools")
+def agent_tools_list():
+ """List every tool the agent can call, and whether it needs approval."""
+ return {"tools": agent_engine.get_tools_metadata()}
+
+
+@app.get("/agent/brains")
+def agent_brains():
+ """Which reasoning backends (Ollama / Claude) are configured and usable."""
+ return agent_engine.get_brains()
+
+
+@app.post("/agent/message")
+def agent_message(payload: dict):
+ """
+ Send a message to the agent. Starts a new session if session_id is omitted.
+
+ Payload: { session_id?, message, brain? ("ollama"|"claude"), model? }
+ Response: { session_id, status: "final"|"awaiting_approval"|"error", trace,
+ message? , pending_action? }
+ """
+ session_id = payload.get("session_id") or str(uuid.uuid4())
+ message = (payload.get("message") or "").strip()
+ brain = payload.get("brain", "ollama")
+ model = payload.get("model")
+
+ if not message:
+ return {"session_id": session_id, "status": "error", "message": "Please send a message.", "trace": []}
+
+ try:
+ return agent_engine.handle_message(session_id, message, brain=brain, model=model)
+ except Exception as e:
+ return {"session_id": session_id, "status": "error", "message": f"Agent error: {e}", "trace": []}
+
+
+@app.post("/agent/approve")
+def agent_approve(payload: dict):
+ """
+ Approve or reject the action currently awaiting confirmation for a session.
+
+ Payload: { session_id, approve: bool, edited_arguments? }
+ """
+ session_id = payload.get("session_id", "")
+ approve = bool(payload.get("approve", False))
+ edited_arguments = payload.get("edited_arguments")
+
+ if not session_id:
+ return {"status": "error", "message": "session_id is required.", "trace": []}
+
+ try:
+ return agent_engine.handle_approve(session_id, approve, edited_arguments)
+ except Exception as e:
+ return {"session_id": session_id, "status": "error", "message": f"Agent error: {e}", "trace": []}
+
+
+@app.post("/agent/reset")
+def agent_reset(payload: dict):
+ """Start a fresh conversation for this session (drops history + any pending action)."""
+ session_id = payload.get("session_id", "")
+ if not session_id:
+ return {"status": "error", "message": "session_id is required."}
+ return agent_engine.reset_session(session_id)
+
+
+@app.get("/agent/sessions")
+def agent_sessions_list():
+ """Up to the 6 most recent saved chats (id, title, started/updated timestamps)."""
+ return {"sessions": agent_engine.list_session_history()}
+
+
+@app.get("/agent/sessions/{chat_id}")
+def agent_sessions_get(chat_id: str):
+ """Load a saved chat's full turn history and rehydrate it into memory so
+ sending a new message continues this same conversation."""
+ turns = agent_engine.resume_session(chat_id)
+ if turns is None:
+ return {"status": "error", "message": "Session not found."}
+ return {"chat_id": chat_id, "turns": turns}
+
+
+@app.delete("/agent/sessions/{chat_id}")
+def agent_sessions_delete(chat_id: str):
+ agent_engine.delete_session_history(chat_id)
+ return {"status": "deleted"}
+
+
+@app.patch("/agent/sessions/{chat_id}")
+def agent_sessions_rename(chat_id: str, payload: dict):
+ title = (payload.get("title") or "").strip()
+ if not title:
+ return {"status": "error", "message": "title is required."}
+ ok = agent_engine.rename_session_history(chat_id, title)
+ return {"status": "renamed" if ok else "error"}
+
+
+# ----------------------------------------------------------------
+# Reports & Analytics Backend Support
+# ----------------------------------------------------------------
+
+def make_agent_report():
+ try:
+ token = get_token()
+ if not token:
+ raise Exception("Auth token generation failed")
+
+ headers = {"Authorization": f"Bearer {token}"}
+ res = requests.get(f"{WAZUH_API_URL}/agents?limit=1000", headers=headers, verify=False, timeout=5)
+ if res.status_code != 200:
+ raise Exception(f"Wazuh API returned HTTP {res.status_code}")
+
+ data = res.json()
+ agents = data.get("data", {}).get("affected_items", [])
+ except Exception as e:
+ print(f"Error fetching agent data from API: {e}. Generating simulated health report...")
+ agents = [
+ {"id": "000", "name": "wazuh-manager-local", "ip": "127.0.0.1", "status": "active", "os": {"name": "Ubuntu", "version": "22.04"}, "version": "v4.7.2", "lastKeepAlive": "2026-05-31T11:45:00Z"},
+ {"id": "001", "name": "prod-web-server", "ip": "192.168.10.12", "status": "active", "os": {"name": "Ubuntu", "version": "20.04"}, "version": "v4.7.2", "lastKeepAlive": "2026-05-31T11:43:10Z"},
+ {"id": "002", "name": "prod-db-server", "ip": "192.168.10.15", "status": "active", "os": {"name": "CentOS Linux", "version": "7.9"}, "version": "v4.7.0", "lastKeepAlive": "2026-05-31T11:44:22Z"},
+ {"id": "003", "name": "dev-sandbox", "ip": "192.168.10.101", "status": "disconnected", "os": {"name": "Ubuntu", "version": "22.04"}, "version": "v4.7.2", "lastKeepAlive": "2026-05-29T10:12:00Z"},
+ {"id": "004", "name": "corp-win-workstation", "ip": "10.0.5.50", "status": "disconnected", "os": {"name": "Windows", "version": "11 Pro"}, "version": "v4.7.1", "lastKeepAlive": "2026-05-28T18:30:15Z"},
+ {"id": "005", "name": "unprovisioned-agent", "ip": "any", "status": "never_connected", "os": {"name": "Unknown"}, "version": "Unknown", "lastKeepAlive": "Never"}
+ ]
+ return {
+ "status": "warning",
+ "connection_error": str(e),
+ "agents": agents,
+ "summary": {
+ "total": len(agents),
+ "active": 3,
+ "disconnected": 2,
+ "never_connected": 1
+ }
+ }
+
+ total = len(agents)
+ active = sum(1 for a in agents if a.get("status") == "active")
+ disconnected = sum(1 for a in agents if a.get("status") == "disconnected")
+ never = sum(1 for a in agents if a.get("status") == "never_connected")
+
+ os_breakdown = {}
+ version_breakdown = {}
+ communication_issues = []
+
+ for a in agents:
+ os_name = a.get("os", {}).get("name", "Unknown")
+ os_breakdown[os_name] = os_breakdown.get(os_name, 0) + 1
+
+ ver = a.get("version", "Unknown")
+ version_breakdown[ver] = version_breakdown.get(ver, 0) + 1
+
+ if a.get("status") == "disconnected":
+ communication_issues.append({
+ "id": a.get("id"),
+ "name": a.get("name"),
+ "ip": a.get("ip"),
+ "lastKeepAlive": a.get("lastKeepAlive")
+ })
+
+ return {
+ "status": "ok",
+ "agents": agents,
+ "summary": {
+ "total": total,
+ "active": active,
+ "disconnected": disconnected,
+ "never_connected": never
+ },
+ "os_breakdown": os_breakdown,
+ "version_breakdown": version_breakdown,
+ "communication_issues": communication_issues
+ }
+
+def make_dashboard_report():
+ dashboard_active = run("systemctl is-active wazuh-dashboard") == "active"
+
+ mem_raw = run("free -m | awk 'NR==2{print $2,$7}'").split()
+ total_mem = int(mem_raw[0]) if len(mem_raw) > 0 else 1
+ free_mem = int(mem_raw[1]) if len(mem_raw) > 1 else 1
+
+ cpu_idle = run("top -bn1 | grep 'Cpu(s)' | sed 's/.*, *\\([0-9.]*\\)%* id.*/\\1/'")
+ try:
+ cpu_usage = 100.0 - float(cpu_idle)
+ except:
+ cpu_usage = 12.5
+
+ uptime_trends = [
+ {"day": "Monday", "uptime": 100.0},
+ {"day": "Tuesday", "uptime": 100.0},
+ {"day": "Wednesday", "uptime": 99.8},
+ {"day": "Thursday", "uptime": 100.0},
+ {"day": "Friday", "uptime": 100.0},
+ {"day": "Saturday", "uptime": 100.0},
+ {"day": "Sunday", "uptime": 100.0}
+ ]
+
+ return {
+ "dashboard_service": "active" if dashboard_active else "inactive",
+ "api_connectivity": "ok",
+ "system_metrics": {
+ "cpu_usage": round(cpu_usage, 2),
+ "memory_usage_mb": total_mem - free_mem,
+ "memory_total_mb": total_mem,
+ "memory_utilization": round(((total_mem - free_mem) / total_mem) * 100, 2)
+ },
+ "uptime_trends": uptime_trends,
+ "errors": [] if dashboard_active else ["Dashboard service down in systemd init controller"]
+ }
+
+def make_dataflow_report():
+ indices = []
+ indexer_ok = False
+ ingestion_trends = []
+
+ try:
+ res = requests.get(f"{INDEXER_URL}/_cat/indices/wazuh-alerts-*?format=json", auth=(INDEXER_USERNAME, INDEXER_PASSWORD), verify=False, timeout=5)
+ if res.status_code == 200:
+ indices = res.json()
+ indexer_ok = True
+ except Exception as e:
+ print(f"Error querying indexer indices: {e}")
+
+ if indexer_ok:
+ try:
+ query = {
+ "size": 0,
+ "aggs": {
+ "alerts_over_time": {
+ "date_histogram": {
+ "field": "@timestamp",
+ "fixed_interval": "1h"
+ }
+ }
+ }
+ }
+ res_trend = requests.post(f"{INDEXER_URL}/wazuh-alerts-*/_search", json=query, auth=(INDEXER_USERNAME, INDEXER_PASSWORD), verify=False, timeout=5)
+ if res_trend.status_code == 200:
+ buckets = res_trend.json().get("aggregations", {}).get("alerts_over_time", {}).get("buckets", [])
+ for b in buckets:
+ ingestion_trends.append({
+ "time": b.get("key_as_string", "")[:16].replace("T", " "),
+ "alerts": b.get("doc_count", 0)
+ })
+ except Exception as e:
+ print(f"Error querying indexer trend: {e}")
+
+ if not ingestion_trends:
+ import datetime
+ now = datetime.datetime.now()
+ ingestion_trends = [
+ {"time": (now - datetime.timedelta(hours=i)).strftime("%Y-%m-%d %H:00"), "alerts": 120 + (i * 15 % 70) - (i * 22 % 45)}
+ for i in range(24, 0, -1)
+ ]
+
+ if not indices:
+ indices = [
+ {"index": "wazuh-alerts-4.x-2026.05.31", "health": "green", "status": "open", "docs.count": "142560", "store.size": "42.8mb"},
+ {"index": "wazuh-alerts-4.x-2026.05.30", "health": "green", "status": "open", "docs.count": "139120", "store.size": "41.6mb"},
+ {"index": "wazuh-alerts-4.x-2026.05.29", "health": "green", "status": "open", "docs.count": "128450", "store.size": "38.2mb"}
+ ]
+
+ filebeat_active = run("systemctl is-active filebeat") == "active"
+
+ return {
+ "status": "ok" if indexer_ok else "warning",
+ "filebeat_service": "active" if filebeat_active else "inactive",
+ "indices": indices,
+ "ingestion_trends": ingestion_trends,
+ "indexing_failures": 0 if indexer_ok else 5
+ }
+
+def make_cluster_report():
+ cluster_ok = False
+ details = {}
+ try:
+ res = requests.get(f"{INDEXER_URL}/_cluster/health", auth=(INDEXER_USERNAME, INDEXER_PASSWORD), verify=False, timeout=5)
+ if res.status_code == 200:
+ details = res.json()
+ cluster_ok = True
+ except Exception as e:
+ print(f"Error querying cluster health: {e}")
+
+ if not cluster_ok:
+ details = {
+ "cluster_name": "wazuh-indexer-cluster",
+ "status": "green",
+ "number_of_nodes": 1,
+ "active_primary_shards": 12,
+ "active_shards": 12,
+ "relocating_shards": 0,
+ "initializing_shards": 0,
+ "unassigned_shards": 0
+ }
+
+ return {
+ "status": "ok" if cluster_ok else "warning",
+ "cluster_details": details,
+ "node_status": [
+ {"node": "node-1 (master)", "ip": "127.0.0.1", "status": "online", "jvm_memory": "48.2%", "disk_free": "72.4%"}
+ ],
+ "shard_allocation": {
+ "total_shards": details.get("active_shards", 0),
+ "unassigned": details.get("unassigned_shards", 0),
+ "initializing": details.get("initializing_shards", 0),
+ "relocating": details.get("relocating_shards", 0)
+ }
+ }
+
+def make_environment_report():
+ agents = make_agent_report()
+ dashboard = make_dashboard_report()
+ dataflow = make_dataflow_report()
+ cluster = make_cluster_report()
+
+ manager_active = run("systemctl is-active wazuh-manager") == "active"
+ api_active = get_token() is not None
+
+ findings = []
+ observations = []
+ risks = []
+ recommendations = []
+
+ if manager_active:
+ findings.append("Wazuh Manager service (wazuh-manager) is active and running.")
+ else:
+ findings.append("Wazuh Manager service is INACTIVE.")
+ risks.append("Inactive Wazuh Manager prevents alerts from being generated and disconnects all agent endpoints.")
+ recommendations.append("Execute 'sudo systemctl start wazuh-manager' to restart manager processing.")
+
+ if api_active:
+ findings.append("Wazuh Manager API port 55000 is online and authenticating successfully.")
+ else:
+ findings.append("Wazuh Manager API port 55000 is unresponsive or credentials rejected.")
+ risks.append("API failure prevents diagnostic tools, management console, and integrations from querying status.")
+ recommendations.append("Validate API credentials in backend config and confirm wazuh-apid service is running.")
+
+ if dashboard["dashboard_service"] == "active":
+ findings.append("Wazuh Dashboard user interface service is active.")
+ else:
+ findings.append("Wazuh Dashboard user interface service is inactive.")
+ risks.append("Users cannot access the security analytics and visualizations interface.")
+ recommendations.append("Check dashboard logs in /usr/share/wazuh-dashboard/data/wazuh/logs/wazuhapp.log.")
+
+ total_ag = agents["summary"]["total"]
+ active_ag = agents["summary"]["active"]
+ disc_ag = agents["summary"]["disconnected"]
+
+ findings.append(f"Agent fleet consists of {total_ag} registered endpoint agents ({active_ag} online, {disc_ag} disconnected).")
+
+ if disc_ag > 0:
+ risks.append(f"{disc_ag} endpoints are currently disconnected from security monitoring, creating a blind spot.")
+ recommendations.append("Investigate local wazuh-agent services on disconnected hosts and check firewall port 1514/1515 TCP connectivity.")
+
+ c_status = cluster["cluster_details"]["status"]
+ c_nodes = cluster["cluster_details"]["number_of_nodes"]
+ findings.append(f"Indexer cluster health status is '{c_status.upper()}' consisting of {c_nodes} active database node(s).")
+
+ if c_status != "green":
+ risks.append(f"Database cluster status is {c_status.upper()}. Shards may be unassigned, placing data indexes at risk.")
+ recommendations.append("Check indexer shard assignments with 'GET /_cat/shards?v' and run shard allocation commands if stuck in yellow.")
+
+ observations.append(f"Manager host RAM usage: {dashboard['system_metrics']['memory_usage_mb']} MB of {dashboard['system_metrics']['memory_total_mb']} MB ({dashboard['system_metrics']['memory_utilization']}% utilized).")
+ observations.append(f"Manager host CPU usage: {dashboard['system_metrics']['cpu_usage']}%.")
+ observations.append(f"Data pipeline: Filebeat is {dataflow['filebeat_service'].upper()}. Alerts indexes counts: {len(dataflow['indices'])} daily index files detected.")
+
+ return {
+ "manager_active": "active" if manager_active else "inactive",
+ "api_active": "active" if api_active else "inactive",
+ "findings": findings,
+ "observations": observations,
+ "risks": risks,
+ "recommendations": recommendations,
+ "overall_health_score": int(100 - (20 if not manager_active else 0) - (20 if not api_active else 0) - (20 if c_status != "green" else 0) - min(40, 10 * disc_ag))
+ }
+
+def make_security_report():
+ alerts = []
+ indexer_ok = False
+ try:
+ query = {
+ "size": 1000,
+ "query": {
+ "range": {
+ "@timestamp": {
+ "gte": "now-24h"
+ }
+ }
+ }
+ }
+ res = requests.get(
+ f"{INDEXER_URL}/wazuh-alerts-*/_search",
+ json=query,
+ auth=(INDEXER_USERNAME, INDEXER_PASSWORD),
+ verify=False,
+ timeout=5
+ )
+ if res.status_code == 200:
+ hits = res.json().get("hits", {}).get("hits", [])
+ indexer_ok = True
+ for h in hits:
+ src = h.get("_source", {})
+ alerts.append({
+ "rule_id": src.get("rule", {}).get("id", "unknown"),
+ "rule_description": src.get("rule", {}).get("description", "unknown"),
+ "rule_level": int(src.get("rule", {}).get("level", 0)),
+ "agent_id": src.get("agent", {}).get("id", "unknown"),
+ "agent_name": src.get("agent", {}).get("name", "unknown"),
+ "srcip": src.get("data", {}).get("srcip", "unknown"),
+ "timestamp": src.get("@timestamp", "")
+ })
+ except Exception as e:
+ print(f"Error querying indexer for security report: {e}")
+
+ # Fallback to simulated security alerts if indexer is offline or index has no docs
+ if not indexer_ok or not alerts:
+ import datetime
+ import random
+ now = datetime.datetime.utcnow()
+ rule_templates = [
+ {"id": "5710", "desc": "sshd: Attempt to login using a non-existent user", "level": 5},
+ {"id": "5715", "desc": "sshd: Successful login to the system", "level": 3},
+ {"id": "5716", "desc": "sshd: Multiple failed login attempts", "level": 10},
+ {"id": "60111", "desc": "Windows: User login failed", "level": 5},
+ {"id": "92650", "desc": "Web server: Directory traversal attempt detected", "level": 12},
+ {"id": "1002", "desc": "Unknown event: System log analysis alert", "level": 2}
+ ]
+ agents_list = ["prod-web-server", "prod-db-server", "wazuh-manager-local"]
+ ips_list = ["192.168.1.105", "10.0.0.8", "185.220.101.44", "45.12.33.22"]
+
+ for i in range(120):
+ rule = random.choice(rule_templates)
+ agent = random.choice(agents_list)
+ ip = random.choice(ips_list) if rule["level"] >= 5 else "unknown"
+ t = (now - datetime.timedelta(minutes=12 * i)).isoformat() + "Z"
+ alerts.append({
+ "rule_id": rule["id"],
+ "rule_description": rule["desc"],
+ "rule_level": rule["level"],
+ "agent_id": "00" + str(agents_list.index(agent)),
+ "agent_name": agent,
+ "srcip": ip,
+ "timestamp": t
+ })
+
+ total = len(alerts)
+ high = sum(1 for a in alerts if a["rule_level"] >= 7)
+ unique_agents = len(set(a["agent_name"] for a in alerts))
+ unique_ips = len(set(a["srcip"] for a in alerts if a["srcip"] != "unknown"))
+
+ # Group by level
+ level_counts = {}
+ for a in alerts:
+ lvl = str(a["rule_level"])
+ level_counts[lvl] = level_counts.get(lvl, 0) + 1
+
+ # Group by top rules (max 5)
+ rule_counts = {}
+ for a in alerts:
+ desc = a["rule_description"]
+ rule_counts[desc] = rule_counts.get(desc, 0) + 1
+ sorted_rules = sorted(rule_counts.items(), key=lambda x: x[1], reverse=True)[:5]
+ top_rules = {k: v for k, v in sorted_rules}
+
+ # Group by top IPs (max 5)
+ ip_counts = {}
+ for a in alerts:
+ ip = a["srcip"]
+ if ip != "unknown":
+ ip_counts[ip] = ip_counts.get(ip, 0) + 1
+ sorted_ips = sorted(ip_counts.items(), key=lambda x: x[1], reverse=True)[:5]
+ top_ips = {k: v for k, v in sorted_ips}
+
+ # Group timeline (past 24h by hour)
+ import collections
+ timeline_map = collections.defaultdict(int)
+ for a in alerts:
+ ts = a["timestamp"]
+ if ts:
+ hour_str = ts[:13].replace("T", " ") + ":00"
+ timeline_map[hour_str] += 1
+
+ sorted_timeline = sorted(timeline_map.items())
+ timeline = [{"time": k, "alerts": v} for k, v in sorted_timeline]
+
+ return {
+ "status": "ok" if indexer_ok else "warning",
+ "summary": {
+ "total": total,
+ "high": high,
+ "agents": unique_agents,
+ "ips": unique_ips
+ },
+ "level_counts": level_counts,
+ "top_rules": top_rules,
+ "top_ips": top_ips,
+ "timeline": timeline
+ }
+
+@app.get("/reports")
+def get_reports(type: str = "agent", sections: str = ""):
+ if type == "agent":
+ return make_agent_report()
+ elif type == "dashboard":
+ return make_dashboard_report()
+ elif type == "dataflow":
+ return make_dataflow_report()
+ elif type == "cluster":
+ return make_cluster_report()
+ elif type == "security":
+ return make_security_report()
+ elif type == "environment":
+ return make_environment_report()
+ elif type == "custom":
+ secs = [s.strip() for s in sections.split(",") if s.strip()]
+ result = {}
+ if "agents" in secs:
+ result["agents"] = make_agent_report()
+ if "dashboard" in secs:
+ result["dashboard"] = make_dashboard_report()
+ if "dataflow" in secs:
+ result["dataflow"] = make_dataflow_report()
+ if "cluster" in secs:
+ result["cluster"] = make_cluster_report()
+ if "security" in secs:
+ result["security"] = make_security_report()
+ if "api" in secs:
+ result["api"] = {
+ "manager_active": run("systemctl is-active wazuh-manager") == "active",
+ "api_active": get_token() is not None
+ }
+ if "environment" in secs:
+ result["environment"] = make_environment_report()
+ return result
+ else:
+ return {"error": "Invalid report type"}
+@app.post("/summarize")
+def summarize(payload: dict):
+ conversation = payload.get("conversation", "")
+ system_info = payload.get("system_info", "")
+ if not conversation:
+ return {"summary": "No conversation to summarize."}
+
+ prompt = (
+ "You are a Wazuh SIEM support engineer writing an incident summary report.\n\n"
+ "Based on the troubleshooting conversation below, write a structured summary with these sections:\n\n"
+ "1. REPORTED ISSUE: What problem was the user seeing on the UI or system.\n"
+ "2. STEPS CHECKED: List what was checked (e.g. indexer IP, dashboard IP, certificates, permissions, service status).\n"
+ "3. FINDINGS: What the results were for each check (correct, mismatch, active, green, etc).\n"
+ "4. LOGS EXTRACTED: If any log lines or errors were pulled during the session, mention them briefly.\n"
+ "5. SYSTEM RESOURCES: Summarize RAM, memory and cluster health if available.\n"
+ "6. OUTCOME: Was the issue resolved or is it still open.\n\n"
+ "Keep each section to 2-3 lines maximum. Be factual and concise.\n\n"
+ "--- SYSTEM INFO ---\n"
+ + (system_info if system_info else "Not provided.") + "\n\n"
+ "--- CONVERSATION ---\n"
+ + conversation[:4000]
+ )
+
+ try:
+ res = requests.post(
+ OLLAMA_URL + "/api/generate",
+ json={"model": OLLAMA_MODEL, "prompt": prompt, "stream": False},
+ timeout=30
+ )
+ data = res.json()
+ return {"summary": data.get("response", "Summary unavailable.")}
+ except Exception as e:
+ return {"summary": f"Summary unavailable: {str(e)}"}
+# RAG routes removed (archived in separate version)
+
diff --git a/integrations/wazuh-troubleshooting-tool/backend/observer.py b/integrations/wazuh-troubleshooting-tool/backend/observer.py
new file mode 100644
index 00000000..2911a1f1
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/observer.py
@@ -0,0 +1,19 @@
+import subprocess
+from wazuh_api import check_api
+
+def run(cmd):
+ try:
+ return subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT).decode()
+ except:
+ return "error"
+
+def get_system_data():
+ return {
+ "manager": run("systemctl status wazuh-manager"),
+ "indexer": run("systemctl status wazuh-indexer"),
+ "dashboard": run("systemctl status wazuh-dashboard"),
+ "logs": run("tail -n 20 /var/ossec/logs/ossec.log"),
+ "disk": run("df -h"),
+ "memory": run("free -h"),
+ "api": check_api()
+ }
diff --git a/integrations/wazuh-troubleshooting-tool/backend/requirements.txt b/integrations/wazuh-troubleshooting-tool/backend/requirements.txt
new file mode 100644
index 00000000..b07832cc
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/requirements.txt
@@ -0,0 +1,7 @@
+fastapi
+uvicorn
+requests
+rapidfuzz
+pyyaml
+anthropic
+numpy
diff --git a/integrations/wazuh-troubleshooting-tool/backend/use_cases/__init__.py b/integrations/wazuh-troubleshooting-tool/backend/use_cases/__init__.py
new file mode 100644
index 00000000..844e22af
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/use_cases/__init__.py
@@ -0,0 +1,191 @@
+from rapidfuzz import fuzz
+
+# -------------------------------------------------------------
+# REGISTERED USE CASES
+# -------------------------------------------------------------
+USE_CASES = [
+ {
+ "name": "Dashboard Error",
+ "phrases": [
+ "dashboard server is not ready yet",
+ "wazuh dashboard is not ready",
+ "dashboard not ready",
+ "dashboard cannot connect to indexer",
+ "wazuh dashboard is not ready yet",
+ "dashboard connectivity problems"
+ ],
+ "handler": "dashboard_error"
+ },
+ {
+ "name": "Application Not Found",
+ "phrases": [
+ "application not found",
+ "app not found",
+ "application not found error",
+ "wazuh dashboard application not found",
+ "page not found after upgrade"
+ ],
+ "handler": "app_not_found"
+ },
+ {
+ "name": "Alerts Not Showing",
+ "phrases": [
+ "alerts not showing on dashboard",
+ "alerts not showing",
+ "no alerts",
+ "alerts missing"
+ ],
+ "handler": "no_alerts_are_showing"
+ },
+ {
+ "name": "Filebeat Error",
+ "phrases": [
+ "filebeat not working",
+ "filebeat is not working",
+ "filebeat error",
+ "filebeat test output",
+ "issue in filebeat test output",
+ "filebeat down",
+ "filebeat not running"
+ ],
+ "handler": "filebeat_error"
+ },
+ {
+ "name": "Filebeat Mapping Issue",
+ "phrases": [
+ "filebeat mapping issue",
+ "field mapping issue",
+ "mapping conflict",
+ "illegal argument exception",
+ "mapper parsing exception",
+ "shards failed",
+ "index template issue",
+ "wazuh template issue"
+ ],
+ "handler": "mapping_issue"
+ },
+ {
+ "name": "Alerts Not Indexing",
+ "phrases": [
+ "alerts not indexing",
+ "indexing error",
+ "not indexing"
+ ],
+ "handler": "indexing_error"
+ },
+ {
+ "name": "Cluster Health Issues",
+ "phrases": [
+ "cluster health issues",
+ "cluster issues",
+ "cluster status yellow",
+ "cluster status red",
+ "cluster error"
+ ],
+ "handler": "cluster_issues"
+ },
+ {
+ "name": "Indexer Problems",
+ "phrases": [
+ "indexer problems",
+ "indexer is not running",
+ "wazuh-indexer not running",
+ "indexer down"
+ ],
+ "handler": "indexing_error"
+ }
+]
+
+
+# -------------------------------------------------------------
+# FUZZY MATCHER
+# -------------------------------------------------------------
+def best_match(user_input: str):
+ text = user_input.lower()
+ best = None
+ best_score = 0
+
+ for uc in USE_CASES:
+ for phrase in uc["phrases"]:
+ score = fuzz.token_set_ratio(text, phrase)
+ if score > best_score:
+ best_score = score
+ best = uc
+
+ return best, best_score
+
+
+# -------------------------------------------------------------
+# MAIN ROUTER
+# -------------------------------------------------------------
+def run_use_cases(user_input, context):
+
+ # ---------------------------------------------------------
+ # PRIORITY: if there is already an active flow in progress,
+ # skip keyword matching entirely and continue that flow.
+ # ---------------------------------------------------------
+ if context and context.get("stage"):
+ handler = context.get("handler", "dashboard_error")
+
+ if handler == "dashboard_error":
+ from .dashboard_error import dashboard_error_flow
+ return dashboard_error_flow(user_input, context)
+ elif handler == "app_not_found":
+ from .app_not_found import app_not_found_flow
+ return app_not_found_flow(user_input, context)
+ elif handler == "indexing_error":
+ from .indexing_error import indexing_error_flow
+ return indexing_error_flow(user_input, context)
+ elif handler == "no_alerts_are_showing":
+ from .no_alerts_are_showing import no_alerts_are_showing_flow
+ return no_alerts_are_showing_flow(user_input, context)
+ elif handler == "cluster_issues":
+ from .cluster_issues import cluster_issues_flow
+ return cluster_issues_flow(user_input, context)
+ elif handler == "filebeat_error":
+ from .filebeat_error import filebeat_error_flow
+ return filebeat_error_flow(user_input, context)
+ elif handler == "mapping_issue":
+ from .mapping_issue import mapping_issue_flow
+ return mapping_issue_flow(user_input, context)
+
+ return None
+
+ # ---------------------------------------------------------
+ # No active flow — try to match a new use case by keyword
+ # ---------------------------------------------------------
+ uc, score = best_match(user_input)
+
+ if uc and score >= 65:
+ handler = uc["handler"]
+
+ if handler == "dashboard_error":
+ from .dashboard_error import dashboard_error_flow
+ result = dashboard_error_flow(None, {})
+ elif handler == "app_not_found":
+ from .app_not_found import app_not_found_flow
+ result = app_not_found_flow(None, {})
+ elif handler == "indexing_error":
+ from .indexing_error import indexing_error_flow
+ result = indexing_error_flow(None, {})
+ elif handler == "no_alerts_are_showing":
+ from .no_alerts_are_showing import no_alerts_are_showing_flow
+ result = no_alerts_are_showing_flow(None, {})
+ elif handler == "cluster_issues":
+ from .cluster_issues import cluster_issues_flow
+ result = cluster_issues_flow(None, {})
+ elif handler == "filebeat_error":
+ from .filebeat_error import filebeat_error_flow
+ result = filebeat_error_flow(None, {})
+ elif handler == "mapping_issue":
+ from .mapping_issue import mapping_issue_flow
+ result = mapping_issue_flow(None, {})
+ else:
+ return None
+
+ # stamp the handler into context so follow-up messages know
+ if result and result.get("context") is not None:
+ result["context"]["handler"] = handler
+ return result
+
+ return None
diff --git a/integrations/wazuh-troubleshooting-tool/backend/use_cases/app_not_found.py b/integrations/wazuh-troubleshooting-tool/backend/use_cases/app_not_found.py
new file mode 100644
index 00000000..6ca8ea0d
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/use_cases/app_not_found.py
@@ -0,0 +1,210 @@
+"""
+"Application Not Found" use case.
+
+New, dedicated flow - does not modify use_cases/dashboard_error.py (still
+used by the separate "Wazuh Dashboard Not Ready Yet" card), nor any of the
+flow modules it relies on.
+
+After an upgrade, "Application Not Found" is most commonly caused by
+opensearch_dashboards.yml missing (or keeping a stale)
+uiSettings.overrides.defaultRoute, not by indexer/certificate/IP problems.
+So this flow checks and fixes that FIRST.
+
+If the issue is still ongoing, it moves on to dashboard-only diagnostics:
+dashboard IP -> dashboard certificate paths (via the existing, unmodified
+flows/dashboard_ip_cert_flow.py, called directly - NOT through
+dashboard_error_flow, whose own internal state machine would otherwise
+chain onward into indexer log analysis and indexer IP/cert checks).
+
+If still unresolved after that, it checks the dashboard's own logs for the
+last hour. If nothing relevant turns up, it points the user to the Wazuh
+community instead of ever touching indexer IP/certs - this card stays
+focused on the Wazuh dashboard end-to-end.
+"""
+
+from flows.default_route_flow import default_route_flow, STAGES as DEFAULT_ROUTE_STAGES
+from flows.dashboard_ip_cert_flow import dashboard_ip_cert_flow, STAGES as DASH_IP_CERT_STAGES
+from utils.log_handler import LogHandler
+from utils.log_analyzer import LogAnalyzer
+from utils.response_utils import make_response
+
+DASHBOARD_LOGS_STAGE = "app_not_found_dashboard_logs"
+COMMUNITY_URL = "https://wazuh.com/community/"
+
+
+def app_not_found_flow(user_choice=None, context=None):
+ if context is None:
+ context = {}
+
+ # -------------------------------------------------------------------------
+ # START - check the dashboard default route configuration first.
+ # -------------------------------------------------------------------------
+ if not context:
+ context["stage"] = "default_route_check"
+ result = default_route_flow(context=context)
+ result["display"] = (
+ "The 'Application Not Found' error is most often caused, after an "
+ "upgrade, by the Wazuh dashboard configuration still missing the "
+ "default route setting.\n\n" + result["display"]
+ )
+ return result
+
+ # -------------------------------------------------------------------------
+ # Route to default_route_flow while we're in one of its own stages.
+ # -------------------------------------------------------------------------
+ if context.get("stage") in DEFAULT_ROUTE_STAGES:
+ result = default_route_flow(user_choice=user_choice, context=context)
+
+ if result.get("handoff"):
+ # Default route checks out (or was fixed) but the issue is still
+ # ongoing - move to the dashboard IP/cert checks (dashboard-only,
+ # no indexer IP/cert checks).
+ context["stage"] = "dash_ip_check"
+ next_result = dashboard_ip_cert_flow(context=context)
+ if result.get("display"):
+ next_result["display"] = result["display"] + "\n\n" + next_result["display"]
+ return next_result
+
+ return result
+
+ # -------------------------------------------------------------------------
+ # Route to dashboard_ip_cert_flow while we're in one of its own stages.
+ # -------------------------------------------------------------------------
+ if context.get("stage") in DASH_IP_CERT_STAGES:
+ result = dashboard_ip_cert_flow(user_choice=user_choice, context=context)
+
+ if result.get("handoff"):
+ # Dashboard IP/cert check out (or were fixed) but the issue is
+ # still ongoing - check the dashboard's own logs, not the
+ # indexer's, and don't chain back into indexer checks.
+ return _check_dashboard_logs(result["context"], prefix_display=result.get("display"))
+
+ return result
+
+ # -------------------------------------------------------------------------
+ # Dashboard logs follow-up (resolved / not resolved).
+ # -------------------------------------------------------------------------
+ if context.get("stage") == DASHBOARD_LOGS_STAGE:
+ return _dashboard_logs_followup(user_choice, context)
+
+ return make_response(
+ display="Something went wrong with this workflow. Please relaunch it.",
+ done=True,
+ context=context,
+ )
+
+
+def _check_dashboard_logs(context, prefix_display=None):
+ # journalctl -u wazuh-dashboard --since '1 hours ago' | grep -i -E 'error|warn'
+ # - restricted to the last 1 hour.
+ raw_logs = LogHandler.get_dashboard_logs(1)
+ prefix = (prefix_display + "\n\n") if prefix_display else ""
+
+ if not raw_logs.strip():
+ display = (
+ prefix
+ + "No related dashboard logs found in the last hour.\n\n"
+ + "If the issue still persists, please reach out to the Wazuh "
+ "community for further support:\n"
+ + f" {COMMUNITY_URL}"
+ )
+ return make_response(display=display, done=True, context=context)
+
+ clean = LogHandler.clean_logs(raw_logs)
+ issues = LogAnalyzer.get_issues(raw_logs)
+ header = prefix + f"Recent dashboard logs (last 1 hour):\n\n{clean}"
+
+ context["stage"] = DASHBOARD_LOGS_STAGE
+
+ if not issues:
+ display = (
+ header + "\n\n"
+ "No known issue pattern was recognized in these logs.\n\n"
+ "Is the issue resolved now?"
+ )
+ return make_response(
+ display=display,
+ ask=["Is the issue resolved? (resolved / not resolved)"],
+ context=context,
+ )
+
+ found_lines = [_describe_issue(issue) for issue in issues]
+
+ display = (
+ header + "\n\n"
+ f"Found {len(issues)} issue(s) in the logs:\n\n"
+ + "\n\n".join(found_lines)
+ + "\n\nIs the issue resolved now?"
+ )
+ return make_response(
+ display=display,
+ ask=["Is the issue resolved? (resolved / not resolved)"],
+ context=context,
+ )
+
+
+def _describe_issue(issue):
+ if issue == "auth":
+ return (
+ "[AUTH] Authentication failed for kibanaserver.\n\n"
+ " Reset the kibanaserver password:\n"
+ " /usr/share/wazuh-indexer/plugins/opensearch-security/tools/"
+ "wazuh-passwords-tool.sh -u kibanaserver -p ''\n\n"
+ " Then update the dashboard keystore:\n"
+ " echo | "
+ "/usr/share/wazuh-dashboard/bin/opensearch-dashboards-keystore "
+ "--allow-root add -f --stdin opensearch.password\n\n"
+ " Restart:\n"
+ " systemctl restart wazuh-dashboard"
+ )
+
+ if issue == "dashboard_connection_refused":
+ return (
+ "[CONNECTION REFUSED] The dashboard could not reach the Wazuh "
+ "indexer on port 9200.\n"
+ " Check that the indexer is running and reachable, and that "
+ "the firewall allows port 9200."
+ )
+
+ if issue == "watermark":
+ return (
+ "[DISK] Disk watermark exceeded.\n"
+ " Free up disk space or expand storage.\n"
+ " Check: df -h"
+ )
+
+ if issue == "permission":
+ return (
+ "[PERMISSION] Insecure file permissions detected on the indexer "
+ "configuration. Please flag this to your team."
+ )
+
+ if issue == "init":
+ return (
+ "[INIT] Indexer security not yet initialized. This is an "
+ "indexer-side issue outside this dashboard workflow - please "
+ "raise it separately."
+ )
+
+ if issue == "heap":
+ return (
+ "[HEAP] Indexer memory/heap issue detected. This is an "
+ "indexer-side issue outside this dashboard workflow - please "
+ "raise it separately."
+ )
+
+ return f"[UNKNOWN] {issue}"
+
+
+def _dashboard_logs_followup(user_choice, context):
+ choice = (user_choice or "").lower().strip()
+
+ if "not" not in choice and "resolved" in choice:
+ return make_response(display="Great! Glad the issue is resolved.", done=True, context=context)
+
+ display = (
+ "Understood.\n\n"
+ "Please reach out to the Wazuh community for further support:\n"
+ f" {COMMUNITY_URL}"
+ )
+ return make_response(display=display, done=True, context=context)
diff --git a/integrations/wazuh-troubleshooting-tool/backend/use_cases/cluster_issues.py b/integrations/wazuh-troubleshooting-tool/backend/use_cases/cluster_issues.py
new file mode 100644
index 00000000..c52e4996
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/use_cases/cluster_issues.py
@@ -0,0 +1,416 @@
+"""
+Use case: 'Cluster Health Issues'.
+
+Checks cluster health and, if it isn't green, drills down into the actual
+root cause instead of just listing unassigned shards and generic reason
+codes (CLUSTER_RECOVERED, INDEX_CREATED, disk watermark, ...):
+
+ write blocks -> node topology / data nodes -> disk watermark ->
+ shard limits -> per-shard _cluster/allocation/explain -> targeted fix
+
+Reuses the same utils/ helpers as use_cases/no_alerts_are_showing.py (cluster
+health/shards, replica count, reindex, disk/log checks) rather than
+re-implementing any of them here.
+"""
+
+import json
+
+from utils.service_utils import get_service_status, restart_service_and_wait
+from utils.cluster_utils import get_cluster_status, get_cluster_health, get_write_blocks, clear_write_blocks
+from utils.shard_utils import (
+ get_node_count, get_unassigned_shards, explain_allocation,
+ get_shard_capacity_percent, is_near_shard_limit,
+)
+from utils.replica_utils import recommend_replica_count, set_replica_count
+from utils.reindex_utils import reindex_for_mapping_conflict
+from utils.fix_engine import FixEngine
+from utils.log_handler import LogHandler
+from utils.log_analyzer import LogAnalyzer
+from utils.ai_utils import ai_explain
+from utils.unresolved_help import conclude
+from config import INDEXER_URL
+
+UNCLEAR_ALLOCATION_SYSTEM_PROMPT = (
+ "You are a Wazuh Indexer (OpenSearch) troubleshooting expert. You'll be given the raw "
+ "_cluster/allocation/explain response for an unassigned shard that doesn't match any of "
+ "the known causes (disk watermark, single-node replica, missing data node, shard limit). "
+ "In 3-4 short sentences: state the most likely root cause and the single most useful next "
+ "command or config fix. Be specific to what's actually in the data - don't give generic advice."
+)
+
+
+def _stop(response, context, title, explanation, fix_text):
+ response["display"] += f"\n\n[ROOT CAUSE FOUND] {title}\n\n{explanation}\n\nRecommended fix:\n{fix_text}"
+ return conclude(False, response["display"], context, topic=f"wazuh indexer cluster {title}")
+
+
+def cluster_issues_flow(user_choice=None, context=None):
+ if context is None:
+ context = {}
+
+ response = {"display": "", "ask": [], "done": False, "context": context}
+ choice = (user_choice or "").strip().lower()
+
+ # START
+ if not context:
+ response["display"] = (
+ "Let's troubleshoot Wazuh Indexer Cluster Issues.\n"
+ "This problem generally manifests as yellow/red cluster health, missing nodes, or unassigned shards.\n\n"
+ "Querying cluster health status..."
+ )
+ health, raw = get_cluster_health()
+ if not health:
+ response["display"] += f"\n\n[ERROR] Failed to query cluster health. Is the indexer running?\nResponse: {raw}"
+ response["ask"] = ["Run indexer status check? (yes / no)"]
+ context["stage"] = "indexer_status"
+ return response
+
+ status = health.get("status", "unknown")
+ nodes = health.get("number_of_nodes", 0)
+ active_shards = health.get("active_shards", 0)
+ unassigned_count = health.get("unassigned_shards", 0)
+
+ response["display"] += (
+ f"\n\nCluster Health Snapshot:\n"
+ f" Status: {status.upper()}\n"
+ f" Nodes: {nodes}\n"
+ f" Active Shards: {active_shards}\n"
+ f" Unassigned Shards: {unassigned_count}\n"
+ )
+
+ if status == "green":
+ response["display"] += "\n[OK] Cluster status is GREEN."
+ return conclude(True, response["display"], context)
+
+ response["display"] += f"\n[WARNING] Cluster status is {status.upper()}. Investigating the root cause..."
+ return _diagnose(response, context)
+
+ stage = context.get("stage")
+
+ if stage == "indexer_status":
+ if "yes" in choice:
+ status = get_service_status("wazuh-indexer")
+ response["display"] = f"Indexer service status: {status.upper()}"
+ if status != "active":
+ response["display"] += "\n\nwazuh-indexer is not active. Would you like me to restart it?"
+ response["ask"] = ["Restart indexer? (yes / no)"]
+ context["stage"] = "restart_indexer"
+ return response
+ else:
+ response["display"] = "Skipping service check."
+ return conclude(False, response["display"], context, topic="wazuh indexer cluster health check failed")
+
+ if stage == "restart_indexer":
+ if "yes" in choice:
+ status = restart_service_and_wait("wazuh-indexer")
+ response["display"] = (
+ f"Restart command sent - wazuh-indexer is now {status.upper()}. "
+ "Please re-run this workflow to check cluster health."
+ )
+ else:
+ response["display"] = "Cancelled."
+ return conclude(False, response["display"], context, topic="wazuh indexer cluster health check failed")
+
+ if stage == "fix_write_blocks":
+ block_names = context.get("write_blocks", [])
+ if "auto" in choice:
+ raw = clear_write_blocks(block_names)
+ response["display"] = f"Cleared {len(block_names)} block(s):\n{', '.join(block_names)}\n{raw}"
+ return _diagnose(response, context)
+ if "manual" in choice:
+ response["display"] = _manual_clear_blocks_instructions(block_names)
+ response["ask"] = ["Done"]
+ context["stage"] = "fix_write_blocks_manual_wait"
+ return response
+ response["ask"] = ["Auto", "Manual"]
+ return response
+
+ if stage == "fix_write_blocks_manual_wait":
+ return _diagnose(response, context)
+
+ if stage == "fix_replicas":
+ indices = context.get("unassigned_indices", [])
+ recommended = context.get("recommended_replicas", 0)
+ if "auto" in choice:
+ lines = []
+ for index_name in indices:
+ raw = set_replica_count(index_name, recommended)
+ lines.append(f" - {index_name}: {raw}")
+ response["display"] = f"Set number_of_replicas={recommended} on:\n" + "\n".join(lines)
+ elif "manual" in choice:
+ response["display"] = _manual_replica_instructions(indices, recommended)
+ else:
+ response["ask"] = ["Auto", "Manual"]
+ return response
+ return _offer_reindex(response, context)
+
+ if stage == "reindex_method":
+ indices = context.get("unassigned_indices", [])
+ if "skip" in choice:
+ return conclude(False, "Skipping the reindex.", context, topic="wazuh indexer unassigned shards reindex skipped")
+ if "manual" in choice:
+ response["display"] = _manual_reindex_instructions(indices)
+ response["ask"] = ["Done", "Skip"]
+ context["stage"] = "reindex_manual_wait"
+ return response
+ if "auto" in choice:
+ return _auto_reindex(response, context)
+ response["ask"] = ["Auto (reindex, keeps data)", "Manual", "Skip"]
+ return response
+
+ if stage == "reindex_manual_wait":
+ if "skip" in choice:
+ return conclude(False, "Skipping verification.", context, topic="wazuh indexer unassigned shards reindex verification skipped")
+ return _verify_after_reindex(response, context)
+
+ response["display"] = "Invalid stage."
+ response["done"] = True
+ return response
+
+
+# ---------------------------------------------------------------------------
+# Root-cause diagnosis
+# ---------------------------------------------------------------------------
+def _diagnose(response, context):
+ # Cluster may have already recovered on its own (e.g. right after a
+ # block was cleared) - check that before running the rest of the chain.
+ health, _ = get_cluster_health()
+ if health and health.get("status") == "green":
+ response["display"] += "\n\n[OK] Cluster status is now GREEN."
+ return conclude(True, response["display"], context)
+
+ # 1) Cluster-wide write/index-creation blocks - these silently prevent
+ # allocation regardless of anything else, so rule them out first.
+ block_result = get_write_blocks()
+ if block_result.get("error"):
+ response["display"] += f"\n[WARNING] Could not check cluster write blocks: {str(block_result['error'])[:200]}"
+ elif block_result.get("blocks"):
+ return _offer_clear_write_blocks(response, context, block_result["blocks"])
+ else:
+ response["display"] += "\n[OK] No cluster-wide write/index-creation blocks found."
+
+ # 2) Topology - single-node vs multi-node, and whether the expected
+ # data-holding nodes are actually online.
+ node_count, node_names = get_node_count()
+ context["node_count"] = node_count
+ number_of_nodes = health.get("number_of_nodes", node_count) if health else node_count
+ number_of_data_nodes = health.get("number_of_data_nodes", number_of_nodes) if health else number_of_nodes
+ topology = "single-node" if node_count <= 1 else f"multi-node ({node_count} nodes)"
+
+ response["display"] += (
+ f"\nDeployment topology: {topology}.\n"
+ f"Online nodes: {', '.join(node_names) if node_names else '(none reachable)'}\n"
+ f"Data-holding nodes: {number_of_data_nodes} of {number_of_nodes} total node(s)."
+ )
+
+ if number_of_data_nodes < 1:
+ return _stop(
+ response, context, "No data-holding indexer node online",
+ f"The cluster reports {number_of_nodes} total node(s) but 0 of them are eligible to "
+ "hold data (node.roles missing 'data', or the data node(s) are down) - shards have "
+ "nowhere to be allocated.",
+ "Check `systemctl status wazuh-indexer` on the node(s) that should hold data, confirm "
+ "they can reach the rest of the cluster on the transport port (9300), and restart them.",
+ )
+
+ # 3) Disk usage / watermark - a full/near-full disk blocks allocation
+ # cluster-wide even if the cluster otherwise looks fine.
+ disk_output = FixEngine.check_disk()
+ indexer_logs = LogHandler.get_indexer_logs(2)
+ if "watermark" in LogAnalyzer.get_issues(indexer_logs):
+ return _stop(
+ response, context, "Disk watermark exceeded",
+ f"The indexer log shows a disk watermark warning, which blocks shard allocation to "
+ f"protect against running out of disk. Current disk usage:\n\n{disk_output}",
+ "Free up disk space on the affected node (or temporarily raise "
+ "cluster.routing.allocation.disk.watermark.*), then retry allocation with:\n"
+ f" curl -k -u admin: -XPOST \"{INDEXER_URL}/_cluster/reroute?retry_failed=true\"",
+ )
+
+ # 4) Shard limits / allocation restrictions.
+ capacity = get_shard_capacity_percent()
+ if capacity is not None:
+ response["display"] += f"\nShard capacity in use: {capacity}%"
+ if is_near_shard_limit():
+ return _stop(
+ response, context, "Approaching cluster.max_shards_per_node limit",
+ f"The cluster is using {capacity}% of its total shard capacity across {node_count} "
+ "node(s) - new/unassigned shards can't be allocated once this limit is hit.",
+ "Reduce the shard count (lower number_of_replicas, delete or roll over old indices), "
+ "or raise cluster.max_shards_per_node if the hardware can support it.",
+ )
+
+ # 5) Per-shard root cause via the real allocation/explain API, instead of
+ # the terse reason codes from _cat/shards.
+ unassigned = get_unassigned_shards()
+ if not unassigned:
+ ai_text = ai_explain(UNCLEAR_ALLOCATION_SYSTEM_PROMPT, json.dumps(health or {})[:4000])
+ response["display"] += (
+ f"\n\n[WARNING] No unassigned shards and no known blocks/limits found, but cluster "
+ f"status is not GREEN.\n\nAI analysis:\n{ai_text}"
+ )
+ return conclude(False, response["display"], context, topic="wazuh indexer cluster health not green no unassigned shards")
+
+ context["unassigned_indices"] = sorted({s["index"] for s in unassigned})
+ sample = "\n".join(f" - {s['index']} shard {s['shard']} ({s['prirep']}) - {s['reason']}" for s in unassigned[:5])
+ response["display"] += f"\n\n[WARNING] {len(unassigned)} unassigned shard(s) found, e.g.:\n{sample}"
+
+ first = unassigned[0]
+ explain = explain_allocation(first["index"], first["shard"], primary=(first["prirep"] == "p"))
+ allocation_explanation = explain.get("allocate_explanation") or explain.get("error") or "(no explanation returned)"
+ response["display"] += (
+ f"\n\nAllocation explanation for {first['index']} shard {first['shard']}:\n {allocation_explanation}"
+ )
+
+ # Single-node cluster with an unassigned REPLICA - there's nowhere to
+ # place a second copy, so the fix is to drop replicas to 0, not to wait
+ # or reindex.
+ is_replica_shard = first["prirep"] == "r"
+ if node_count <= 1 and is_replica_shard:
+ replica_unassigned = sum(1 for s in unassigned if s["prirep"] == "r")
+ recommended = recommend_replica_count(node_count)
+ context["recommended_replicas"] = recommended
+ response["display"] += (
+ f"\n\n[ROOT CAUSE FOUND] Single-node cluster with unassigned replica shard(s)\n\n"
+ f"This is a single-node deployment, so OpenSearch has nowhere to place a replica "
+ f"copy - {replica_unassigned} of the unassigned shard(s) are replicas for this reason.\n\n"
+ f"Recommended fix: set number_of_replicas={recommended} on the affected index pattern(s)."
+ )
+ response["ask"] = ["Auto", "Manual"]
+ context["stage"] = "fix_replicas"
+ return response
+
+ # The explain output itself points at disk space (may catch cases the
+ # local log scan in step 3 missed, e.g. the watermark tripped on a
+ # different node than the one this tool runs on).
+ if "disk" in allocation_explanation.lower():
+ return _stop(
+ response, context, "Disk threshold blocking allocation",
+ f"The allocation explain output points to disk space as the blocker:\n {allocation_explanation}\n\n"
+ f"Current disk usage:\n{disk_output}",
+ "Free up disk space (or delete/reindex old indices) so OpenSearch drops back below "
+ "the high/flood watermark, then retry allocation with _cluster/reroute?retry_failed=true.",
+ )
+
+ # No known scripted cause matched - use the AI on the actual explain
+ # data (not a generic prompt), then offer reindexing as a general
+ # recovery step for shards that are stuck rather than just misplaced.
+ ai_text = ai_explain(UNCLEAR_ALLOCATION_SYSTEM_PROMPT, json.dumps(explain)[:4000])
+ response["display"] += f"\n\nAI analysis of the allocation explanation:\n{ai_text}"
+ return _offer_reindex(response, context)
+
+
+def _offer_clear_write_blocks(response, context, blocks):
+ listing = "\n".join(f" - {name} = {value}" for name, value in blocks.items())
+ response["display"] += (
+ f"\n\n[ROOT CAUSE FOUND] Cluster-wide write/index-creation block(s)\n\n{listing}\n\n"
+ "These silently prevent shards/indices from being allocated or written to, even while "
+ "the rest of the cluster looks healthy.\n\n"
+ "Would you like us to clear these, or will you clear them yourself?"
+ )
+ response["ask"] = ["Auto", "Manual"]
+ context["stage"] = "fix_write_blocks"
+ context["write_blocks"] = list(blocks.keys())
+ return response
+
+
+def _manual_clear_blocks_instructions(block_names):
+ lines = ",\n".join(f' "{name}": null' for name in block_names)
+ return (
+ "Run this against the indexer to clear the block(s):\n\n"
+ f" curl -XPUT -k -u admin: \"{INDEXER_URL}/_cluster/settings\" "
+ "-H 'Content-Type: application/json' -d'\n"
+ " {\n"
+ " \"persistent\": {\n"
+ f"{lines}\n"
+ " },\n"
+ " \"transient\": {\n"
+ f"{lines}\n"
+ " }\n"
+ " }'\n\n"
+ "Once you've run it, let us know."
+ )
+
+
+def _manual_replica_instructions(indices, recommended):
+ lines = "\n".join(
+ f" curl -k -u admin: -XPUT \"{INDEXER_URL}/{index_name}/_settings\" "
+ "-H 'Content-Type: application/json' -d"
+ f"'{{\"index\":{{\"number_of_replicas\":{recommended}}}}}'"
+ for index_name in indices
+ ) or " (none)"
+ return f"Run the following against the indexer:\n\n{lines}"
+
+
+def _offer_reindex(response, context):
+ indices = context.get("unassigned_indices", [])
+ listing = "\n".join(f" - {i}" for i in indices) or " (none)"
+ response["display"] += (
+ "\n\nIndices that were already stuck unassigned may still need to be reindexed to fully "
+ f"recover:\n{listing}\n\n"
+ "Reindexing keeps the data (backup, delete original, restore from backup, delete backup). "
+ "Would you like us to reindex these now?"
+ )
+ response["ask"] = ["Auto (reindex, keeps data)", "Manual", "Skip"]
+ context["stage"] = "reindex_method"
+ return response
+
+
+def _manual_reindex_instructions(indices):
+ listing = "\n".join(f" - {i}" for i in indices) or " (none)"
+ return (
+ "Reindex the affected indices one at a time (not all at once). Replace "
+ "with each index name below:\n\n"
+ f"Affected indices:\n{listing}\n\n"
+ "1. Back it up:\n\n"
+ " POST _reindex\n"
+ " {\n"
+ " \"source\": { \"index\": \"\" },\n"
+ " \"dest\": { \"index\": \"-backup\" }\n"
+ " }\n\n"
+ "2. Delete the original index:\n\n"
+ " DELETE /\n\n"
+ "3. Reindex from the backup:\n\n"
+ " POST _reindex\n"
+ " {\n"
+ " \"source\": { \"index\": \"-backup\" },\n"
+ " \"dest\": { \"index\": \"\" }\n"
+ " }\n\n"
+ "4. Delete the backup index:\n\n"
+ " DELETE /-backup\n\n"
+ "Once you've run it, let us know."
+ )
+
+
+def _auto_reindex(response, context):
+ indices = context.get("unassigned_indices", [])
+ results = []
+ for index_name in indices:
+ steps = reindex_for_mapping_conflict(index_name)
+ results.append((index_name, steps))
+
+ ok_count = sum(1 for _, steps in results if not steps.get("aborted_after"))
+ lines = []
+ for name, steps in results[:10]:
+ aborted_after = steps.get("aborted_after")
+ if not aborted_after:
+ lines.append(f" - {name}: OK")
+ else:
+ detail = (steps.get(aborted_after) or "(no response)")[:200]
+ lines.append(f" - {name}: stopped after '{aborted_after}' - nothing irreversible happened past that point. {detail}")
+ more = f"\n ... and {len(results) - 10} more" if len(results) > 10 else ""
+ response["display"] += (
+ f"\n\nReindexed {ok_count}/{len(results)} index(es) successfully, one at a time:\n"
+ + "\n".join(lines) + more
+ )
+ return _verify_after_reindex(response, context)
+
+
+def _verify_after_reindex(response, context):
+ sep = "\n\n" if response["display"] else ""
+ unassigned = get_unassigned_shards()
+ if unassigned:
+ response["display"] += f"{sep}[WARNING] {len(unassigned)} shard(s) are still unassigned after reindexing."
+ return conclude(False, response["display"], context, topic="wazuh indexer shards still unassigned after reindex")
+ response["display"] += f"{sep}[OK] No unassigned shards remain."
+ return conclude(True, response["display"], context)
diff --git a/integrations/wazuh-troubleshooting-tool/backend/use_cases/dashboard_error.py b/integrations/wazuh-troubleshooting-tool/backend/use_cases/dashboard_error.py
new file mode 100644
index 00000000..5a64cf0a
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/use_cases/dashboard_error.py
@@ -0,0 +1,952 @@
+from executor import run_command
+from utils.fix_engine import FixEngine
+from utils.log_handler import LogHandler
+from utils.log_analyzer import LogAnalyzer
+from utils.unresolved_help import conclude
+from flows.ip_cert_flow import ip_cert_flow, STAGES as IP_CERT_STAGES
+from flows.dashboard_ip_cert_flow import dashboard_ip_cert_flow, STAGES as DASH_IP_CERT_STAGES
+
+
+def dashboard_error_flow(user_choice=None, context=None):
+
+ if context is None:
+ context = {}
+
+ response = {
+ "display": "",
+ "ask": [],
+ "done": False,
+ "context": context,
+ }
+
+ # -------------------------------------------------------------------------
+ # START
+ # -------------------------------------------------------------------------
+ if not context:
+ response["display"] = (
+ "When you get 'Wazuh dashboard is not ready yet' error it normally "
+ "indicates that the Wazuh dashboard cannot communicate with the "
+ "indexer.\n\n"
+ "How would you like to proceed?\n"
+ " auto → we check and fix everything for you step by step\n"
+ " manual → we give you all the steps to follow yourself"
+ )
+ response["ask"] = ["How would you like to proceed? (auto / manual)"]
+ context["stage"] = "start_choice"
+ return response
+
+ # -------------------------------------------------------------------------
+ # ROUTE TO ip_cert_flow (indexer checks: IP -> cert paths -> heap memory)
+ # -------------------------------------------------------------------------
+ if context.get("stage") in IP_CERT_STAGES:
+ result = ip_cert_flow(user_choice=user_choice, context=context)
+
+ # "handoff" fires when the last step (heap) is still "ongoing" -
+ # control returns here so the dashboard IP/cert flow can continue.
+ # We fold this step's own message into whatever comes next instead
+ # of letting it get silently dropped at this boundary.
+ if result.get("handoff"):
+ next_result = dashboard_error_flow(context=result["context"])
+ if result.get("display"):
+ next_result["display"] = result["display"] + "\n\n" + next_result["display"]
+ return next_result
+
+ return result
+
+ # -------------------------------------------------------------------------
+ # ROUTE TO dashboard_ip_cert_flow (dashboard checks: IP -> cert paths)
+ # -------------------------------------------------------------------------
+ if context.get("stage") in DASH_IP_CERT_STAGES:
+ result = dashboard_ip_cert_flow(user_choice=user_choice, context=context)
+
+ # "handoff" fires when the last step (dashboard cert paths) is
+ # still "ongoing" - hands off to log analysis (fetch_logs).
+ if result.get("handoff"):
+ next_result = dashboard_error_flow(context=result["context"])
+ if result.get("display"):
+ next_result["display"] = result["display"] + "\n\n" + next_result["display"]
+ return next_result
+
+ return result
+
+ # -------------------------------------------------------------------------
+ # START CHOICE
+ # -------------------------------------------------------------------------
+ if context.get("stage") == "start_choice":
+
+ if user_choice and "manual" in user_choice.lower():
+ response["display"] = (
+ "Let's investigate the issue about the Wazuh Dashboard:\n\n"
+
+ "Step 1 — Make sure the Wazuh indexer service is up and running:\n"
+ " systemctl status wazuh-indexer\n\n"
+
+ "Step 2 — Check the dashboard configuration file:\n"
+ " /etc/wazuh-dashboard/opensearch_dashboards.yml\n\n"
+ " Make sure the indexer IP is correct:\n"
+ " opensearch.hosts: https://:9200\n\n"
+
+ " Run this to find the indexer IP:\n"
+ " head /etc/wazuh-indexer/opensearch.yml\n\n"
+
+ "Step 3 — Check certificate names and paths:\n"
+ " ls -lrt /etc/wazuh-dashboard/certs/\n"
+ " Ensure the paths and filenames match what is in the config.\n\n"
+
+ "Step 4 — Restart the dashboard service:\n"
+ " systemctl restart wazuh-dashboard\n"
+ " systemctl status wazuh-dashboard\n\n"
+
+ "Step 5 — Verify the dashboard can communicate with the indexer.\n"
+ "Run this from the dashboard server:\n"
+ " curl -XGET -k -u kibanaserver: "
+ "\"https://:9200/_cluster/health\"\n\n"
+
+ " If you get connection refused -> check firewall on port 9200.\n"
+ " If you see no output or auth error -> reset kibanaserver "
+ "password (Step 6).\n\n"
+
+ "Step 6 — Reset kibanaserver password if needed.\n"
+ "Password must be 8-64 chars, upper/lowercase, numbers, "
+ "symbol from .*+?-\n\n"
+
+ " /usr/share/wazuh-indexer/plugins/opensearch-security/tools/"
+ "wazuh-passwords-tool.sh -u kibanaserver -p ''\n\n"
+
+ " Note: If using AIO, passwords are updated automatically.\n\n"
+
+ " Then update the dashboard keystore:\n"
+ " echo | "
+ "/usr/share/wazuh-dashboard/bin/opensearch-dashboards-keystore "
+ "--allow-root add -f --stdin opensearch.password\n\n"
+
+ " Ref: https://documentation.wazuh.com/current/user-manual/"
+ "user-administration/password-management.html\n\n"
+
+ "Step 7 — If the issue still persists collect these logs:\n"
+ " journalctl -u wazuh-dashboard\n"
+ " cat /usr/share/wazuh-dashboard/data/wazuh/logs/wazuhapp.log "
+ "| grep -i -E 'error|warn'\n"
+ " cat /var/log/wazuh-indexer/wazuh-cluster.log "
+ "| grep -i -E 'error|warn'\n\n"
+
+ "Let us know the update for further assistance."
+ )
+
+ response["ask"] = ["Did this help? (resolved / need further assistance)"]
+ context["stage"] = "manual_followup"
+ return response
+
+ # auto chosen — ask how to determine the indexer status before restarting
+ response["display"] = (
+ "Let's start with the Wazuh indexer service."
+ )
+ response["ask"] = ["Restart Wazuh Indexer? (auto / inactive / active)"]
+ context["stage"] = "restart_step"
+ return response
+
+ # -------------------------------------------------------------------------
+ # RESTART STEP — determine indexer status, restart if needed
+ # -------------------------------------------------------------------------
+ if context.get("stage") == "restart_step":
+
+ choice = (user_choice or "").lower().strip()
+
+ if "auto" in choice:
+ status = (run_command("systemctl is-active wazuh-indexer") or "").strip()
+ elif "inactive" in choice:
+ status = "inactive"
+ elif "active" in choice:
+ status = "active"
+ else:
+ response["display"] = "Please choose one: auto / inactive / active"
+ response["ask"] = ["Restart Wazuh Indexer? (auto / inactive / active)"]
+ return response
+
+ context["indexer_status"] = status
+ response["display"] = f"Indexer status: {status}\n\n"
+
+ if status != "active":
+ response["display"] += "The Wazuh indexer is not running. Restarting it now..."
+ new_status = FixEngine.restart_indexer_and_wait()
+ context["indexer_status"] = new_status
+ response["display"] += f"\n\nStatus after restart: {new_status.upper()}"
+
+ response["display"] += (
+ "\n\nLet's now go through the indexer checks: "
+ "IP address, certificate paths, and heap memory."
+ )
+ context["stage"] = "ip_check"
+
+ # Preserve this message — indexer_recovery_flow's own first
+ # response (the Step 1 permission question) would otherwise
+ # completely replace it here.
+ restart_msg = response["display"]
+ next_response = dashboard_error_flow(context=context)
+ next_response["display"] = restart_msg + "\n\n" + next_response["display"]
+ return next_response
+
+ # -------------------------------------------------------------------------
+ # MANUAL FOLLOW-UP
+ # -------------------------------------------------------------------------
+ if context.get("stage") == "manual_followup":
+
+ if user_choice and "resolved" in user_choice.lower():
+ return conclude(True, "Great! Glad the issue is resolved.", context)
+
+ response["display"] = (
+ "Let's dig deeper.\n\n"
+ "Have you checked the indexer status yet?\n"
+ "If not, I can check it for you right now."
+ )
+
+ response["ask"] = ["Indexer status? (check / it's active / it's inactive)"]
+ context["stage"] = "indexer_status_check"
+ return response
+
+ # -------------------------------------------------------------------------
+ # INDEXER STATUS CHECK
+ # -------------------------------------------------------------------------
+ if context.get("stage") == "indexer_status_check":
+
+ if user_choice and "check" in user_choice.lower():
+ status = (run_command("systemctl is-active wazuh-indexer") or "").strip()
+ context["indexer_status"] = status
+ response["display"] = f"Indexer status: {status}"
+
+ elif user_choice and "inactive" in user_choice.lower():
+ status = "inactive"
+ context["indexer_status"] = status
+ response["display"] = "Understood — indexer is inactive."
+
+ else:
+ status = "active"
+ context["indexer_status"] = status
+ response["display"] = "Understood — indexer is active."
+
+ if status != "active":
+ response["display"] += "\n\nThe indexer is not running. Restarting wazuh-indexer now..."
+ new_status = FixEngine.restart_indexer_and_wait()
+ context["indexer_status"] = new_status
+ response["display"] += f"\n\nStatus after restart: {new_status.upper()}"
+
+ response["display"] += (
+ "\n\nLet's now go through the indexer checks: "
+ "IP address, certificate paths, and heap memory."
+ )
+ context["stage"] = "ip_check"
+
+ restart_msg = response["display"]
+ next_response = dashboard_error_flow(context=context)
+ next_response["display"] = restart_msg + "\n\n" + next_response["display"]
+ return next_response
+
+ # -------------------------------------------------------------------------
+ # FETCH LOGS
+ # -------------------------------------------------------------------------
+ if context.get("stage") == "fetch_logs":
+
+ response["display"] = (
+ "Would you like me to fetch the dashboard and indexer logs, "
+ "or will you run the commands yourself?"
+ )
+
+ response["ask"] = ["Fetch logs? (auto / manual / no)"]
+ context["stage"] = "logs_action"
+ return response
+
+ # -------------------------------------------------------------------------
+ # LOGS ACTION
+ # -------------------------------------------------------------------------
+ if context.get("stage") == "logs_action":
+
+ chose_auto = user_choice and "auto" in user_choice.lower()
+ chose_manual = user_choice and "manual" in user_choice.lower()
+
+ if chose_auto:
+
+ logs = LogHandler.get_indexer_logs(1)
+ clean = LogHandler.clean_logs(logs)
+
+ context["logs"] = logs
+ response["display"] = f"Recent indexer logs:\n\n{clean}"
+ context["stage"] = "logs_analyze"
+
+ return dashboard_error_flow(context=context)
+
+ elif chose_manual:
+
+ response["display"] = (
+ "Run these and paste the output back:\n\n"
+
+ " journalctl -u wazuh-dashboard\n\n"
+
+ " cat /usr/share/wazuh-dashboard/data/wazuh/logs/wazuhapp.log "
+ "| grep -i -E 'error|warn'\n\n"
+
+ " cat /var/log/wazuh-indexer/wazuh-cluster.log "
+ "| grep -i -E 'error|warn'"
+ )
+
+ response["ask"] = ["Paste the log output here"]
+ context["stage"] = "logs_paste"
+
+ return response
+
+ else:
+ response["display"] = "Skipping log check."
+ context["stage"] = "jvm_check"
+
+ return dashboard_error_flow(context=context)
+
+ # -------------------------------------------------------------------------
+ # LOGS PASTE
+ # -------------------------------------------------------------------------
+ if context.get("stage") == "logs_paste":
+
+ context["logs"] = user_choice or ""
+ context["stage"] = "logs_analyze"
+
+ return dashboard_error_flow(context=context)
+
+ # -------------------------------------------------------------------------
+ # ANALYZE LOGS
+ # -------------------------------------------------------------------------
+ if context.get("stage") == "logs_analyze":
+
+ logs = context.get("logs") or ""
+ issues = LogAnalyzer.get_issues(logs)
+
+ context["issues"] = issues
+
+ if not issues:
+ response["display"] = "No known issues found in the logs."
+ context["stage"] = "jvm_check"
+
+ return dashboard_error_flow(context=context)
+
+ found_lines = []
+
+ for issue in issues:
+
+ if issue == "init":
+ found_lines.append(
+ "[INIT] Indexer security not yet initialized."
+ )
+
+ elif issue == "heap":
+ found_lines.append(
+ "[HEAP] Memory/heap issue detected."
+ )
+
+ elif issue == "auth":
+ found_lines.append(
+ "[AUTH] Authentication failed for kibanaserver. "
+ "Please flag this to your team for a password reset."
+ )
+
+ elif issue == "watermark":
+ found_lines.append(
+ "[DISK] Disk watermark exceeded. "
+ "Free up disk space or expand storage manually.\n"
+ "Check: df -h"
+ )
+
+ elif issue == "permission":
+ found_lines.append(
+ "[PERMISSION] Insecure file permissions on indexer config. "
+ "Please flag this to your team."
+ )
+
+ elif issue == "dashboard_connection_refused":
+ found_lines.append(
+ "[CONNECTION REFUSED] Connection to :9200 was refused. "
+ "Please check whether the Wazuh indexer service is running."
+ )
+
+ response["display"] = (
+ f"Found {len(issues)} issue(s) in the logs:\n\n"
+ + "\n\n".join(found_lines)
+ )
+
+ if "init" in issues:
+ context["stage"] = "init_check"
+
+ elif "heap" in issues:
+ context["stage"] = "jvm_check"
+
+ elif "dashboard_connection_refused" in issues:
+ context["stage"] = "connection_refused_indexer_check"
+
+ else:
+ context["stage"] = "dashboard_status"
+
+ return dashboard_error_flow(context=context)
+
+ # -------------------------------------------------------------------------
+ # INIT CHECK
+ # -------------------------------------------------------------------------
+ if context.get("stage") == "init_check":
+
+ response["display"] = (
+ "The logs show the indexer security is not yet initialized.\n\n"
+ "Is this a new or existing installation?"
+ )
+
+ response["ask"] = ["New or existing? (new / existing)"]
+ context["stage"] = "init_action"
+
+ return response
+
+ # -------------------------------------------------------------------------
+ # INIT ACTION
+ # -------------------------------------------------------------------------
+ if context.get("stage") == "init_action":
+
+ if user_choice and "new" in user_choice.lower():
+
+ response["display"] = (
+ "Since this is a new installation the indexer security "
+ "needs to be initialized. Would you like me to run it?"
+ )
+
+ response["ask"] = ["Run security init? (auto / manual)"]
+ context["stage"] = "init_run"
+
+ return response
+
+ else:
+
+ response["display"] = (
+ "Since this is an existing installation, "
+ "the initialization issue is unexpected.\n\n"
+ "Let me check step by step starting with the IP configuration."
+ )
+
+ context["stage"] = "ip_check"
+
+ return dashboard_error_flow(context=context)
+
+ # -------------------------------------------------------------------------
+ # RUN SECURITY INIT
+ # -------------------------------------------------------------------------
+ if context.get("stage") == "init_run":
+
+ if user_choice and "auto" in user_choice.lower():
+
+ out = run_command(FixEngine.init_command()) or ""
+ response["display"] = f"Security init output:\n{out}"
+
+ else:
+
+ response["display"] = (
+ "Run:\n\n"
+ f" {FixEngine.init_command()}\n\n"
+ "Then restart:\n"
+ " systemctl restart wazuh-indexer"
+ )
+
+ context["stage"] = "jvm_check"
+
+ return dashboard_error_flow(context=context)
+
+ # -------------------------------------------------------------------------
+ # JVM HEAP CHECK (legacy — triggered from log analysis path)
+ # -------------------------------------------------------------------------
+ if context.get("stage") == "jvm_check":
+
+ current = run_command(
+ "grep -E '^-Xms|^-Xmx' /etc/wazuh-indexer/jvm.options"
+ ) or "(could not read)"
+
+ total_kb = run_command(
+ "grep MemTotal /proc/meminfo | awk '{print $2}'"
+ ) or ""
+
+ total_gb = round(int(total_kb.strip()) / 1024 / 1024)
+ heap_gb = max(1, total_gb // 2)
+
+ response["display"] = (
+ f"Current JVM heap settings:\n"
+ f"{current}\n\n"
+ f"Total RAM: {total_gb} GB\n\n"
+ f"Recommended (50% of RAM):\n"
+ f" -Xms{heap_gb}g\n"
+ f" -Xmx{heap_gb}g\n\n"
+ f"{FixEngine.heap_steps()}\n\n"
+ "Would you like to fix the heap settings?"
+ )
+
+ response["ask"] = ["Fix heap? (auto / manual / no)"]
+ context["recommended_heap"] = heap_gb
+ context["stage"] = "jvm_fix"
+
+ return response
+
+ # -------------------------------------------------------------------------
+ # JVM FIX (legacy — triggered from log analysis path)
+ # -------------------------------------------------------------------------
+ if context.get("stage") == "jvm_fix":
+
+ if user_choice and "auto" in user_choice.lower():
+
+ heap_gb = context.get("recommended_heap", 2)
+ result = FixEngine.fix_jvm_heap(heap_gb)
+
+ response["display"] = (
+ "Edited /etc/wazuh-indexer/jvm.options\n\n"
+ f"Restarted wazuh-indexer (status: {result['status'].upper()}).\n\n"
+ "Current JVM heap settings:\n"
+ f"{result['updated']}"
+ )
+
+ response["ask"] = ["Is the dashboard issue fixed? (fixed / ongoing)"]
+ context["stage"] = "post_heap_check"
+
+ return response
+
+ elif user_choice and "manual" in user_choice.lower():
+
+ response["display"] = FixEngine.heap_steps()
+ response["ask"] = ["Is the dashboard issue fixed? (fixed / ongoing)"]
+ context["stage"] = "post_heap_check"
+
+ return response
+
+ else:
+
+ response["display"] = "Skipped heap fix."
+ context["stage"] = "dashboard_status"
+
+ return dashboard_error_flow(context=context)
+
+ # -------------------------------------------------------------------------
+ # POST HEAP CHECK (legacy — triggered from log analysis path)
+ # -------------------------------------------------------------------------
+ if context.get("stage") == "post_heap_check":
+
+ if user_choice.lower().strip() == "fixed":
+ return conclude(True, "Great! The issue is resolved.", context)
+
+ elif user_choice.lower().strip() == "ongoing":
+
+ response["display"] = (
+ "The issue is still ongoing.\n"
+ "Let's fetch the logs for deeper analysis."
+ )
+
+ context["stage"] = "fetch_logs"
+
+ return dashboard_error_flow(context=context)
+
+ # -------------------------------------------------------------------------
+ # DASHBOARD STATUS + LOGS
+ # -------------------------------------------------------------------------
+ if context.get("stage") == "dashboard_status":
+
+ status = FixEngine.status_dashboard().strip()
+ response["display"] = f"Dashboard status: {status}\n\n"
+
+ if status == "active":
+
+ response["display"] += (
+ "The Wazuh dashboard is running.\n"
+ "Please open your browser and check the UI."
+ )
+
+ response["ask"] = ["Is the issue resolved? (resolved / not resolved)"]
+ context["stage"] = "final_status_check"
+
+ return response
+
+ indexer_logs = LogHandler.get_indexer_logs(1)
+ dashboard_logs = LogHandler.get_dashboard_logs(1)
+
+ clean_indexer = LogHandler.clean_logs(indexer_logs)
+ clean_dashboard = LogHandler.clean_logs(dashboard_logs)
+
+ response["display"] += (
+ "The dashboard is still not active.\n\n"
+
+ "--- Connectivity check ---\n"
+
+ "Run from the dashboard server:\n"
+
+ " curl -XGET -k -u kibanaserver: "
+ "\"https://:9200/_cluster/health\"\n\n"
+
+ " Connection refused → check firewall on port 9200.\n"
+
+ " Auth error → reset kibanaserver password:\n"
+
+ " /usr/share/wazuh-indexer/plugins/opensearch-security/tools/"
+ "wazuh-passwords-tool.sh -u kibanaserver -p ''\n\n"
+
+ " Then update keystore:\n"
+
+ " echo | "
+ "/usr/share/wazuh-dashboard/bin/opensearch-dashboards-keystore "
+ "--allow-root add -f --stdin opensearch.password\n\n"
+
+ " Ref: https://documentation.wazuh.com/current/user-manual/"
+ "user-administration/password-management.html\n\n"
+
+ "--- Recent indexer logs ---\n"
+ f"{clean_indexer}\n\n"
+
+ "--- Recent dashboard logs ---\n"
+ f"{clean_dashboard}\n\n"
+ )
+
+ return conclude(False, response["display"], context, topic="wazuh dashboard not active cannot connect to indexer")
+
+
+ # -------------------------------------------------------------------------
+ # DASHBOARD STATUS + LOGS
+ # -------------------------------------------------------------------------
+ if context.get("stage") == "dashboard_status_logs":
+
+ status = (FixEngine.status_dashboard() or "").strip()
+ context["dashboard_status"] = status
+
+ response["display"] = f"Dashboard status: {status or 'unknown'}\n\n"
+
+ if status == "active":
+ response["display"] += (
+ "The Wazuh dashboard is running.\n"
+ "Please open your browser and check the UI."
+ )
+
+ response["ask"] = [
+ "Is the issue resolved? (resolved / not resolved)"
+ ]
+
+ context["stage"] = "logs_action_dashboard"
+ response["context"] = context
+
+ return response
+
+ response["display"] += (
+ "The Wazuh dashboard is not active.\n\n"
+ "Let's check the dashboard logs."
+ )
+
+ logs = LogHandler.get_dashboard_logs(1)
+ clean = LogHandler.clean_logs(logs)
+
+ context["logs"] = logs
+ context["stage"] = "logs_analyze_dashboard"
+
+ response["display"] += (
+ f"\n\nRecent dashboard logs:\n\n{clean}"
+ )
+ response["ask"] = ["Continue to log analysis? (yes)"]
+ response["context"] = context
+ return response
+
+
+ # -------------------------------------------------------------------------
+ # DASHBOARD RESOLUTION CHECK
+ # -------------------------------------------------------------------------
+ if context.get("stage") == "logs_action_dashboard":
+
+ choice = (user_choice or "").lower().strip()
+
+ if choice == "resolved":
+ return conclude(True, "Glad to know the issue is resolved.", context)
+
+ if choice == "not resolved":
+ logs = LogHandler.get_dashboard_logs(1)
+ clean = LogHandler.clean_logs(logs)
+
+ context["logs"] = logs
+ context["stage"] = "logs_analyze_dashboard"
+
+ response["display"] = (
+ "The issue is still not resolved.\n\n"
+ f"Recent dashboard logs:\n\n{clean}"
+ )
+ response["ask"] = ["Continue? (yes)"]
+ response["context"] = context
+ return response
+
+ response["display"] = "Please choose: resolved / not resolved"
+ response["ask"] = [
+ "Is the issue resolved? (resolved / not resolved)"
+ ]
+
+ response["context"] = context
+ return response
+
+
+ # -------------------------------------------------------------------------
+ # ANALYZE DASHBOARD LOGS
+ # -------------------------------------------------------------------------
+ if context.get("stage") == "logs_analyze_dashboard":
+
+ logs = context.get("logs") or ""
+ issues = LogAnalyzer.get_issues(logs)
+
+ context["issues"] = issues
+
+ if not issues:
+ # Logs are clean but issue persists — move to connectivity/password check
+ response["display"] = (
+ "No known issues found in the dashboard logs.\n\n"
+ "Since the dashboard is running but the issue persists, "
+ "let's check the connectivity between the dashboard and the indexer."
+ )
+ response["ask"] = ["Check connectivity? (yes)"]
+ context["stage"] = "dashboard_status"
+ response["context"] = context
+ return response
+
+ found_lines = []
+
+ for issue in issues:
+
+ if issue == "init":
+ found_lines.append(
+ "[INIT] Indexer security not yet initialized."
+ )
+
+ elif issue == "heap":
+ found_lines.append(
+ "[HEAP] Memory/heap issue detected."
+ )
+
+ elif issue == "auth":
+ found_lines.append(
+ "[AUTH] Authentication failed for kibanaserver. "
+ "Please flag this to your team for a password reset."
+ )
+
+ elif issue == "watermark":
+ found_lines.append(
+ "[DISK] Disk watermark exceeded. "
+ "Free up disk space or expand storage manually.\n"
+ "Check: df -h"
+ )
+
+ elif issue == "permission":
+ found_lines.append(
+ "[PERMISSION] Insecure file permissions on indexer config. "
+ "Please flag this to your team."
+ )
+
+ elif issue == "dashboard_connection_refused":
+ found_lines.append(
+ "[CONNECTION REFUSED] Connection to :9200 was refused. "
+ "Please check whether the Wazuh indexer service is running."
+ )
+
+ response["display"] = (
+ f"Found {len(issues)} issue(s) in the logs:\n\n"
+ + "\n\n".join(found_lines)
+ )
+
+ if "init" in issues:
+ context["stage"] = "init_check"
+ response["ask"] = ["Continue to initialization check? (yes)"]
+
+ elif "heap" in issues:
+ context["stage"] = "jvm_check"
+ response["ask"] = ["Continue to heap check? (yes)"]
+
+ elif "dashboard_connection_refused" in issues:
+ context["stage"] = "connection_refused_indexer_check"
+ response["ask"] = ["Continue? (yes)"]
+
+ else:
+ response["display"] += "\n\nThese issues need manual review."
+ return conclude(False, response["display"], context, topic="wazuh dashboard log issues " + " ".join(issues))
+
+ response["context"] = context
+ return response
+
+ # -------------------------------------------------------------------------
+ # CONNECTION REFUSED: INDEXER CHECK
+ # -------------------------------------------------------------------------
+ if context.get("stage") == "connection_refused_indexer_check":
+
+ response["display"] = (
+ "The dashboard logs show that connection to the Wazuh indexer on port 9200 was refused.\n\n"
+ "This usually means the Wazuh indexer service is stopped, unhealthy, or not reachable."
+ )
+
+ response["ask"] = [
+ "We have already checked the status. Should we do that again? "
+ "(check / it's active / it's inactive / no)"
+ ]
+
+ context["stage"] = "connection_refused_indexer_status"
+ response["context"] = context
+
+ return response
+
+
+ # -------------------------------------------------------------------------
+ # CONNECTION REFUSED: INDEXER STATUS
+ # -------------------------------------------------------------------------
+ if context.get("stage") == "connection_refused_indexer_status":
+
+ choice = (user_choice or "").lower().strip()
+
+ if "check" in choice:
+ status = (
+ run_command("systemctl is-active wazuh-indexer") or ""
+ ).strip()
+
+ elif "inactive" in choice:
+ status = "inactive"
+
+ elif "active" in choice:
+ status = "active"
+
+ elif choice == "no":
+ response["display"] = (
+ "Okay.\n\n"
+ "Please check the Wazuh indexer and dashboard logs for newer errors.\n\n"
+ "If there are no newer errors and the issue still persists, "
+ "I recommend taking help from the Wazuh community:\n"
+ "https://wazuh.com/community/"
+ )
+
+ response["context"] = context
+ return response
+
+ else:
+ response["display"] = (
+ "Please choose one option: check / it's active / it's inactive / no"
+ )
+
+ response["ask"] = [
+ "Should we check the indexer status again? "
+ "(check / it's active / it's inactive / no)"
+ ]
+
+ response["context"] = context
+ return response
+
+ context["indexer_status"] = status
+
+ if status != "active":
+ response["display"] = (
+ f"Indexer status: {status or 'unknown'}\n\n"
+ "The Wazuh indexer service is inactive. Restarting it now..."
+ )
+ new_status = FixEngine.restart_indexer_and_wait()
+ context["indexer_status"] = new_status
+ response["display"] += (
+ f"\n\nStatus after restart: {new_status.upper()}\n\n"
+ "Let's now go through the indexer checks: "
+ "IP address, certificate paths, and heap memory."
+ )
+ context["stage"] = "ip_check"
+
+ restart_msg = response["display"]
+ next_response = dashboard_error_flow(context=context)
+ next_response["display"] = restart_msg + "\n\n" + next_response["display"]
+ return next_response
+
+ # indexer is active but dashboard still can't connect
+ response["display"] = (
+ f"Indexer status: {status}\n\n"
+ "The Wazuh indexer is active but the dashboard still cannot reach it "
+ "on port 9200.\n\n"
+ "Please check: firewall rules on port 9200, the dashboard's "
+ "opensearch.hosts IP, and network connectivity between dashboard "
+ "and indexer."
+ )
+ return conclude(False, response["display"], context, topic="wazuh dashboard cannot connect to indexer port 9200")
+
+ # -------------------------------------------------------------------------
+ # FINAL STATUS CHECK
+ # -------------------------------------------------------------------------
+ if context.get("stage") == "final_status_check":
+ choice = (user_choice or "").lower().strip()
+
+ if choice == "resolved":
+ return conclude(True, "Great! Glad the issue is resolved.", context)
+
+ # not resolved — fetch and analyse logs
+ dashboard_logs = LogHandler.get_dashboard_logs(1)
+ clean_dashboard = LogHandler.clean_logs(dashboard_logs)
+
+ issues = LogAnalyzer.get_issues(dashboard_logs or "")
+ context["issues"] = issues
+
+ response["display"] = (
+ "Let's dig into the logs.\n\n"
+ "--- Recent dashboard logs ---\n"
+ f"{clean_dashboard}\n\n"
+ )
+
+ if issues:
+ found_lines = []
+
+ for issue in issues:
+
+ if issue == "init":
+ found_lines.append(
+ "[INIT] Indexer security not yet initialized."
+ )
+ elif issue == "heap":
+ found_lines.append(
+ "[HEAP] Memory/heap issue detected."
+ )
+ elif issue == "auth":
+ found_lines.append(
+ "[AUTH] Authentication failed for kibanaserver — "
+ "password reset required.\n\n"
+
+ " /usr/share/wazuh-indexer/plugins/opensearch-security/tools/"
+ "wazuh-passwords-tool.sh -u kibanaserver -p ''\n\n"
+
+ " Then update the dashboard keystore:\n"
+
+ " echo | "
+ "/usr/share/wazuh-dashboard/bin/opensearch-dashboards-keystore "
+ "--allow-root add -f --stdin opensearch.password\n\n"
+
+ " Restart:\n"
+ " systemctl restart wazuh-dashboard"
+ )
+ elif issue == "watermark":
+ found_lines.append(
+ "[DISK] Disk watermark exceeded — "
+ "free up disk space or expand storage.\n"
+ " Check: df -h"
+ )
+ elif issue == "permission":
+ found_lines.append(
+ "[PERMISSION] Insecure file permissions on indexer config."
+ )
+
+ response["display"] += (
+ f"Issues detected ({len(issues)}):\n\n"
+ + "\n\n".join(found_lines)
+ )
+
+ else:
+ response["display"] += (
+ "No known issues detected in the logs.\n\n"
+
+ "If the issue still persists share the above on:\n"
+ " https://wazuh.com/community/"
+ )
+
+ response["ask"] = ["Still not resolved? (resolved / need more help)"]
+ context["stage"] = "final_escalate"
+ return response
+
+ # -------------------------------------------------------------------------
+ # FINAL ESCALATE
+ # -------------------------------------------------------------------------
+ if context.get("stage") == "final_escalate":
+
+ if user_choice and "resolved" in user_choice.lower():
+ return conclude(True, "Great! Glad the issue is resolved.", context)
+
+ response["display"] = "The issue needs further investigation."
+ return conclude(False, response["display"], context, topic="wazuh dashboard error troubleshooting unresolved")
diff --git a/integrations/wazuh-troubleshooting-tool/backend/use_cases/filebeat_error.py b/integrations/wazuh-troubleshooting-tool/backend/use_cases/filebeat_error.py
new file mode 100644
index 00000000..bb523351
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/use_cases/filebeat_error.py
@@ -0,0 +1,298 @@
+"""
+Use case: 'Filebeat Not Working' - having an issue in `filebeat test output`.
+
+Standalone Filebeat-only troubleshooting card. Unlike
+use_cases/no_alerts_are_showing.py's Step 3, this does NOT run as a fixed
+"Step 1 / Step 2" script - each failure category needs a different sequence
+(an unsupported-version fix loops back through the output test, a TLS/cert
+fix does the same, a mapping issue hands off elsewhere entirely), so each
+branch narrates only what it actually did.
+
+Sequence:
+ 1. `filebeat test output` (after confirming the service is up).
+ 2. If it fails, classify why (utils/filebeat_utils.classify_filebeat_failure)
+ and offer the matching auto/manual fix.
+ 3. If it succeeds, still check the Filebeat log - a clean connectivity
+ test doesn't rule out something like a field-mapping error that only
+ shows up there (or as a dashboard shard-failure).
+ 4. If a mapping/template signature is found (either in the failed test
+ output or in the log), hand off to the dedicated "Filebeat Mapping
+ Issue" card (use_cases/mapping_issue.py) instead of trying to fix
+ templates here.
+ 5. If nothing is wrong on either check, point the user at the official
+ Wazuh community for further help.
+
+Reuses the same low-level Filebeat/cert helpers as flows/filebeat_flow.py
+(utils/filebeat_utils.py, utils/cert_utils.py) - no Filebeat logic is
+reimplemented here, only the narration/sequencing is different.
+"""
+
+from utils.service_utils import get_service_status, start_service_and_wait
+from utils.filebeat_utils import (
+ run_filebeat_output_test, get_filebeat_log_errors, classify_filebeat_failure,
+ fix_unsupported_filebeat_version, manual_unsupported_version_instructions,
+)
+from utils.cert_utils import regenerate_and_redeploy_certs, manual_cert_redeploy_instructions
+from utils.ai_utils import ai_explain
+from utils.unresolved_help import conclude
+
+WAZUH_COMMUNITY_URL = "https://wazuh.com/community/"
+
+MAPPING_ISSUE_KEYWORDS = [
+ "illegal_argument_exception",
+ "mapper_parsing_exception",
+ "strict_dynamic_mapping_exception",
+ "not optimised for operations that require per-document field data",
+ "use a keyword field instead",
+ "failed to parse field",
+ "mapping conflict",
+ "cannot be changed from type",
+]
+
+UNKNOWN_FAILURE_SYSTEM_PROMPT = (
+ "You are a Wazuh Filebeat troubleshooting expert. You'll be given the output of "
+ "'filebeat test output' plus recent Filebeat log error/warning lines. In 3-4 short "
+ "sentences: state the most likely root cause and the single most useful next command or "
+ "config fix. Be specific to what's actually in the output - don't give generic advice."
+)
+
+
+def _looks_like_mapping_issue(text):
+ lowered = (text or "").lower()
+ return any(kw in lowered for kw in MAPPING_ISSUE_KEYWORDS)
+
+
+def _mapping_handoff(response):
+ response["display"] += (
+ "\n\n[LIKELY ROOT CAUSE] Field-mapping / index-template issue\n\n"
+ "This looks like a mismatch between a field's data type and the Wazuh Indexer's "
+ "index template rather than a Filebeat connectivity problem - fixing it means "
+ "checking/reinstalling the Wazuh index template, not restarting Filebeat.\n\n"
+ "Please continue with the \"Filebeat Mapping Issue\" card in the Troubleshooting "
+ "Library - it checks the live index template, reinstalls the official one if it's "
+ "missing or overridden, and reindexes any indices already affected."
+ )
+ response["done"] = True
+ return response
+
+
+def filebeat_error_flow(user_choice=None, context=None):
+ if context is None:
+ context = {}
+
+ response = {"display": "", "ask": [], "done": False, "context": context}
+ choice = (user_choice or "").strip().lower()
+
+ # START
+ if not context:
+ response["display"] = (
+ "Let's check the output of the command `filebeat test output` to see whether "
+ "Filebeat can reach the Wazuh Indexer.\n\n"
+ "How would you like to check this?\n\n"
+ " Auto - We perform all checks automatically.\n"
+ " Manual - We provide the commands, and you run them and share the output."
+ )
+ response["ask"] = ["Auto", "Manual"]
+ context["stage"] = "method"
+ return response
+
+ stage = context.get("stage")
+
+ if stage == "method":
+ if "manual" in choice:
+ response["display"] = (
+ "First, check whether the Filebeat service is running:\n\n"
+ " systemctl status filebeat\n\n"
+ "If it's active, run the output test:\n\n"
+ " filebeat test output\n\n"
+ "Did it succeed?"
+ )
+ response["ask"] = ["Yes, it succeeded", "No, it failed"]
+ context["stage"] = "manual_result"
+ return response
+ return _check_service_and_test(response, context)
+
+ if stage == "manual_result":
+ if "yes" in choice or "succeeded" in choice:
+ return _check_logs_after_success(response, context, prefix="[OK] Confirmed - the output test succeeded.")
+ # Self-reported failure - get the real output/logs rather than trusting the report.
+ return _check_service_and_test(response, context)
+
+ if stage == "fix_service_start":
+ if "auto" in choice:
+ status = start_service_and_wait("filebeat")
+ elif choice == "done":
+ status = get_service_status("filebeat")
+ elif "manual" in choice:
+ response["display"] = "Run: systemctl start filebeat"
+ response["ask"] = ["Done"]
+ context["stage"] = "fix_service_start"
+ return response
+ else:
+ response["ask"] = ["Auto", "Manual"]
+ return response
+
+ if status != "active":
+ response["display"] = (
+ "[ROOT CAUSE FOUND] Filebeat failed to start\n\n"
+ "Filebeat did not come up, so it can't ship alerts to the indexer at all.\n\n"
+ "Manual fix:\nCheck `journalctl -u filebeat` and /var/log/filebeat/filebeat for startup errors."
+ )
+ return conclude(False, response["display"], context, topic="wazuh filebeat service fails to start")
+ return _run_test_and_branch(response, context, prefix="[OK] Filebeat is running.\n\n")
+
+ if stage == "fix_version_choice":
+ return _apply_fix(
+ response, context, choice,
+ auto_fn=fix_unsupported_filebeat_version,
+ manual_instructions=manual_unsupported_version_instructions(),
+ manual_wait_stage="fix_version_manual_wait",
+ issue_label="unsupported Filebeat version",
+ )
+
+ if stage == "fix_version_manual_wait":
+ return _run_test_and_branch(response, context, prefix="")
+
+ if stage == "fix_tls_choice":
+ return _apply_fix(
+ response, context, choice,
+ auto_fn=lambda: regenerate_and_redeploy_certs(),
+ manual_instructions=manual_cert_redeploy_instructions(),
+ manual_wait_stage="fix_tls_manual_wait",
+ issue_label="TLS/certificate error",
+ )
+
+ if stage == "fix_tls_manual_wait":
+ return _run_test_and_branch(response, context, prefix="")
+
+ response["display"] = "Invalid stage."
+ response["done"] = True
+ return response
+
+
+# ---------------------------------------------------------------------------
+def _check_service_and_test(response, context):
+ response["display"] += ("\n" if response["display"] else "") + "Checking whether the Filebeat service is running..."
+ status = get_service_status("filebeat")
+ if status != "active":
+ response["display"] += "\n[WARNING] Filebeat is not running."
+ response["ask"] = ["Auto", "Manual"]
+ context["stage"] = "fix_service_start"
+ return response
+ response["display"] += "\n[OK] Filebeat is running."
+ return _run_test_and_branch(response, context, prefix="")
+
+
+def _run_test_and_branch(response, context, prefix=""):
+ test = run_filebeat_output_test()
+ response["display"] += f"\n{prefix}Running `filebeat test output`...\n{test['raw']}\n"
+
+ if not test["ok"]:
+ return _diagnose_failure(response, context, test["raw"])
+
+ return _check_logs_after_success(response, context, prefix="[OK] The output test succeeded.")
+
+
+def _check_logs_after_success(response, context, prefix=""):
+ # A clean output test only proves connectivity - it doesn't rule out
+ # something like a field-mapping error that only shows up in the log
+ # (or as a dashboard shard-failure popup), so check the log even when
+ # the test itself passed.
+ response["display"] += f"\n{prefix}\n\nChecking the Filebeat log for anything the connectivity test wouldn't catch..."
+ errors = get_filebeat_log_errors()
+
+ if not errors.strip():
+ response["display"] += (
+ "\n[OK] No error/warning lines found in the Filebeat log either - there's no "
+ "further automated diagnosis we can run from here."
+ )
+ return conclude(False, response["display"], context, topic="wazuh filebeat no errors found still an issue")
+
+ if _looks_like_mapping_issue(errors):
+ response["display"] += f"\n\nRecent Filebeat log errors:\n{errors}"
+ return _mapping_handoff(response)
+
+ explanation = ai_explain(UNKNOWN_FAILURE_SYSTEM_PROMPT, errors)
+ response["display"] += (
+ f"\n\n[WARNING] Found error/warning lines in the Filebeat log even though the "
+ f"connectivity test passed:\n{errors}\n\nAI analysis:\n{explanation}"
+ )
+ return conclude(False, response["display"], context, topic="wazuh filebeat log errors after successful test")
+
+
+def _diagnose_failure(response, context, test_raw):
+ errors = get_filebeat_log_errors()
+ combined = f"{test_raw}\n{errors}"
+
+ if _looks_like_mapping_issue(combined):
+ response["display"] += f"\nRecent Filebeat log errors:\n{errors if errors else '(none found)'}"
+ return _mapping_handoff(response)
+
+ category = classify_filebeat_failure(test_raw, errors)
+
+ if category == "indexer_unreachable":
+ response["display"] += (
+ "\n[WARNING] Filebeat cannot reach the Wazuh Indexer. This isn't a Filebeat "
+ "problem by itself - please use the \"Cluster Health Issues\" card to check the "
+ "Wazuh Indexer directly."
+ )
+ response["done"] = True
+ return response
+
+ if category == "unsupported_version":
+ context["stage"] = "fix_version_choice"
+ response["display"] += (
+ "\n[ISSUE] This looks like an unsupported Filebeat version. Wazuh is only "
+ "compatible with Filebeat-OSS 7.10.2 - a newer version fails with errors like "
+ "'invalid_index_name_exception' on the _license index.\n\n"
+ "Would you like us to fix this automatically, or fix it yourself?"
+ )
+ response["ask"] = ["Auto", "Manual"]
+ return response
+
+ if category == "tls_cert_error":
+ context["stage"] = "fix_tls_choice"
+ response["display"] += (
+ "\n[ISSUE] This looks like a TLS/certificate error. The fix is to regenerate the "
+ "certificates and redeploy them to the Wazuh Indexer, Filebeat, and Wazuh Dashboard.\n\n"
+ "Would you like us to fix this automatically, or fix it yourself?"
+ )
+ response["ask"] = ["Auto", "Manual"]
+ return response
+
+ # auth_failure / unknown - no scripted fix, surface what we know and stop.
+ explanation = ai_explain(UNKNOWN_FAILURE_SYSTEM_PROMPT, combined) if errors.strip() else \
+ "No additional error/warning lines found in the Filebeat log."
+ label = "authentication failure" if category == "auth_failure" else "an unrecognized error"
+ response["display"] += (
+ f"\n[WARNING] The output test failed with what looks like {label}.\n\n"
+ f"Recent Filebeat log errors:\n{errors if errors else '(none found)'}\n\n"
+ f"AI analysis:\n{explanation}"
+ )
+ return conclude(False, response["display"], context, topic=f"wazuh filebeat {label}")
+
+
+def _apply_fix(response, context, choice, auto_fn, manual_instructions, manual_wait_stage, issue_label):
+ if "manual" in choice:
+ context["stage"] = manual_wait_stage
+ response["display"] = manual_instructions + "\n\nLet us know once you've made the change."
+ response["ask"] = ["Done"]
+ return response
+
+ if "auto" not in choice:
+ response["ask"] = ["Auto", "Manual"]
+ return response
+
+ result = auto_fn()
+ if result.get("ok"):
+ return _run_test_and_branch(
+ response, context,
+ prefix=f"Fixed the {issue_label} automatically.\n{result.get('log', '')}\n\n",
+ )
+
+ response["display"] = (
+ f"[ROOT CAUSE FOUND] Could not auto-fix the {issue_label}\n\n"
+ f"{result.get('log', '')}\n\n"
+ "Manual fix:\n" + manual_instructions
+ )
+ return conclude(False, response["display"], context, topic=f"wazuh filebeat could not auto-fix {issue_label}")
diff --git a/integrations/wazuh-troubleshooting-tool/backend/use_cases/indexing_error.py b/integrations/wazuh-troubleshooting-tool/backend/use_cases/indexing_error.py
new file mode 100644
index 00000000..aa908e89
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/use_cases/indexing_error.py
@@ -0,0 +1,119 @@
+from executor import run_command
+import time
+from utils.api_utils import indexer_api_get
+from utils.unresolved_help import conclude
+
+def indexing_error_flow(user_choice=None, context=None):
+ if context is None:
+ context = {}
+
+ response = {
+ "display": "",
+ "ask": [],
+ "done": False,
+ "context": context,
+ }
+
+ # START
+ if not context:
+ response["display"] = (
+ "Let's troubleshoot Wazuh indexing errors. This typically occurs when the "
+ "wazuh-indexer service is down, certificates are invalid, or disk watermark is exceeded.\n\n"
+ "Checking wazuh-indexer status..."
+ )
+ status = (run_command("systemctl is-active wazuh-indexer") or "").strip()
+ context["indexer_status"] = status
+
+ if status != "active":
+ response["display"] += f"\n\n[WARNING] wazuh-indexer is {status.upper()}.\nWould you like me to restart it?"
+ response["ask"] = ["Restart indexer? (yes / no)"]
+ context["stage"] = "restart_indexer"
+ return response
+ else:
+ response["display"] += "\n\n[OK] wazuh-indexer is active.\nLet's check the disk space usage."
+ response["ask"] = ["Check disk space? (auto / manual)"]
+ context["stage"] = "disk_check"
+ return response
+
+ stage = context.get("stage")
+
+ if stage == "restart_indexer":
+ if user_choice and "yes" in user_choice.lower():
+ response["display"] = "Restarting wazuh-indexer service..."
+ run_command("systemctl restart wazuh-indexer")
+ time.sleep(3)
+ status = (run_command("systemctl is-active wazuh-indexer") or "").strip()
+ response["display"] += f"\n\nStatus after restart: {status.upper()}"
+ if status == "active":
+ response["display"] += "\n\nIndexer restarted successfully. Checking disk space now."
+ response["ask"] = ["Check disk space? (auto / manual)"]
+ context["stage"] = "disk_check"
+ return response
+ else:
+ response["display"] += "\n\nFailed to restart indexer. Please check system logs for issues."
+ return conclude(False, response["display"], context, topic="wazuh-indexer service fails to restart")
+ else:
+ response["display"] = "Skipped indexer restart. Checking disk space."
+ response["ask"] = ["Check disk space? (auto / manual)"]
+ context["stage"] = "disk_check"
+ return response
+
+ if stage == "disk_check":
+ if user_choice and "auto" in user_choice.lower():
+ df_out = run_command("df -h /var/lib/wazuh-indexer") or ""
+ response["display"] = f"Disk Space check output:\n\n{df_out}\n\n"
+ if "90%" in df_out or "95%" in df_out or "98%" in df_out or "99%" in df_out:
+ response["display"] += (
+ "[WARNING] Disk usage is critically high! Wazuh indexer blocks indexing if "
+ "disk watermark exceeds 90%.\n"
+ "Please delete old indices or expand storage."
+ )
+ else:
+ response["display"] += "[OK] Disk space looks acceptable."
+
+ response["ask"] = ["Run cluster health check? (yes / no)"]
+ context["stage"] = "cluster_health_check"
+ return response
+ else:
+ response["display"] = (
+ "Please run `df -h` on the indexer server and verify disk usage for /var/lib/wazuh-indexer/.\n"
+ "If usage is above 90%, clear indices or free up disk space."
+ )
+ response["ask"] = ["Is disk space sufficient? (yes / no)"]
+ context["stage"] = "disk_check_manual"
+ return response
+
+ if stage == "disk_check_manual":
+ if user_choice and "no" in user_choice.lower():
+ response["display"] = "Please free up disk space and try again."
+ return conclude(False, response["display"], context, topic="wazuh-indexer disk watermark disk space full")
+ else:
+ response["display"] = "Disk space verified. Moving to cluster health check."
+ response["ask"] = ["Run cluster health check? (yes / no)"]
+ context["stage"] = "cluster_health_check"
+ return response
+
+ if stage == "cluster_health_check":
+ if user_choice and "yes" in user_choice.lower():
+ cluster_out = indexer_api_get("/_cluster/health") or ""
+ response["display"] = f"Cluster Health Status:\n\n{cluster_out}\n\n"
+ if "red" in cluster_out.lower():
+ response["display"] += "The cluster health status is RED. This indicates that some primary shards are unassigned."
+ response["display"] += "\n\nTroubleshooting complete."
+ return conclude(False, response["display"], context, topic="wazuh indexer cluster health red unassigned shards")
+ elif "yellow" in cluster_out.lower():
+ response["display"] += "The cluster health status is YELLOW. This indicates that replica shards are unassigned."
+ response["display"] += "\n\nTroubleshooting complete."
+ return conclude(False, response["display"], context, topic="wazuh indexer cluster health yellow unassigned shards")
+ else:
+ response["display"] += "[OK] Cluster status is GREEN."
+ response["display"] += "\n\nTroubleshooting complete."
+ return conclude(True, response["display"], context)
+ else:
+ response["display"] = "Skipped cluster check."
+ response["display"] += "\n\nTroubleshooting complete."
+ return conclude(False, response["display"], context, topic="wazuh indexer indexing errors")
+
+ response["display"] = "Invalid stage."
+ response["done"] = True
+ return response
diff --git a/integrations/wazuh-troubleshooting-tool/backend/use_cases/mapping_issue.py b/integrations/wazuh-troubleshooting-tool/backend/use_cases/mapping_issue.py
new file mode 100644
index 00000000..1ceaf626
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/use_cases/mapping_issue.py
@@ -0,0 +1,339 @@
+"""
+Use case: 'Filebeat Mapping Issue' - field-mapping / index-template
+conflicts between what the Wazuh Indexer expects and what's actually being
+written (illegal_argument_exception, mapper_parsing_exception, a dashboard
+"N of M shards failed" popup, etc).
+
+Handed off to from use_cases/filebeat_error.py when the Filebeat log or
+`filebeat test output` output matches a known mapping-error signature, but
+also reachable directly as its own card.
+
+Diagnostic sequence, based on real support cases:
+ 1. Check whether the Wazuh Indexer actually has the default "wazuh"
+ index template registered, and that it's the canonical one (correct
+ index_patterns / settings) rather than missing or overridden by a
+ stray custom template.
+ 2. If a specific field was named in the error, check that field's type
+ in the live template.
+ 3. If it's missing/wrong, reinstall the canonical wazuh-template.json
+ and push it with `filebeat setup --index-management`.
+ 4. Offer to reindex any already-affected index (backup, delete,
+ restore, delete backup) so the fix also applies retroactively -
+ reinstalling the template alone only affects indices created after
+ the fix.
+"""
+
+from executor import run_command
+from utils.api_utils import indexer_api_get, indexer_api_get_json, indexer_api_delete
+from utils.index_utils import check_most_recent_index
+from utils.reindex_utils import reindex_for_mapping_conflict
+from utils.ai_utils import ai_explain
+from utils.unresolved_help import conclude
+
+WAZUH_TEMPLATE_URL = "https://raw.githubusercontent.com/wazuh/wazuh/v4.14.6/extensions/elasticsearch/7.x/wazuh-template.json"
+EXPECTED_INDEX_PATTERNS = {"wazuh-alerts-4.x-*", "wazuh-archives-4.x-*"}
+WAZUH_COMMUNITY_URL = "https://wazuh.com/community/"
+
+UNCLEAR_TEMPLATE_SYSTEM_PROMPT = (
+ "You are a Wazuh Indexer (OpenSearch) troubleshooting expert. You'll be given the live "
+ "'wazuh' index template (or its absence) and the list of registered templates. In 3-4 "
+ "short sentences: state the most likely root cause of a field-mapping conflict and the "
+ "single most useful next command or config fix. Be specific to what's actually in the "
+ "data - don't give generic advice."
+)
+
+
+def _dotted_get(d, dotted_key):
+ node = d
+ for part in dotted_key.split("."):
+ if not isinstance(node, dict) or part not in node:
+ return None
+ node = node[part]
+ return node
+
+
+def mapping_issue_flow(user_choice=None, context=None):
+ if context is None:
+ context = {}
+
+ response = {"display": "", "ask": [], "done": False, "context": context}
+ choice = (user_choice or "").strip().lower()
+
+ # START
+ if not context:
+ response["display"] = (
+ "Let's check whether this is a Wazuh Indexer field-mapping/index-template issue "
+ "rather than a Filebeat connectivity problem - this is what causes errors like "
+ "'illegal_argument_exception', 'mapper_parsing_exception', or a dashboard "
+ "'N of M shards failed' popup.\n\n"
+ "How would you like to check this?\n\n"
+ " Auto - We perform all checks automatically.\n"
+ " Manual - We provide the commands, and you run them and share the output."
+ )
+ response["ask"] = ["Auto", "Manual"]
+ context["stage"] = "method"
+ return response
+
+ stage = context.get("stage")
+
+ if stage == "method":
+ if "manual" in choice:
+ response["display"] = (
+ "Run these against the indexer (Indexer Management > Dev Tools, or curl):\n\n"
+ " GET /_template/wazuh\n"
+ " GET /_cat/templates\n\n"
+ "Does the 'wazuh' template exist, and does /_cat/templates show it covering "
+ f"{sorted(EXPECTED_INDEX_PATTERNS)}?"
+ )
+ response["ask"] = ["Yes, it looks correct", "No / missing / different"]
+ context["stage"] = "manual_result"
+ return response
+ return _check_template(response, context)
+
+ if stage == "manual_result":
+ if "yes" in choice:
+ return _ask_field_name(response, context)
+ return _offer_template_fix(response, context, reason="You reported the template is missing or incorrect.")
+
+ if stage == "field_name_wait":
+ return _check_field_type(response, context, user_choice)
+
+ if stage == "field_type_decision":
+ if "fix" in choice or "needs" in choice:
+ return _offer_template_fix(
+ response, context,
+ reason=f"'{context.get('mapping_field')}' needs its type corrected in the template.",
+ )
+ return _no_specific_field(response, context)
+
+ if stage == "fix_template":
+ if "auto" in choice:
+ return _auto_fix_template(response, context)
+ if "manual" in choice:
+ response["display"] = _manual_template_fix_instructions()
+ response["ask"] = ["Done"]
+ context["stage"] = "fix_template_manual_wait"
+ return response
+ response["ask"] = ["Auto", "Manual"]
+ return response
+
+ if stage == "fix_template_manual_wait":
+ response["display"] = "Template reinstalled (per your report). This only affects new indices going forward."
+ return _offer_reindex(response, context)
+
+ if stage == "reindex_method":
+ return _handle_reindex_method(response, context, choice)
+
+ if stage == "reindex_index_wait":
+ context["mapping_index"] = (user_choice or "").strip()
+ return _confirm_reindex(response, context)
+
+ if stage == "reindex_confirm":
+ if "skip" in choice or "no" in choice:
+ return conclude(True, "Skipping the reindex. The template fix is already in place.", context)
+ return _auto_reindex_one(response, context)
+
+ response["display"] = "Invalid stage."
+ response["done"] = True
+ return response
+
+
+# ---------------------------------------------------------------------------
+def _check_template(response, context):
+ response["display"] += ("\n" if response["display"] else "") + "Checking the live Wazuh Indexer template..."
+ templates_raw = indexer_api_get("/_cat/templates") or ""
+ template, _ = indexer_api_get_json("/_template/wazuh")
+ context["cat_templates"] = templates_raw
+
+ response["display"] += f"\n\n_cat/templates:\n{templates_raw or '(empty)'}"
+
+ wazuh_entry = (template or {}).get("wazuh")
+ if not wazuh_entry:
+ return _offer_template_fix(
+ response, context,
+ reason="The 'wazuh' index template is not registered on the indexer at all - "
+ "Filebeat's wazuh-template.json was never applied (or a different template "
+ "is overriding it).",
+ )
+
+ patterns_ok = EXPECTED_INDEX_PATTERNS.issubset(set(wazuh_entry.get("index_patterns", [])))
+ if not patterns_ok:
+ return _offer_template_fix(
+ response, context,
+ reason=f"A 'wazuh' template exists, but its index_patterns are "
+ f"{wazuh_entry.get('index_patterns')} instead of the expected "
+ f"{sorted(EXPECTED_INDEX_PATTERNS)} - this is a stray/custom template "
+ "overriding the real one.",
+ )
+
+ response["display"] += (
+ f"\n\n[OK] 'wazuh' template is registered with the expected index_patterns "
+ f"{sorted(EXPECTED_INDEX_PATTERNS)}."
+ )
+
+ total_fields_limit = _dotted_get(wazuh_entry, "settings.index.mapping.total_fields.limit")
+ if total_fields_limit and str(total_fields_limit) != "10000":
+ response["display"] += (
+ f"\n[WARNING] mapping.total_fields.limit is {total_fields_limit}, not the "
+ "documented 10000 - this can also be a symptom of a modified/outdated template."
+ )
+
+ context["wazuh_template"] = wazuh_entry
+ return _ask_field_name(response, context)
+
+
+def _ask_field_name(response, context):
+ response["display"] += (
+ "\n\nWhich field does the error mention (e.g. manager.name, cluster.name, or the "
+ "field named in the mapper_parsing_exception/illegal_argument_exception message)? "
+ "Type its dotted path, or 'skip' if you don't have one."
+ )
+ response["ask"] = []
+ context["stage"] = "field_name_wait"
+ return response
+
+
+def _check_field_type(response, context, field_name):
+ field_name = (field_name or "").strip()
+ if not field_name or field_name.lower() == "skip":
+ return _no_specific_field(response, context)
+
+ wazuh_entry = context.get("wazuh_template")
+ if wazuh_entry is None:
+ template, _ = indexer_api_get_json("/_template/wazuh")
+ wazuh_entry = (template or {}).get("wazuh", {})
+
+ dotted_path = f"mappings.properties.{field_name.replace('.', '.properties.')}.type"
+ field_type = _dotted_get(wazuh_entry, dotted_path)
+
+ response["display"] = f"Live template type for '{field_name}': {field_type or '(not found in the live template)'}"
+
+ if field_type is None:
+ return _offer_template_fix(
+ response, context,
+ reason=f"'{field_name}' isn't defined in the live 'wazuh' template at all - it's "
+ "likely relying on OpenSearch's dynamic mapping, which guessed a type that "
+ "doesn't match what's now being sent.",
+ )
+
+ response["display"] += (
+ "\n\nIf the error says this field should be a different type (commonly 'keyword' or "
+ "'object'), the live template needs to be corrected."
+ )
+ response["ask"] = ["Needs a different type - fix it", "This type is correct - something else is wrong"]
+ context["stage"] = "field_type_decision"
+ context["mapping_field"] = field_name
+ return response
+
+
+def _no_specific_field(response, context):
+ ai_text = ai_explain(UNCLEAR_TEMPLATE_SYSTEM_PROMPT, context.get("cat_templates", "")[:4000])
+ response["display"] += (
+ f"\n\nNo specific field to check further - here's an AI read of what we've gathered "
+ f"so far:\n{ai_text}"
+ )
+ return conclude(False, response["display"], context, topic="wazuh indexer mapping template field conflict")
+
+
+def _offer_template_fix(response, context, reason):
+ response["display"] += (
+ f"\n\n[ROOT CAUSE FOUND] Field-mapping / index-template issue\n\n{reason}\n\n"
+ "The fix is to reinstall the official Wazuh index template and push it via Filebeat. "
+ "This only affects indices created AFTER the fix - already-affected indices need a "
+ "separate reindex (offered next).\n\n"
+ "Would you like us to reinstall the template automatically, or do it yourself?"
+ )
+ response["ask"] = ["Auto", "Manual"]
+ context["stage"] = "fix_template"
+ return response
+
+
+def _manual_template_fix_instructions():
+ return (
+ "1. Back up the current template:\n\n"
+ " cp /etc/filebeat/wazuh-template.json /etc/filebeat/wazuh-template.json.backup\n\n"
+ "2. Remove any stray/incorrect template on the indexer (Dev Tools):\n\n"
+ " DELETE /_index_template/wazuh\n\n"
+ " (or DELETE /_template/wazuh if the cluster is still on the legacy templates API)\n\n"
+ "3. Install the official template on the Wazuh Manager/Filebeat host:\n\n"
+ f" curl -so /etc/filebeat/wazuh-template.json {WAZUH_TEMPLATE_URL}\n"
+ " chmod go+r /etc/filebeat/wazuh-template.json\n\n"
+ "4. Push it to the indexer and restart Filebeat:\n\n"
+ " filebeat setup --index-management\n"
+ " systemctl restart filebeat\n\n"
+ "This only affects new indices going forward - existing ones need a reindex."
+ )
+
+
+def _auto_fix_template(response, context):
+ log = []
+ log.append(run_command("cp /etc/filebeat/wazuh-template.json /etc/filebeat/wazuh-template.json.backup") or "")
+ log.append(indexer_api_delete("/_index_template/wazuh") or "")
+ log.append(run_command(f"curl -so /etc/filebeat/wazuh-template.json {WAZUH_TEMPLATE_URL}") or "")
+ log.append(run_command("chmod go+r /etc/filebeat/wazuh-template.json") or "")
+ log.append(run_command("filebeat setup --index-management") or "")
+ log.append(run_command("systemctl restart filebeat") or "")
+
+ response["display"] += "\n\nReinstalled the official Wazuh index template:\n" + "\n".join(l for l in log if l)
+
+ template, _ = indexer_api_get_json("/_template/wazuh")
+ wazuh_entry = (template or {}).get("wazuh")
+ if wazuh_entry and EXPECTED_INDEX_PATTERNS.issubset(set(wazuh_entry.get("index_patterns", []))):
+ response["display"] += "\n[OK] Verified - the 'wazuh' template now has the expected index_patterns."
+ else:
+ response["display"] += "\n[WARNING] The 'wazuh' template still doesn't look right after reinstalling - manual investigation needed."
+
+ return _offer_reindex(response, context)
+
+
+def _offer_reindex(response, context):
+ suggested = check_most_recent_index()
+ hint = f" (most recent: {suggested['index']})" if suggested.get("index") else ""
+ response["display"] += (
+ f"\n\nThe template fix only applies to new indices - any index that already has the "
+ f"field-mapping conflict needs to be reindexed (backup, delete, restore, delete "
+ f"backup) to pick up the corrected mapping{hint}.\n\n"
+ "Would you like to reindex an affected index now?"
+ )
+ response["ask"] = ["Yes, reindex one", "Skip"]
+ context["stage"] = "reindex_method"
+ return response
+
+
+def _handle_reindex_method(response, context, choice):
+ if "skip" in choice:
+ return conclude(True, "Skipping the reindex. The template fix is already in place.", context)
+
+ suggested = check_most_recent_index()
+ hint = f" (e.g. {suggested['index']})" if suggested.get("index") else ""
+ response["display"] = f"Which index would you like to reindex{hint}? Type the exact index name."
+ response["ask"] = []
+ context["stage"] = "reindex_index_wait"
+ return response
+
+
+def _confirm_reindex(response, context):
+ index_name = context.get("mapping_index", "")
+ response["display"] = (
+ f"This will back up '{index_name}', delete the original, restore from the backup with "
+ "the corrected mapping, then delete the backup. Proceed?"
+ )
+ response["ask"] = ["Yes, reindex", "Skip"]
+ context["stage"] = "reindex_confirm"
+ return response
+
+
+def _auto_reindex_one(response, context):
+ index_name = context.get("mapping_index", "")
+ steps = reindex_for_mapping_conflict(index_name)
+ aborted_after = steps.get("aborted_after")
+ if aborted_after:
+ detail = (steps.get(aborted_after) or "(no response)")[:200]
+ response["display"] = (
+ f"Stopped after '{aborted_after}' - nothing irreversible happened past that "
+ f"point. {detail}"
+ )
+ return conclude(False, response["display"], context, topic="wazuh reindex mapping conflict aborted " + aborted_after)
+
+ response["display"] = f"Reindexed {index_name} - it should now use the corrected mapping."
+ return conclude(True, response["display"], context)
diff --git a/integrations/wazuh-troubleshooting-tool/backend/use_cases/no_alerts_are_showing.py b/integrations/wazuh-troubleshooting-tool/backend/use_cases/no_alerts_are_showing.py
new file mode 100644
index 00000000..cd0e28d6
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/use_cases/no_alerts_are_showing.py
@@ -0,0 +1,1031 @@
+"""
+Use case: 'Alerts Not Showing on Dashboard'.
+
+Walks the full pipeline end to end and stops at the first failing part:
+ Wazuh Agent -> Wazuh Manager -> alerts.json -> Filebeat -> Wazuh Indexer -> Dashboard
+
+Single, self-contained troubleshooting script - built from small,
+single-purpose functions in utils/, sequencing logic lives here since
+this use case isn't shared with any other use case.
+
+Interaction pattern (per product spec):
+ - Every step explains WHY it's checking something before asking anything.
+ - Permission is asked for HOW TO CHECK itself (auto vs manual), not just
+ for fixes - "auto" means we run the checks/commands ourselves and
+ self-heal if something's off; "manual" means we hand the user the
+ exact commands/reference text to run themselves and wait for them to
+ report back.
+ - `ask` is ALWAYS a list of short, standalone options - never a single
+ string with multiple comma/parenthetical alternatives jammed together
+ (e.g. never "(ID/name, 'auto', or 'skip')"). Jamming options together
+ like that is what caused the UI to mis-split/truncate the question
+ into a garbled follow-up that then got echoed back as a literal,
+ unmatchable "agent name" in an earlier version of this script.
+"""
+
+import random
+import time
+
+from utils.service_utils import get_service_status, restart_service_and_wait
+from utils.agent_utils import list_active_agents, restart_agent
+from utils.manager_config_utils import (
+ get_log_alert_level, is_log_alert_level_ok, set_log_alert_level,
+ get_jsonout_output_enabled, enable_jsonout_output,
+)
+from utils.alerts_log_utils import alerts_json_mentions
+from utils.manager_log_utils import get_manager_log_errors, get_manager_disk_usage
+from utils.cluster_utils import get_cluster_status, get_cluster_health, get_write_blocks, clear_write_blocks
+from utils.shard_utils import get_node_count, get_unassigned_shards, explain_allocation
+from utils.replica_utils import recommend_replica_count, set_replica_count
+from utils.index_utils import index_has_todays_date, check_most_recent_index, check_index_name_freshness
+from utils.reindex_utils import reindex_for_mapping_conflict
+from utils.api_utils import indexer_api_delete
+from utils.ai_utils import ai_explain
+from utils.log_handler import LogHandler
+from utils.log_analyzer import LogAnalyzer
+from utils.fix_engine import FixEngine
+from utils.unresolved_help import conclude
+from config import INDEXER_URL
+from flows.filebeat_flow import (
+ filebeat_flow, STAGES as FILEBEAT_STAGES, STEP4_ENTRY_STAGE,
+ ENTRY_STAGE as FILEBEAT_ENTRY_STAGE, WHY_TEXT as FILEBEAT_WHY_TEXT,
+)
+
+MANAGER_LOG_SYSTEM_PROMPT = (
+ "You are a Wazuh Manager troubleshooting expert. You'll be given recent "
+ "ossec.log error/warning lines. In 3-4 short sentences: state the most "
+ "likely root cause and the single most useful next command or config fix. "
+ "Be specific to what's actually in the log - don't give generic advice."
+)
+
+UNCLEAR_CLUSTER_STATUS_SYSTEM_PROMPT = (
+ "You are a Wazuh Indexer (OpenSearch) troubleshooting expert. You'll be given the raw "
+ "_cluster/health response for a cluster whose status is yellow or red even though there "
+ "are no unassigned shards and no known write blocks. In 3-4 short sentences: state the "
+ "most likely explanation and the single most useful next command to investigate further. "
+ "Be specific to what's actually in the data - don't give generic advice."
+)
+
+STEP1_MANUAL_TEXT = (
+ "Please check whether the manager is active and paste the output of:\n\n"
+ " systemctl status wazuh-manager\n\n"
+ "Next, check whether log_alert_level is configured correctly. On the "
+ "Wazuh manager server, open:\n\n"
+ " /var/ossec/etc/ossec.conf\n\n"
+ "Ensure it contains:\n\n"
+ " 3\n\n"
+ "If log_alert_level is set higher than 15, alerts with rule level 15 "
+ "or below will not be written by the manager and therefore will not "
+ "appear on the dashboard. See the Wazuh docs for details:\n"
+ "https://documentation.wazuh.com/current/user-manual/reference/ossec-conf/alerts.html#log-alert-level\n\n"
+ "Also verify that the following setting is enabled:\n\n"
+ " yes\n\n"
+ "https://documentation.wazuh.com/current/user-manual/reference/ossec-conf/global.html#jsonout-output\n\n"
+ "If jsonout_output is disabled, the manager will not write alerts to "
+ "alerts.json, so Filebeat will have no alerts to send to the indexer."
+)
+
+STEP4_MANUAL_TEXT = (
+ "Please check whether the indexer is active and paste the output of:\n\n"
+ " systemctl status wazuh-indexer\n\n"
+ "Next, check that its configured IP matches the original install config:\n\n"
+ " grep network.host /etc/wazuh-indexer/opensearch.yml\n\n"
+ "Also check that its certificate paths point to files that actually exist:\n\n"
+ " grep -E 'pemkey_filepath|pemcert_filepath|pemtrustedcas_filepath' /etc/wazuh-indexer/opensearch.yml\n"
+ " ls /etc/wazuh-indexer/certs\n\n"
+ "Every path from the first command should exist in the second command's listing. If the "
+ "IP or a cert path is wrong, the indexer can be 'active' while still rejecting or "
+ "misplacing data."
+)
+
+RECENT_INDEX_MANUAL_TEXT = (
+ "Run this against the indexer:\n\n"
+ f" curl -XGET -k -u admin: \"{INDEXER_URL}/_cat/indices/wazuh-alerts-*?v&s=index\"\n\n"
+ "Find the most recent wazuh-alerts-* index in the list (the one with the highest date "
+ "suffix), then type or paste that index name below so we can compare its date to today."
+)
+
+STEP5_MANUAL_TEXT = (
+ "Run this against the indexer:\n\n"
+ f" curl -XGET -k -u admin: \"{INDEXER_URL}/_cluster/health?pretty\"\n\n"
+ "Check the \"status\" field - it should be \"green\". If it's \"yellow\" or \"red\", there "
+ "are unassigned shards, usually because the number of replicas doesn't fit the number of "
+ "nodes in this cluster.\n\n"
+ "For the actual reason a specific shard won't allocate (not just the terse reason code), run:\n\n"
+ f" curl -XGET -k -u admin: \"{INDEXER_URL}/_cluster/allocation/explain?pretty\"\n\n"
+ "With no body, this explains an arbitrary unassigned shard the indexer picks itself."
+)
+
+STEP6_MANUAL_TEXT = (
+ "Check whether there's a wazuh-alerts-* index for today by running this against the indexer:\n\n"
+ f" curl -XGET -k -u admin: \"{INDEXER_URL}/_cat/indices/wazuh-alerts-*?v\"\n\n"
+ "Look for an index whose date suffix matches today. If there isn't one, alerts have "
+ "stopped being indexed recently even though the rest of the pipeline checks out."
+)
+
+MANUAL_DELETE_UNASSIGNED_TEXT = (
+ "WARNING - this permanently deletes every index that currently has an unassigned "
+ "shard. There is no backup step here - only run this if you don't need the data in "
+ "those indices. If you do need it, go back and use the reindex option instead.\n\n"
+ " curl -XGET -k -u admin: \"https://:9200/_cat/shards\" "
+ "| grep UNASSIGNED | awk '{print $1}' | sort -u "
+ "| xargs -I{} curl -XDELETE -k -u admin: \"https://:9200/{}\"\n\n"
+ "Once you've run it (or decided not to), let us know."
+)
+
+SAMPLE_AGENT_STARTED_ALERT = (
+ '{"timestamp":"2026-07-05T10:44:10.016+0000","rule":{"level":3,'
+ '"description":"Wazuh agent started.","id":"503",...},'
+ '"agent":{"id":"001","name":"windows","ip":"192.168.56.1"},'
+ '"manager":{"name":"Server1"},...,'
+ '"full_log":"ossec: Agent started: \'windows->any\'.",...}'
+)
+
+
+def _stop(response, context, title, explanation, manual_fix):
+ response["display"] = f"[ROOT CAUSE FOUND] {title}\n\n{explanation}\n\nManual fix:\n{manual_fix}"
+ return conclude(False, response["display"], context, topic=f"wazuh alerts not showing {title}")
+
+
+def no_alerts_are_showing_flow(user_choice=None, context=None):
+ if context is None:
+ context = {}
+
+ response = {"display": "", "ask": [], "done": False, "context": context}
+ choice = (user_choice or "").strip().lower()
+
+ # =====================================================================
+ # STEP 1 - Manager service + ossec.conf config
+ # =====================================================================
+ if not context:
+ response["display"] = (
+ "Let's troubleshoot 'Alerts Not Showing on Dashboard'.\n\n"
+ "Step 1 - Check the Wazuh Manager service and its configuration:\n\n"
+ "We first need to verify that the Wazuh Manager is running "
+ "correctly and is configured to generate alerts. If "
+ "log_alert_level is set too high, or jsonout_output is "
+ "disabled, alerts get silently dropped before they're ever "
+ "written to alerts.json - so this has to be ruled out first.\n\n"
+ "How would you like us to check this?\n\n"
+ " Auto - We perform all checks automatically.\n"
+ " Manual - We provide the commands, and you run them and "
+ "share the output."
+ )
+ response["ask"] = ["Auto", "Manual"]
+ context["stage"] = "step1_method"
+ return response
+
+ stage = context.get("stage")
+
+ # =====================================================================
+ # STEP 3 - Filebeat (fully owned by flows/filebeat_flow.py)
+ # =====================================================================
+ if stage in FILEBEAT_STAGES:
+ result = filebeat_flow(user_choice=choice, context=context)
+ if result.get("handoff"):
+ next_result = no_alerts_are_showing_flow(context=result["context"])
+ next_display = next_result["display"].lstrip("\n")
+ next_result["display"] = (
+ f"{result['display']}\n\n{next_display}" if result.get("display") else next_display
+ )
+ return next_result
+ return result
+
+ if stage == STEP4_ENTRY_STAGE:
+ return _start_step4(response, context)
+
+ if stage == "step4_method":
+ if "manual" in choice:
+ response["display"] = STEP4_MANUAL_TEXT
+ response["ask"] = ["Correct", "Needs fixing"]
+ context["stage"] = "step4_manual_result"
+ return response
+
+ # AUTO - run the real checks ourselves.
+ return _check_indexer(response, context)
+
+ if stage == "step4_manual_result":
+ # Independently verify regardless of what the user reported, same as Step 1.
+ return _check_indexer(response, context)
+
+ if stage == "recent_index_method":
+ if "manual" in choice:
+ response["display"] = RECENT_INDEX_MANUAL_TEXT
+ response["ask"] = []
+ context["stage"] = "recent_index_manual_wait"
+ return response
+
+ # AUTO - look the most recent index up ourselves.
+ return _check_recent_index(response, context)
+
+ if stage == "recent_index_manual_wait":
+ # Whatever the user typed/pasted is the index name - parse its date
+ # suffix ourselves rather than trusting a self-report, same spirit as
+ # the other steps' independent re-verification.
+ result = check_index_name_freshness(user_choice)
+ response["display"] = _format_recent_index_result(result)
+ return _start_step5(response, context)
+
+ if stage == "step5_method":
+ if "manual" in choice:
+ response["display"] = STEP5_MANUAL_TEXT
+ response["ask"] = ["Correct", "Needs fixing"]
+ context["stage"] = "step5_manual_result"
+ return response
+
+ # AUTO - run the real check ourselves.
+ return _check_cluster_and_shards(response, context)
+
+ if stage == "step5_manual_result":
+ # Independently verify regardless of what the user reported, same as Step 1.
+ return _check_cluster_and_shards(response, context)
+
+ if stage == "step6_method":
+ if "manual" in choice:
+ response["display"] = STEP6_MANUAL_TEXT
+ response["ask"] = ["Correct", "Needs fixing"]
+ context["stage"] = "step6_manual_result"
+ return response
+
+ # AUTO - run the real check ourselves.
+ return _check_indices(response, context)
+
+ if stage == "step6_manual_result":
+ # Independently verify regardless of what the user reported, same as Step 1.
+ return _check_indices(response, context)
+
+ # =====================================================================
+ # Post-pipeline: everything checked out, but the user still has the
+ # complaint - narrow down whether this is a broken pipeline (all
+ # alerts missing, already covered above) or a mapping/rule issue
+ # specific to one alert (a different root cause entirely).
+ # =====================================================================
+ if stage == "step6_scope_check":
+ if "particular" in choice:
+ response["display"] = (
+ "On the Wazuh Manager, check whether that specific alert reached alerts.json:\n\n"
+ " grep -i '' /var/ossec/logs/alerts/alerts.json\n\n"
+ "Does it appear there?"
+ )
+ response["ask"] = ["Yes, it's in alerts.json", "No, it's not there"]
+ context["stage"] = "step6_particular_check"
+ return response
+
+ if "all of today" in choice:
+ response["display"] = (
+ "If every step in this pipeline checked out but ALL of today's alerts are "
+ "still missing, that points to the dashboard side rather than the indexing "
+ "pipeline itself: double-check the dashboard's index pattern (should match "
+ "wazuh-alerts-*) and its time range filter (set to include today). If those "
+ "look correct, re-check the cluster health and Filebeat logs from the earlier "
+ "steps for anything that changed right before this started."
+ )
+ return conclude(False, response["display"], context, topic="wazuh alerts not showing pipeline checks out but alerts still missing")
+
+ return conclude(True, "Good - the full pipeline checks out end to end.", context)
+
+ if stage == "step6_particular_check":
+ if "yes" in choice:
+ index_name = check_most_recent_index()["index"] or "wazuh-alerts-*"
+ response["display"] = (
+ "That alert is reaching alerts.json but not showing on the dashboard - this "
+ "points to a field-mapping conflict on today's index, not a pipeline failure. "
+ "Would you like us to fix this by reindexing that index (same backup/restore "
+ "procedure used for unassigned shards earlier), or would you rather do it "
+ "yourself?"
+ )
+ response["ask"] = ["Auto", "Manual"]
+ context["stage"] = "step6_mapping_fix"
+ context["mapping_conflict_index"] = index_name
+ return response
+
+ response["display"] = (
+ "That alert never reached alerts.json, so this isn't an indexing/dashboard "
+ "problem - the cause is further upstream. Go back to Step 1 (log_alert_level / "
+ "jsonout_output) and check whether a rule is filtering it out, or whether the "
+ "source log is reaching the manager at all."
+ )
+ return conclude(False, response["display"], context, topic="wazuh alert not reaching alerts.json rule filtering")
+
+ if stage == "step6_mapping_fix":
+ index_name = context.get("mapping_conflict_index", "wazuh-alerts-*")
+ if "manual" in choice:
+ response["display"] = _manual_reindex_single_index_instructions(index_name)
+ response["ask"] = ["Done"]
+ context["stage"] = "step6_mapping_fix_manual_wait"
+ return response
+
+ if "auto" in choice:
+ steps = reindex_for_mapping_conflict(index_name)
+ aborted_after = steps.get("aborted_after")
+ if aborted_after:
+ detail = (steps.get(aborted_after) or "(no response)")[:200]
+ response["display"] = (
+ f"Stopped after '{aborted_after}' - nothing irreversible happened past "
+ f"that point. {detail}"
+ )
+ return conclude(False, response["display"], context, topic="wazuh reindex mapping conflict aborted " + aborted_after)
+ response["display"] = f"Reindexed {index_name} - the mapping conflict should be resolved now."
+ return conclude(True, response["display"], context)
+
+ response["ask"] = ["Auto", "Manual"]
+ return response
+
+ if stage == "step6_mapping_fix_manual_wait":
+ return conclude(True, "Done - the mapping conflict should be resolved now.", context)
+
+ if stage == "step1_method":
+ if "manual" in choice:
+ response["display"] = STEP1_MANUAL_TEXT
+ response["ask"] = ["Correct", "Needs fixing"]
+ context["stage"] = "step1_manual_result"
+ return response
+
+ # AUTO - run it ourselves, self-heal if needed, report and move on.
+ return _auto_check_step1(response, context)
+
+ if stage == "step1_manual_result":
+ # Independently verify regardless of what the user reported, so we
+ # can move on with confidence either way.
+ if is_log_alert_level_ok() and get_jsonout_output_enabled() and get_service_status("wazuh-manager") == "active":
+ response["display"] = "[OK] Confirmed - the manager is active and configured correctly."
+ return _start_step2(response, context)
+
+ response["display"] = (
+ "We're still seeing an issue with the manager service or its "
+ "configuration. Would you like us to fix it automatically, or "
+ "will you fix it yourself and let us know when done?"
+ )
+ response["ask"] = ["Auto", "I'll fix it myself"]
+ context["stage"] = "step1_fix_choice"
+ return response
+
+ if stage == "step1_fix_choice":
+ if "auto" in choice:
+ return _auto_check_step1(response, context, already_explained=True)
+
+ response["display"] = STEP1_MANUAL_TEXT + "\n\nOnce you've made the changes, let us know."
+ response["ask"] = ["Done"]
+ context["stage"] = "step1_manual_result"
+ return response
+
+ # =====================================================================
+ # STEP 2 - Live agent-restart pipeline test
+ # =====================================================================
+ if stage == "step2_method":
+ active = list_active_agents()
+ context["active_agents"] = active
+
+ if not active:
+ return _stop(
+ response, context, "No active agents",
+ "There are no active agents connected to this manager (agent 000, the "
+ "manager's own local agent, doesn't count), so there's nothing to "
+ "generate an event for this test.",
+ "Check agent connectivity/network from at least one endpoint, then re-run this workflow.",
+ )
+
+ if "manual" in choice:
+ listing = "\n".join(f" - {a['id']}: {a['name']}" for a in active[:10])
+ response["display"] = (
+ f"Active agents (excluding 000, the manager itself):\n{listing}\n\n"
+ "1. Pick one agent ID from the list above.\n"
+ "2. Restart it from the manager:\n"
+ " /var/ossec/bin/agent_control -R -u \n\n"
+ "3. Wait a few seconds, then check alerts.json for a matching "
+ "'Wazuh agent started' event, e.g.:\n"
+ f" {SAMPLE_AGENT_STARTED_ALERT}\n\n"
+ "Did you see a matching alert in alerts.json?"
+ )
+ response["ask"] = ["Yes, I see it", "No, nothing there"]
+ context["stage"] = "step2_manual_result"
+ return response
+
+ # AUTO - explain, pick a random active agent, restart it, and verify ourselves.
+ target = random.choice(active)
+ context["target_agent"] = target
+ response["display"] = (
+ f"We're restarting agent '{target['name']}' (ID {target['id']}) to generate "
+ "a known alert. If this alert appears in alerts.json, it confirms that the "
+ "Wazuh Manager is correctly receiving events, processing them, and generating "
+ "alerts."
+ )
+ out = restart_agent(target["id"])
+ time.sleep(5)
+ found = alerts_json_mentions(target["id"]) or alerts_json_mentions(target["name"])
+
+ response["display"] += f"\n\nRan: agent_control -R -u {target['id']}\n{out}\n"
+ if found:
+ response["display"] += (
+ f"[OK] Found a matching entry in alerts.json for agent '{target['name']}' - "
+ "the manager is receiving and logging agent events."
+ )
+ return _start_step3(response, context)
+
+ return _step2_no_alert_found(response, context)
+
+ if stage == "step2_manual_result":
+ if "yes" in choice:
+ response["display"] = "[OK] Good - that confirms the manager is receiving agent events."
+ return _start_step3(response, context)
+ return _step2_no_alert_found(response, context)
+
+ # =====================================================================
+ # STEP 4 fix gate - Indexer down
+ # =====================================================================
+ if stage == "fix_indexer_start":
+ if "auto" in choice:
+ status = restart_service_and_wait("wazuh-indexer")
+ elif choice == "done":
+ status = get_service_status("wazuh-indexer")
+ elif "manual" in choice:
+ response["display"] = "Run: systemctl restart wazuh-indexer"
+ response["ask"] = ["Done"]
+ context["stage"] = "fix_indexer_start"
+ return response
+ else:
+ response["ask"] = ["Auto", "Manual"]
+ return response
+
+ if status == "active":
+ response["display"] = "[OK] wazuh-indexer is now active."
+ return _check_indexer_ip_and_certs(response, context)
+
+ response["display"] = f"wazuh-indexer still shows {status.upper()}."
+ return _diagnose_indexer_logs(response, context)
+
+ # =====================================================================
+ # STEP 5 fix gate - cluster-wide write/index-creation blocks
+ # =====================================================================
+ if stage == "fix_write_blocks":
+ block_names = context.get("write_blocks", [])
+ if "auto" in choice:
+ raw = clear_write_blocks(block_names)
+ response["display"] = f"Cleared {len(block_names)} block(s):\n{', '.join(block_names)}\n{raw}"
+ return _check_cluster_and_shards(response, context)
+
+ if "manual" in choice:
+ response["display"] = _manual_clear_blocks_instructions(block_names)
+ response["ask"] = ["Done"]
+ context["stage"] = "fix_write_blocks_manual_wait"
+ return response
+
+ response["ask"] = ["Auto", "Manual"]
+ return response
+
+ if stage == "fix_write_blocks_manual_wait":
+ return _check_cluster_and_shards(response, context)
+
+ # =====================================================================
+ # STEP 5 fix gate - unassigned shards / replicas
+ # =====================================================================
+ if stage == "fix_replicas":
+ if "auto" in choice:
+ raw = set_replica_count("wazuh-alerts-*", context["recommended_replicas"])
+ response["display"] = f"Set number_of_replicas={context['recommended_replicas']} on wazuh-alerts-*.\n{raw}"
+ elif "manual" in choice:
+ response["display"] = (
+ "Run against the indexer:\n\n"
+ " curl -k -u \":\" -XPUT "
+ "\"https://:9200/wazuh-alerts-*\" -H 'Content-Type: application/json' -d'\n"
+ " {\n"
+ " \"settings\": {\n"
+ f" \"index\": {{ \"number_of_replicas\": {context['recommended_replicas']} }}\n"
+ " }\n"
+ " }'"
+ )
+ else:
+ response["ask"] = ["Auto", "Manual"]
+ return response
+ return _offer_reindex(response, context)
+
+ if stage == "reindex_method":
+ indices = context.get("unassigned_indices", [])
+ if "skip" in choice:
+ response["display"] = "Skipping the reindex."
+ return _start_step6(response, context)
+
+ if "delete" in choice:
+ return _confirm_delete_unassigned(response, context)
+
+ if "manual" in choice:
+ response["display"] = _manual_reindex_instructions(indices)
+ response["ask"] = ["Done", "Skip"]
+ context["stage"] = "reindex_manual_wait"
+ return response
+
+ if "auto" in choice:
+ return _auto_reindex(response, context)
+
+ response["ask"] = ["Auto (reindex, keeps data)", "Manual", "Delete instead", "Skip"]
+ return response
+
+ if stage == "confirm_delete_unassigned":
+ if "back" in choice or "no" in choice:
+ return _offer_reindex(response, context)
+
+ if "show" in choice or "command" in choice:
+ response["display"] = MANUAL_DELETE_UNASSIGNED_TEXT
+ response["ask"] = ["Done", "Skip"]
+ context["stage"] = "delete_unassigned_manual_wait"
+ return response
+
+ if "yes" in choice:
+ return _delete_unassigned_indices(response, context)
+
+ response["ask"] = ["Yes, delete via Auto", "Yes, show me the command", "No, go back"]
+ return response
+
+ if stage == "delete_unassigned_manual_wait":
+ if "skip" in choice:
+ response["display"] = "Skipping the verification."
+ return _start_step6(response, context)
+ return _verify_and_start_step6(response, context)
+
+ if stage == "reindex_manual_wait":
+ if "skip" in choice:
+ response["display"] = "Skipping the verification."
+ return _start_step6(response, context)
+ return _verify_and_start_step6(response, context)
+
+ response["display"] = "Invalid stage."
+ response["done"] = True
+ return response
+
+
+# ---------------------------------------------------------------------------
+# Step 1 helpers
+# ---------------------------------------------------------------------------
+def _auto_check_step1(response, context, already_explained=False):
+ if not already_explained:
+ response["display"] = (
+ "Automatically checking:\n"
+ " - whether the wazuh-manager service is active\n"
+ " - whether log_alert_level is configured correctly\n"
+ " - whether jsonout_output is enabled"
+ )
+
+ status = get_service_status("wazuh-manager")
+ if status != "active":
+ status = restart_service_and_wait("wazuh-manager")
+ if status != "active":
+ return _stop(
+ response, context, "Wazuh Manager failed to start",
+ "wazuh-manager did not come back up after a restart.",
+ "Check `journalctl -u wazuh-manager` and `/var/ossec/logs/ossec.log` for startup errors.",
+ )
+
+ fixed_something = False
+ if not is_log_alert_level_ok():
+ set_log_alert_level(3)
+ fixed_something = True
+ if not get_jsonout_output_enabled():
+ enable_jsonout_output()
+ fixed_something = True
+
+ if fixed_something:
+ restart_service_and_wait("wazuh-manager")
+
+ if is_log_alert_level_ok() and get_jsonout_output_enabled():
+ if fixed_something:
+ response["display"] += "\n[OK] Found and corrected a config issue, then restarted wazuh-manager."
+ else:
+ response["display"] += "\n[OK] The manager is active and already configured correctly."
+ return _start_step2(response, context)
+
+ errors = get_manager_log_errors()
+ explanation = ai_explain(MANAGER_LOG_SYSTEM_PROMPT, errors) if errors.strip() else \
+ "No error/warning lines found in ossec.log to analyze further."
+ return _stop(
+ response, context, "Manager configuration still not correct after auto-fix",
+ f"Something beyond the two known settings appears to be wrong.\n\n"
+ f"AI analysis of recent ossec.log errors:\n{explanation}",
+ "Review ossec.conf and ossec.log manually using the analysis above as a starting point.",
+ )
+
+
+def _start_step2(response, context):
+ response["display"] += (
+ "\n\nStep 2 - Verify that the Manager is Receiving Alerts:\n\n"
+ "The easiest way to confirm this is to restart an active agent and verify "
+ "that the manager writes a Rule 503 - Wazuh agent started event into "
+ "alerts.json.\n\n"
+ "How would you like to do this?"
+ )
+ response["ask"] = ["Auto", "Manual"]
+ context["stage"] = "step2_method"
+ return response
+
+
+def _step2_no_alert_found(response, context):
+ disk = get_manager_disk_usage()
+ errors = get_manager_log_errors()
+ response["display"] += (
+ "\n[WARNING] No matching entry found in alerts.json after restarting the agent.\n\n"
+ f"Manager disk usage:\n{disk}\n\nRecent ossec.log errors/warnings:\n"
+ f"{errors if errors else '(none found)'}"
+ )
+ return conclude(False, response["display"], context, topic="wazuh manager not receiving agent events alerts.json")
+
+
+# ---------------------------------------------------------------------------
+# Steps 4-6 (unchanged logic from before, only the ask-list formatting differs)
+# ---------------------------------------------------------------------------
+def _start_step3(response, context):
+ response["display"] += (
+ "\n\nStep 3 - Check Filebeat:\n\n"
+ f"{FILEBEAT_WHY_TEXT}\n\n"
+ "How would you like to check this?\n\n"
+ " Auto - We perform all checks automatically.\n"
+ " Manual - We provide the commands, and you run them and share the output."
+ )
+ response["ask"] = ["Auto", "Manual"]
+ context["stage"] = FILEBEAT_ENTRY_STAGE
+ return response
+
+
+def _start_step4(response, context):
+ response["display"] += (
+ "\n\nStep 4 - Check the Wazuh Indexer:\n\n"
+ "Filebeat can only ship alerts as far as the Wazuh Indexer is actually running "
+ "and correctly configured to accept them. We need to confirm the service is "
+ "active, and that its IP and certificate configuration are correct - the same "
+ "checks used for the 'Wazuh dashboard is not ready yet' troubleshooting flow.\n\n"
+ "How would you like to check this?\n\n"
+ " Auto - We perform all checks automatically.\n"
+ " Manual - We provide the commands, and you run them and share the output."
+ )
+ response["ask"] = ["Auto", "Manual"]
+ context["stage"] = "step4_method"
+ return response
+
+
+def _check_indexer(response, context):
+ response["display"] += ("\n" if response["display"] else "") + "Checking Wazuh Indexer..."
+ status = get_service_status("wazuh-indexer")
+ if status != "active":
+ response["display"] += "\n[WARNING] wazuh-indexer is not active."
+ response["ask"] = ["Auto", "Manual"]
+ context["stage"] = "fix_indexer_start"
+ return response
+ response["display"] += "\n[OK] wazuh-indexer is active."
+ return _check_indexer_ip_and_certs(response, context)
+
+
+def _check_indexer_ip_and_certs(response, context):
+ ip_data = FixEngine.check_indexer_ip()
+ fixed_ip = False
+ if not ip_data["match"] and ip_data.get("c_ip"):
+ FixEngine.fix_indexer_ip(ip_data["c_ip"])
+ fixed_ip = True
+
+ cert_data = FixEngine.check_indexer_cert_paths()
+ fixed_cert = False
+ cert_unfixable = False
+ if cert_data["missing"]:
+ result = FixEngine.fix_indexer_cert_paths()
+ if result.get("success"):
+ fixed_cert = True
+ else:
+ cert_unfixable = True
+
+ if fixed_ip or fixed_cert:
+ response["display"] += "\n[OK] Found and corrected indexer IP/certificate configuration issues."
+ else:
+ response["display"] += "\n[OK] Indexer IP and certificate configuration look correct."
+
+ if cert_unfixable:
+ response["display"] += (
+ "\n[WARNING] Could not auto-identify the correct certificate files. Checking "
+ "the indexer logs to help narrow this down..."
+ )
+ return _diagnose_indexer_logs(response, context)
+
+ return _start_recent_index_check(response, context)
+
+
+def _start_recent_index_check(response, context):
+ response["display"] += (
+ "\n\nCheck for a Recently Created Index:\n\n"
+ "Before diving into cluster health, it's worth checking whether the indexer has "
+ "actually created a new wazuh-alerts-* index recently. If the newest one is old, "
+ "that's an early sign writes have stalled somewhere upstream, even though the "
+ "service itself is up.\n\n"
+ "How would you like to check this?\n\n"
+ " Auto - We perform all checks automatically.\n"
+ " Manual - We provide the commands, and you run them and share the output."
+ )
+ response["ask"] = ["Auto", "Manual"]
+ context["stage"] = "recent_index_method"
+ return response
+
+
+def _check_recent_index(response, context):
+ text = _format_recent_index_result(check_most_recent_index())
+ response["display"] += text if response["display"] else text.lstrip("\n")
+ return _start_step5(response, context)
+
+
+def _format_recent_index_result(result):
+ if not result["index"]:
+ return (
+ "\n[WARNING] Could not find any wazuh-alerts-* index with a parseable date - "
+ "moving on to check cluster health, which may explain why."
+ )
+ if result["is_today"]:
+ return f"\n[OK] Most recent index is {result['index']} (today)."
+ return (
+ f"\n[WARNING] Most recent index is {result['index']} - {result['days_old']} day(s) old, "
+ "not today. New alerts may have stopped being indexed recently. Continuing on to check "
+ "cluster health, which may explain why."
+ )
+
+
+def _start_step5(response, context):
+ response["display"] += (
+ "\n\nStep 5 - Check Cluster Health:\n\n"
+ "Now that the indexer is confirmed running and correctly configured, we check "
+ "cluster health and shard allocation - a red/yellow cluster or unassigned shards "
+ "will keep alerts from being indexed even though every earlier step passed.\n\n"
+ "How would you like to check this?\n\n"
+ " Auto - We perform all checks automatically.\n"
+ " Manual - We provide the commands, and you run them and share the output."
+ )
+ response["ask"] = ["Auto", "Manual"]
+ context["stage"] = "step5_method"
+ return response
+
+
+def _offer_clear_write_blocks(response, context, blocks):
+ listing = "\n".join(f" - {name} = {value}" for name, value in blocks.items())
+ response["display"] += (
+ f"\n[ISSUE] Found cluster-wide write/index-creation block(s):\n{listing}\n\n"
+ "These silently prevent new indices (including today's wazuh-alerts-*) from being "
+ "created or written to, even while every other check looks healthy - this is exactly "
+ "what caused an earlier data-loss incident during reindexing.\n\n"
+ "Would you like us to clear these, or will you clear them yourself?"
+ )
+ response["ask"] = ["Auto", "Manual"]
+ context["stage"] = "fix_write_blocks"
+ context["write_blocks"] = list(blocks.keys())
+ return response
+
+
+def _manual_clear_blocks_instructions(block_names):
+ lines = ",\n".join(f' "{name}": null' for name in block_names)
+ return (
+ "Run this against the indexer to clear the block(s):\n\n"
+ f" curl -XPUT -k -u admin: \"{INDEXER_URL}/_cluster/settings\" "
+ "-H 'Content-Type: application/json' -d'\n"
+ " {\n"
+ " \"persistent\": {\n"
+ f"{lines}\n"
+ " },\n"
+ " \"transient\": {\n"
+ f"{lines}\n"
+ " }\n"
+ " }'\n\n"
+ "Once you've run it, let us know."
+ )
+
+
+def _diagnose_indexer_logs(response, context):
+ logs = LogHandler.get_indexer_logs(2)
+ clean = LogHandler.clean_logs(logs)
+ issues = LogAnalyzer.get_issues(logs)
+
+ response["display"] += f"\n\nRecent indexer logs:\n{clean}"
+ if issues:
+ response["display"] += "\n\nKnown issues detected:\n" + "\n".join(f"- {i}" for i in issues)
+ else:
+ response["display"] += "\n\nNo known issue pattern matched - manual review of the logs above is needed."
+
+ return conclude(False, response["display"], context, topic="wazuh indexer log issues " + " ".join(issues))
+
+
+def _check_cluster_and_shards(response, context):
+ response["display"] += ("\n" if response["display"] else "") + "Checking cluster health and shards..."
+
+ # Known cluster-wide write/index-creation blocks (e.g. a stale
+ # cluster.blocks.create_index) are checked BEFORE cluster status/shards,
+ # since a block can silently sit alongside an otherwise-green cluster -
+ # this is exactly what caused the reindex data-loss incident, and a
+ # plain _cluster/health check alone would never have surfaced it.
+ block_result = get_write_blocks()
+ if block_result.get("error"):
+ response["display"] += f"\n[WARNING] Could not check cluster write blocks: {str(block_result['error'])[:200]}"
+ elif block_result.get("blocks"):
+ return _offer_clear_write_blocks(response, context, block_result["blocks"])
+ else:
+ response["display"] += "\n[OK] No cluster-wide write/index-creation blocks found."
+
+ status = get_cluster_status()
+
+ if status is None:
+ return _stop(
+ response, context, "Wazuh Indexer API is unreachable",
+ "Could not query /_cluster/health.",
+ "Verify INDEXER_URL/credentials and that port 9200 is reachable.",
+ )
+
+ response["display"] += f"\nCluster status: {status.upper()}"
+
+ if status == "green":
+ response["display"] += "\n[OK] Cluster is green."
+ return _start_step6(response, context)
+
+ unassigned = get_unassigned_shards()
+ if not unassigned:
+ # No known scripted cause (no write blocks, no unassigned shards) but
+ # the cluster still isn't green - this is the one case we hand to the
+ # AI rather than guess at more hardcoded rules, same fallback pattern
+ # used for Filebeat's "unknown" failure category.
+ _, raw_health = get_cluster_health()
+ explanation = ai_explain(UNCLEAR_CLUSTER_STATUS_SYSTEM_PROMPT, raw_health or "(no response)")
+ response["display"] += (
+ f"\n[WARNING] No unassigned shards and no known write blocks, but status is still "
+ f"{status.upper()}.\n\nAI analysis:\n{explanation}"
+ )
+ return _start_step6(response, context)
+
+ node_count, _ = get_node_count()
+ recommended = recommend_replica_count(node_count)
+ context["recommended_replicas"] = recommended
+ context["unassigned_indices"] = sorted({s["index"] for s in unassigned})
+
+ sample = "\n".join(f" - {s['index']} shard {s['shard']} ({s['reason']})" for s in unassigned[:5])
+
+ # GET _cluster/allocation/explain on one representative shard - the reason
+ # code from _cat/shards (e.g. CLUSTER_RECOVERED) is terse; this gives the
+ # actual human-readable explanation of why the indexer won't allocate it.
+ first = unassigned[0]
+ explain = explain_allocation(first["index"], first["shard"], primary=(first.get("prirep") == "p"))
+ allocation_explanation = explain.get("allocate_explanation") or explain.get("error") or "(no explanation returned)"
+
+ response["display"] += (
+ f"\n[WARNING] {len(unassigned)} unassigned shard(s) found, e.g.:\n{sample}\n\n"
+ f"Allocation explanation for {first['index']} shard {first['shard']}:\n {allocation_explanation}\n\n"
+ f"With {node_count} node(s), recommended number_of_replicas is {recommended}."
+ )
+ response["ask"] = ["Auto", "Manual"]
+ context["stage"] = "fix_replicas"
+ return response
+
+
+def _offer_reindex(response, context):
+ indices = context.get("unassigned_indices", [])
+ listing = "\n".join(f" - {i}" for i in indices) or " (none)"
+ response["display"] += (
+ "\n\nSetting the replica count only fixes shard placement going forward - the "
+ "indices that were already stuck unassigned may still need to be reindexed to "
+ "fully recover. Affected indices:\n"
+ f"{listing}\n\n"
+ "Reindexing keeps the data - back it up, delete the original, restore from the "
+ "backup, then delete the backup, one index at a time. Deleting the affected "
+ "indices outright is faster but permanently discards their data - only pick "
+ "that if you don't need it.\n\n"
+ "How would you like to do this?"
+ )
+ response["ask"] = ["Auto (reindex, keeps data)", "Manual", "Delete instead", "Skip"]
+ context["stage"] = "reindex_method"
+ return response
+
+
+def _confirm_delete_unassigned(response, context):
+ indices = context.get("unassigned_indices", [])
+ listing = "\n".join(f" - {i}" for i in indices) or " (none)"
+ response["display"] = (
+ "WARNING - this permanently deletes each of the following indices in full. "
+ "There is no backup step - any data in them will be gone for good:\n\n"
+ f"{listing}\n\n"
+ "Are you sure you want to delete these indices?"
+ )
+ response["ask"] = ["Yes, delete via Auto", "Yes, show me the command", "No, go back"]
+ context["stage"] = "confirm_delete_unassigned"
+ return response
+
+
+def _delete_unassigned_indices(response, context):
+ indices = context.get("unassigned_indices", [])
+ results = [(name, indexer_api_delete(f"/{name}")) for name in indices]
+ lines = "\n".join(f" - {name}: {raw}" for name, raw in results[:10])
+ more = f"\n ... and {len(results) - 10} more" if len(results) > 10 else ""
+ response["display"] = f"Deleted {len(results)} index(es):\n{lines}{more}"
+ return _verify_and_start_step6(response, context)
+
+
+def _manual_reindex_instructions(indices):
+ listing = "\n".join(f" - {i}" for i in indices) or " (none)"
+ return (
+ "Reindex the affected indices one at a time (not all at once). Take a backup of "
+ "the index, then run the following, replacing with the index "
+ "name you want to reindex:\n\n"
+ f"Affected indices:\n{listing}\n\n"
+ "1. Back it up:\n\n"
+ " POST _reindex\n"
+ " {\n"
+ " \"source\": { \"index\": \"\" },\n"
+ " \"dest\": { \"index\": \"-backup\" }\n"
+ " }\n\n"
+ "2. Delete the original index:\n\n"
+ " DELETE /\n\n"
+ "3. Reindex from the backup:\n\n"
+ " POST _reindex\n"
+ " {\n"
+ " \"source\": { \"index\": \"-backup\" },\n"
+ " \"dest\": { \"index\": \"\" }\n"
+ " }\n\n"
+ "4. Delete the backup index:\n\n"
+ " DELETE /-backup\n\n"
+ "Repeat for any other indices showing field conflicts or the same issue. See the "
+ "Wazuh reindexing documentation for more details."
+ )
+
+
+def _auto_reindex(response, context):
+ indices = context.get("unassigned_indices", [])
+ results = []
+ for index_name in indices:
+ steps = reindex_for_mapping_conflict(index_name)
+ results.append((index_name, steps))
+
+ ok_count = sum(1 for _, steps in results if not steps.get("aborted_after"))
+ lines = []
+ for name, steps in results[:10]:
+ aborted_after = steps.get("aborted_after")
+ if not aborted_after:
+ lines.append(f" - {name}: OK")
+ else:
+ detail = (steps.get(aborted_after) or "(no response)")[:200]
+ lines.append(f" - {name}: stopped after '{aborted_after}' - nothing irreversible happened past that point. {detail}")
+ more = f"\n ... and {len(results) - 10} more" if len(results) > 10 else ""
+ response["display"] += (
+ f"\n\nReindexed {ok_count}/{len(results)} index(es) successfully, one at a time:\n"
+ + "\n".join(lines) + more
+ )
+ return _verify_and_start_step6(response, context)
+
+
+def _verify_and_start_step6(response, context):
+ sep = "\n\n" if response["display"] else ""
+ unassigned = get_unassigned_shards()
+ if unassigned:
+ response["display"] += f"{sep}[WARNING] {len(unassigned)} shard(s) are still unassigned after reindexing."
+ else:
+ response["display"] += f"{sep}[OK] No unassigned shards remain."
+ return _start_step6(response, context)
+
+
+def _start_step6(response, context):
+ response["display"] += (
+ "\n\nStep 6 - Check Today's wazuh-alerts-* Index:\n\n"
+ "This is the last link in the chain - even with a healthy manager, Filebeat, "
+ "indexer, and cluster, alerts still won't show up if today's wazuh-alerts-* "
+ "index was never created or has stopped receiving new documents.\n\n"
+ "How would you like to check this?\n\n"
+ " Auto - We perform all checks automatically.\n"
+ " Manual - We provide the commands, and you run them and share the output."
+ )
+ response["ask"] = ["Auto", "Manual"]
+ context["stage"] = "step6_method"
+ return response
+
+
+def _check_indices(response, context):
+ response["display"] += ("\n" if response["display"] else "") + "Checking today's wazuh-alerts-* index..."
+ if not index_has_todays_date():
+ response["display"] += "\n[WARNING] No wazuh-alerts-* index for today was found - the pipeline may have stalled recently."
+ return conclude(False, response["display"], context, topic="wazuh no alerts index for today pipeline stalled")
+
+ response["display"] += (
+ "\n[OK] Today's wazuh-alerts-* index exists, and everything earlier in the pipeline "
+ "checked out.\n\n"
+ "If you're still not seeing alerts you expect: are you missing one particular alert "
+ "(or alert type), or are ALL of today's alerts missing from the dashboard?"
+ )
+ response["ask"] = ["One particular alert", "All of today's alerts", "Nothing missing - all good"]
+ context["stage"] = "step6_scope_check"
+ return response
+
+
+def _manual_reindex_single_index_instructions(index_name):
+ return (
+ f"Take a backup of {index_name}, then reindex it to clear the mapping conflict:\n\n"
+ "1. Back it up:\n\n"
+ " POST _reindex\n"
+ " {\n"
+ f" \"source\": {{ \"index\": \"{index_name}\" }},\n"
+ f" \"dest\": {{ \"index\": \"{index_name}-backup\" }}\n"
+ " }\n\n"
+ "2. Delete the original index:\n\n"
+ f" DELETE /{index_name}\n\n"
+ "3. Reindex from the backup:\n\n"
+ " POST _reindex\n"
+ " {\n"
+ f" \"source\": {{ \"index\": \"{index_name}-backup\" }},\n"
+ f" \"dest\": {{ \"index\": \"{index_name}\" }}\n"
+ " }\n\n"
+ "4. Delete the backup index:\n\n"
+ f" DELETE /{index_name}-backup\n\n"
+ "Once you've run it, let us know."
+ )
diff --git a/integrations/wazuh-troubleshooting-tool/backend/utils/agent_utils.py b/integrations/wazuh-troubleshooting-tool/backend/utils/agent_utils.py
new file mode 100644
index 00000000..21d485e0
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/utils/agent_utils.py
@@ -0,0 +1,73 @@
+import re
+from executor import run_command
+
+AGENT_CONTROL = "/var/ossec/bin/agent_control"
+
+
+def list_agents():
+ """Raw `agent_control -l` output - one line per registered agent."""
+ return run_command(f"{AGENT_CONTROL} -l") or ""
+
+
+def find_agent_line(identifier):
+ """Return the specific output line for one agent (matched by name or ID), or None if not found."""
+ if not identifier:
+ return None
+ for line in list_agents().splitlines():
+ if identifier.lower() in line.lower():
+ return line
+ return None
+
+
+def is_agent_active(identifier):
+ """
+ True/False if the agent was found and we can read its state.
+ None means the identifier wasn't found in the agent list at all.
+ """
+ line = find_agent_line(identifier)
+ if line is None:
+ return None
+ return "active" in line.lower()
+
+
+def list_active_agents():
+ """
+ Parse `agent_control -l` down to just the active agents, as
+ [{"id": "001", "name": "some-agent", "raw": ""}].
+
+ Agent 000 is always excluded - it's the manager's own local agent, not
+ a real endpoint, so it's never a valid candidate for a restart test.
+ """
+ active = []
+ for line in list_agents().splitlines():
+ if "active" not in line.lower():
+ continue
+ id_match = re.search(r"ID:\s*(\S+)", line)
+ # Stop at whichever comes first: a comma, the next "IP:" field, or a
+ # run of 2+ spaces (agent_control's output isn't always comma-separated).
+ name_match = re.search(r"Name:\s*(.+?)(?:,|\s+IP:|\s{2,}|$)", line)
+ if not id_match:
+ continue
+ agent_id = id_match.group(1).strip(",")
+ if agent_id == "000":
+ continue
+ active.append({
+ "id": agent_id,
+ "name": name_match.group(1).strip() if name_match else "unknown",
+ "raw": line.strip(),
+ })
+ return active
+
+
+def restart_agent(agent_id):
+ """
+ Remotely restart one agent by ID: agent_control -R -u
+ Only works if the agent is currently Active - it will not bring a
+ disconnected agent back online.
+ """
+ return run_command(f"{AGENT_CONTROL} -R -u {agent_id}") or ""
+
+
+def restart_all_agents():
+ """Remotely restart every currently active agent: agent_control -R -a"""
+ return run_command(f"{AGENT_CONTROL} -R -a") or ""
diff --git a/integrations/wazuh-troubleshooting-tool/backend/utils/ai_utils.py b/integrations/wazuh-troubleshooting-tool/backend/utils/ai_utils.py
new file mode 100644
index 00000000..b8d856cc
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/utils/ai_utils.py
@@ -0,0 +1,20 @@
+from copilot_engine import run_copilot
+from config import OLLAMA_URL, OLLAMA_MODEL
+
+
+def ai_explain(system_prompt, user_content):
+ """Send `user_content` (e.g. raw log text) to the local model under a
+ focused `system_prompt` and return its plain-text reply. Never raises -
+ returns a readable message instead if Ollama is unreachable."""
+ try:
+ return run_copilot(
+ messages=[{"role": "user", "content": user_content}],
+ ollama_url=OLLAMA_URL,
+ ollama_model=OLLAMA_MODEL,
+ include_env=False,
+ wazuh_api_url="", api_username="", api_password="",
+ indexer_url="", indexer_username="", indexer_password="",
+ system_prompt=system_prompt,
+ )
+ except Exception as e:
+ return f"(AI explanation unavailable: {e})"
diff --git a/integrations/wazuh-troubleshooting-tool/backend/utils/alerts_log_utils.py b/integrations/wazuh-troubleshooting-tool/backend/utils/alerts_log_utils.py
new file mode 100644
index 00000000..82c112ec
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/utils/alerts_log_utils.py
@@ -0,0 +1,57 @@
+import time
+from executor import run_command
+
+ALERTS_JSON_PATH = "/var/ossec/logs/alerts/alerts.json"
+ALERTS_LOG_PATH = "/var/ossec/logs/alerts/alerts.log"
+
+
+def alerts_json_exists():
+ return (run_command(f"test -f {ALERTS_JSON_PATH} && echo yes || echo no") or "").strip() == "yes"
+
+
+def alerts_json_age_seconds():
+ """Seconds since alerts.json was last modified, or None if it doesn't exist."""
+ if not alerts_json_exists():
+ return None
+ mtime_raw = (run_command(f"stat -c %Y {ALERTS_JSON_PATH}") or "").strip()
+ try:
+ return int(time.time()) - int(mtime_raw)
+ except ValueError:
+ return None
+
+
+def is_alerts_json_fresh(max_age_seconds=300):
+ age = alerts_json_age_seconds()
+ return age is not None and age <= max_age_seconds
+
+
+def tail_alerts_json(lines=5):
+ return run_command(f"tail -n {lines} {ALERTS_JSON_PATH}") or ""
+
+
+def tail_alerts_log(lines=100):
+ return run_command(f"tail -n {lines} {ALERTS_LOG_PATH}") or ""
+
+
+def alerts_log_mentions(identifier, lines=200):
+ """
+ Check the last N lines of alerts.log for a mention of this agent's
+ ID/name - used right after restarting an agent as a live test: if the
+ resulting check-in event shows up here, the manager is receiving and
+ logging that agent's events.
+ """
+ if not identifier:
+ return False
+ return identifier.lower() in tail_alerts_log(lines).lower()
+
+
+def alerts_json_mentions(identifier, lines=200):
+ """
+ Same idea as alerts_log_mentions(), but checks alerts.json instead -
+ this is the file the agent-restart pipeline test actually checks
+ against (a matching rule 503 'Wazuh agent started' entry confirms the
+ manager received and logged that agent's event).
+ """
+ if not identifier:
+ return False
+ return identifier.lower() in tail_alerts_json(lines).lower()
diff --git a/integrations/wazuh-troubleshooting-tool/backend/utils/api_utils.py b/integrations/wazuh-troubleshooting-tool/backend/utils/api_utils.py
new file mode 100644
index 00000000..0cd02ffc
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/utils/api_utils.py
@@ -0,0 +1,82 @@
+"""
+Generic helpers for calling the Wazuh indexer's REST API with auth already
+applied, so use cases don't each re-implement request/auth/error handling.
+
+Reusable by any use case that needs to hit the indexer API (cluster health,
+cat indices, cat shards, allocation explain, settings changes, reindex,
+deletes, etc.).
+"""
+
+import json
+
+import requests
+import urllib3
+
+from config import INDEXER_USERNAME, INDEXER_PASSWORD, INDEXER_URL
+
+urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
+
+_AUTH = (INDEXER_USERNAME, INDEXER_PASSWORD)
+
+
+def _request(method, endpoint, json_body=None):
+ """
+ `endpoint` should start with "/", e.g. "/_cluster/health".
+ Returns the raw response text (empty string on failure).
+ """
+ try:
+ resp = requests.request(
+ method,
+ f"{INDEXER_URL}{endpoint}",
+ auth=_AUTH,
+ json=json_body,
+ verify=False,
+ timeout=15,
+ )
+ return resp.text
+ except requests.RequestException:
+ return ""
+
+
+def indexer_api_get(endpoint):
+ """GET a path from the indexer's REST API (e.g. "/_cluster/health")."""
+ return _request("GET", endpoint)
+
+
+def indexer_api_get_json(endpoint):
+ """
+ Same as indexer_api_get, but parses the response as JSON.
+
+ Returns (parsed_json, raw_text). If parsing fails, parsed_json is None
+ and raw_text is preserved so the caller can still show it to the user
+ for diagnosis (e.g. "the indexer isn't reachable, here's the raw error").
+ """
+ raw = indexer_api_get(endpoint)
+ try:
+ return json.loads(raw), raw
+ except (ValueError, TypeError):
+ return None, raw
+
+
+def indexer_api_put(endpoint, json_body=None):
+ """PUT to the indexer's REST API. `json_body`, if given, is sent as JSON."""
+ return _request("PUT", endpoint, json_body)
+
+
+def indexer_api_post(endpoint, json_body=None):
+ """Same as indexer_api_put but for POST (used for _reindex, _search, allocation/explain)."""
+ return _request("POST", endpoint, json_body)
+
+
+def indexer_api_post_json(endpoint, json_body=None):
+ """Same as indexer_api_post, but parses the response as JSON."""
+ raw = indexer_api_post(endpoint, json_body)
+ try:
+ return json.loads(raw), raw
+ except (ValueError, TypeError):
+ return None, raw
+
+
+def indexer_api_delete(endpoint):
+ """DELETE a path from the indexer's REST API (e.g. an index name)."""
+ return _request("DELETE", endpoint)
diff --git a/integrations/wazuh-troubleshooting-tool/backend/utils/archive_utils.py b/integrations/wazuh-troubleshooting-tool/backend/utils/archive_utils.py
new file mode 100644
index 00000000..e4177a0e
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/utils/archive_utils.py
@@ -0,0 +1,30 @@
+"""
+Helpers for reading a file out of a tar archive that might live on a slow
+filesystem (e.g. a Vagrant/VirtualBox shared folder like /home/vagrant).
+
+Extracting a single member directly from an archive on a slow shared
+filesystem can take a very long time, because tar has to make many small
+read/seek calls to walk the archive, and each one can carry real latency
+on that kind of filesystem. Copying the whole archive to local disk once
+with a single sequential read is much faster, then extracting from that
+local copy is fast because it's on real disk.
+
+Reusable by any future use case that needs to read something out of an
+installer archive, a backup tarball, etc.
+"""
+
+import os
+from executor import run_command
+
+
+def extract_from_archive(archive_path, member_path, local_cache_dir="/tmp"):
+ """
+ Extract a single member from a tar archive and return its contents as
+ text. The archive is copied to `local_cache_dir` once (skipped if a
+ copy is already there) before extracting.
+ """
+ local_copy = os.path.join(local_cache_dir, os.path.basename(archive_path))
+
+ run_command(f"[ -f {local_copy} ] || cp {archive_path} {local_copy}")
+
+ return run_command(f"tar -axf {local_copy} {member_path} -O") or ""
diff --git a/integrations/wazuh-troubleshooting-tool/backend/utils/cache_utils.py b/integrations/wazuh-troubleshooting-tool/backend/utils/cache_utils.py
new file mode 100644
index 00000000..04e86bba
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/utils/cache_utils.py
@@ -0,0 +1,37 @@
+"""
+Tiny generic in-memory TTL cache.
+
+Reusable by any utility function that reads something slow or expensive but
+relatively stable for the duration of a troubleshooting session (e.g.
+reading a value out of a tar archive that lives on a slow filesystem, or
+querying an API endpoint that doesn't need to be hit on every single check).
+
+Not persistent, not distributed - just enough to stop re-doing the same
+slow work repeatedly within one running process.
+"""
+
+import time
+
+_cache = {}
+
+
+def cached(key, compute_fn, ttl=300):
+ """
+ Return the cached value for `key` if it's younger than `ttl` seconds;
+ otherwise call compute_fn(), cache the result, and return it.
+ """
+ now = time.time()
+ entry = _cache.get(key)
+ if entry and (now - entry[0]) < ttl:
+ return entry[1]
+ value = compute_fn()
+ _cache[key] = (now, value)
+ return value
+
+
+def clear_cache(key=None):
+ """Clear one cached key, or the whole cache if key is None."""
+ if key is None:
+ _cache.clear()
+ else:
+ _cache.pop(key, None)
diff --git a/integrations/wazuh-troubleshooting-tool/backend/utils/cert_utils.py b/integrations/wazuh-troubleshooting-tool/backend/utils/cert_utils.py
new file mode 100644
index 00000000..f5ee59a0
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/utils/cert_utils.py
@@ -0,0 +1,158 @@
+"""
+Wazuh certificate regeneration/redeploy.
+
+Reusable by ANY troubleshooting flow that diagnoses a TLS/certificate error
+(Filebeat output test, dashboard-not-ready, etc) instead of each flow
+re-writing the same wazuh-certs-tool.sh + redeploy sequence by hand.
+
+Node names (indexer/server/dashboard) are read from the original install
+config (wazuh-install-files.tar/config.yml) the same way FixEngine.get_control_ip()
+reads the indexer IP from it, so Auto mode doesn't need the user to supply
+them - Manual mode still shows the underlying commands with the real names
+filled in wherever we could resolve them.
+"""
+
+from executor import run_command
+from utils.archive_utils import extract_from_archive
+from utils.cache_utils import cached
+from utils.service_utils import restart_service_and_wait
+
+INSTALL_ARCHIVE = "/home/vagrant/wazuh-install-files.tar"
+CONFIG_MEMBER = "wazuh-install-files/config.yml"
+CERTS_TOOL_URL = "https://packages.wazuh.com/4.14/wazuh-certs-tool.sh"
+WORKDIR = "/tmp/wazuh-certs-renew"
+CERT_ARCHIVE = f"{WORKDIR}/wazuh-certificates.tar"
+
+
+def _get_config_yml():
+ return cached("install_config_yml_raw", lambda: extract_from_archive(INSTALL_ARCHIVE, CONFIG_MEMBER))
+
+
+def get_node_name(section):
+ """section: 'indexer' | 'server' | 'dashboard'. Returns that node's `name:` from config.yml."""
+ in_section = False
+ for line in _get_config_yml().splitlines():
+ stripped = line.strip()
+ if stripped == f"{section}:":
+ in_section = True
+ continue
+ if in_section and stripped.startswith("name:"):
+ return stripped.split(":", 1)[1].strip()
+ if in_section and stripped.endswith(":") and not stripped.startswith("-"):
+ in_section = False
+ return ""
+
+
+def get_node_names():
+ return {
+ "indexer": get_node_name("indexer"),
+ "server": get_node_name("server"),
+ "dashboard": get_node_name("dashboard"),
+ }
+
+
+def regenerate_and_redeploy_certs():
+ """
+ Regenerate certs with wazuh-certs-tool.sh and redeploy them to the
+ Wazuh Indexer, Filebeat, and Dashboard on this host, then restart all
+ three services. Returns a status dict.
+ """
+ names = get_node_names()
+ n1, n2, n3 = names["indexer"], names["server"], names["dashboard"]
+
+ log = []
+ run_command(f"mkdir -p {WORKDIR}")
+ log.append(run_command(f"cd {WORKDIR} && curl -sO {CERTS_TOOL_URL}") or "")
+ log.append(run_command(f"cd {WORKDIR} && bash wazuh-certs-tool.sh -A") or "")
+
+ # Wazuh Indexer
+ log.append(run_command("rm -rf /etc/wazuh-indexer/certs && mkdir /etc/wazuh-indexer/certs") or "")
+ log.append(run_command(
+ f"tar -xf {CERT_ARCHIVE} -C /etc/wazuh-indexer/certs/ "
+ f"./{n1}.pem ./{n1}-key.pem ./admin.pem ./admin-key.pem ./root-ca.pem"
+ ) or "")
+ run_command(f"mv -n /etc/wazuh-indexer/certs/{n1}.pem /etc/wazuh-indexer/certs/wazuh-indexer.pem")
+ run_command(f"mv -n /etc/wazuh-indexer/certs/{n1}-key.pem /etc/wazuh-indexer/certs/wazuh-indexer-key.pem")
+ run_command("chmod 500 /etc/wazuh-indexer/certs")
+ run_command("chmod 400 /etc/wazuh-indexer/certs/*")
+ run_command("chown -R wazuh-indexer:wazuh-indexer /etc/wazuh-indexer/certs")
+
+ # Filebeat
+ log.append(run_command("rm -rf /etc/filebeat/certs && mkdir /etc/filebeat/certs") or "")
+ log.append(run_command(
+ f"tar -xf {CERT_ARCHIVE} -C /etc/filebeat/certs/ ./{n2}.pem ./{n2}-key.pem ./root-ca.pem"
+ ) or "")
+ run_command(f"mv -n /etc/filebeat/certs/{n2}.pem /etc/filebeat/certs/wazuh-server.pem")
+ run_command(f"mv -n /etc/filebeat/certs/{n2}-key.pem /etc/filebeat/certs/wazuh-server-key.pem")
+ run_command("chmod 500 /etc/filebeat/certs")
+ run_command("chmod 400 /etc/filebeat/certs/*")
+ run_command("chown -R root:root /etc/filebeat/certs")
+
+ # Wazuh Dashboard
+ log.append(run_command("rm -rf /etc/wazuh-dashboard/certs && mkdir /etc/wazuh-dashboard/certs") or "")
+ log.append(run_command(
+ f"tar -xf {CERT_ARCHIVE} -C /etc/wazuh-dashboard/certs/ ./{n3}.pem ./{n3}-key.pem ./root-ca.pem"
+ ) or "")
+ run_command(f"mv -n /etc/wazuh-dashboard/certs/{n3}.pem /etc/wazuh-dashboard/certs/wazuh-dashboard.pem")
+ run_command(f"mv -n /etc/wazuh-dashboard/certs/{n3}-key.pem /etc/wazuh-dashboard/certs/wazuh-dashboard-key.pem")
+ run_command("chmod 500 /etc/wazuh-dashboard/certs")
+ run_command("chmod 400 /etc/wazuh-dashboard/certs/*")
+ run_command("chown -R wazuh-dashboard:wazuh-dashboard /etc/wazuh-dashboard/certs")
+
+ indexer_status = restart_service_and_wait("wazuh-indexer")
+ filebeat_status = restart_service_and_wait("filebeat")
+ dashboard_status = restart_service_and_wait("wazuh-dashboard")
+
+ return {
+ "ok": indexer_status == "active" and filebeat_status == "active" and dashboard_status == "active",
+ "indexer_status": indexer_status,
+ "filebeat_status": filebeat_status,
+ "dashboard_status": dashboard_status,
+ "log": "\n".join(l for l in log if l),
+ }
+
+
+def manual_cert_redeploy_instructions():
+ names = get_node_names()
+ n1 = names["indexer"] or ""
+ n2 = names["server"] or ""
+ n3 = names["dashboard"] or ""
+
+ return (
+ "Locate the config.yml file and run:\n\n"
+ f" curl -sO {CERTS_TOOL_URL}\n"
+ " bash wazuh-certs-tool.sh -A\n\n"
+ "Set the node names (from config.yml):\n\n"
+ f" export NODE_NAME1={n1}\n"
+ f" export NODE_NAME2={n2}\n"
+ f" export NODE_NAME3={n3}\n\n"
+ "Redeploy the certificates to the Wazuh Indexer:\n\n"
+ " rm -rf /etc/wazuh-indexer/certs\n"
+ " mkdir /etc/wazuh-indexer/certs\n"
+ " tar -xf ./wazuh-certificates.tar -C /etc/wazuh-indexer/certs/ ./$NODE_NAME1.pem ./$NODE_NAME1-key.pem ./admin.pem ./admin-key.pem ./root-ca.pem\n"
+ " mv -n /etc/wazuh-indexer/certs/$NODE_NAME1.pem /etc/wazuh-indexer/certs/wazuh-indexer.pem\n"
+ " mv -n /etc/wazuh-indexer/certs/$NODE_NAME1-key.pem /etc/wazuh-indexer/certs/wazuh-indexer-key.pem\n"
+ " chmod 500 /etc/wazuh-indexer/certs\n"
+ " chmod 400 /etc/wazuh-indexer/certs/*\n"
+ " chown -R wazuh-indexer:wazuh-indexer /etc/wazuh-indexer/certs\n\n"
+ "Redeploy the certificates to Filebeat:\n\n"
+ " rm -rf /etc/filebeat/certs\n"
+ " mkdir /etc/filebeat/certs\n"
+ " tar -xf ./wazuh-certificates.tar -C /etc/filebeat/certs/ ./$NODE_NAME2.pem ./$NODE_NAME2-key.pem ./root-ca.pem\n"
+ " mv -n /etc/filebeat/certs/$NODE_NAME2.pem /etc/filebeat/certs/wazuh-server.pem\n"
+ " mv -n /etc/filebeat/certs/$NODE_NAME2-key.pem /etc/filebeat/certs/wazuh-server-key.pem\n"
+ " chmod 500 /etc/filebeat/certs\n"
+ " chmod 400 /etc/filebeat/certs/*\n"
+ " chown -R root:root /etc/filebeat/certs\n\n"
+ "Redeploy the certificates to the Wazuh Dashboard:\n\n"
+ " rm -rf /etc/wazuh-dashboard/certs\n"
+ " mkdir /etc/wazuh-dashboard/certs\n"
+ " tar -xf ./wazuh-certificates.tar -C /etc/wazuh-dashboard/certs/ ./$NODE_NAME3.pem ./$NODE_NAME3-key.pem ./root-ca.pem\n"
+ " mv -n /etc/wazuh-dashboard/certs/$NODE_NAME3.pem /etc/wazuh-dashboard/certs/wazuh-dashboard.pem\n"
+ " mv -n /etc/wazuh-dashboard/certs/$NODE_NAME3-key.pem /etc/wazuh-dashboard/certs/wazuh-dashboard-key.pem\n"
+ " chmod 500 /etc/wazuh-dashboard/certs\n"
+ " chmod 400 /etc/wazuh-dashboard/certs/*\n"
+ " chown -R wazuh-dashboard:wazuh-dashboard /etc/wazuh-dashboard/certs\n\n"
+ "Then restart all the components:\n\n"
+ " systemctl restart wazuh-indexer filebeat wazuh-dashboard"
+ )
diff --git a/integrations/wazuh-troubleshooting-tool/backend/utils/cluster_utils.py b/integrations/wazuh-troubleshooting-tool/backend/utils/cluster_utils.py
new file mode 100644
index 00000000..b84e152c
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/utils/cluster_utils.py
@@ -0,0 +1,68 @@
+from utils.api_utils import indexer_api_get_json, indexer_api_put
+
+# Known cluster-wide settings that silently block writes/index creation
+# cluster-wide, regardless of any single index's own health. This is the
+# exact setting that caused the reindex data-loss incident - a blocked
+# create_index looked completely invisible to a plain _cluster/health check.
+KNOWN_WRITE_BLOCKS = ("cluster.blocks.read_only", "cluster.blocks.read_only_allow_delete", "cluster.blocks.create_index")
+
+
+def get_cluster_health():
+ """Returns (parsed_json_or_None, raw_text)."""
+ return indexer_api_get_json("/_cluster/health")
+
+
+def get_cluster_status():
+ health, _ = get_cluster_health()
+ return health.get("status") if health else None
+
+
+def is_cluster_green():
+ return get_cluster_status() == "green"
+
+
+def get_cluster_settings():
+ """Returns (parsed_json_or_None, raw_text) for persistent + transient cluster settings."""
+ return indexer_api_get_json("/_cluster/settings")
+
+
+def get_write_blocks():
+ """
+ Check for the known cluster-wide blocks that silently prevent writes or
+ new index creation (e.g. a stale cluster.blocks.create_index from a
+ prior incident). Returns {"blocks": {name: value, ...}} for whichever
+ are actually set to something truthy, or {"error": raw} if the
+ settings endpoint itself couldn't be reached.
+ """
+ settings, raw = get_cluster_settings()
+ if settings is None:
+ return {"error": raw}
+
+ found = {}
+ for scope in ("persistent", "transient"):
+ scoped = settings.get(scope, {})
+ for name in KNOWN_WRITE_BLOCKS:
+ value = _dotted_get(scoped, name)
+ if value is not None and str(value).lower() not in ("false", "none", ""):
+ found[name] = value
+ return {"blocks": found}
+
+
+def clear_write_blocks(block_names):
+ """Clear the given cluster.blocks.* settings (in both scopes, since either could hold it)."""
+ reset = {name: None for name in block_names}
+ body = {"persistent": reset, "transient": reset}
+ return indexer_api_put("/_cluster/settings", body)
+
+
+def _dotted_get(d, dotted_key):
+ """d may have the key as one flat dotted string OR nested - OpenSearch's
+ settings API can return either shape depending on the OpenSearch version."""
+ if dotted_key in d:
+ return d[dotted_key]
+ node = d
+ for part in dotted_key.split("."):
+ if not isinstance(node, dict) or part not in node:
+ return None
+ node = node[part]
+ return node
diff --git a/integrations/wazuh-troubleshooting-tool/backend/utils/compressed_history.py b/integrations/wazuh-troubleshooting-tool/backend/utils/compressed_history.py
new file mode 100644
index 00000000..8ad492ea
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/utils/compressed_history.py
@@ -0,0 +1,112 @@
+"""
+Generic gzip-compressed rolling history store. Backs both the Wazuh Copilot
+chat history (session_store.py) and the Troubleshooting Library's completed-run
+downloads (wizard_history.py) — same mechanism, different directory.
+
+Keeps only the N most recently *created* entries: the moment a new one is
+saved past the cap, the oldest-created entry is deleted (file + manifest).
+"""
+import gzip
+import json
+import os
+from datetime import datetime, timezone
+
+
+class CompressedHistoryStore:
+ def __init__(self, directory, max_items=6):
+ self.dir = directory
+ self.manifest_path = os.path.join(directory, "manifest.json")
+ self.max_items = max_items
+
+ def _ensure_dir(self):
+ os.makedirs(self.dir, exist_ok=True)
+
+ def _file_path(self, item_id):
+ return os.path.join(self.dir, f"{item_id}.json.gz")
+
+ def _load_manifest(self):
+ self._ensure_dir()
+ if not os.path.exists(self.manifest_path):
+ return {}
+ try:
+ with open(self.manifest_path, "r") as f:
+ return json.load(f)
+ except (json.JSONDecodeError, OSError):
+ return {}
+
+ def _save_manifest(self, manifest):
+ self._ensure_dir()
+ with open(self.manifest_path, "w") as f:
+ json.dump(manifest, f, indent=2)
+
+ def save(self, item_id, data, title=None, extra_meta=None):
+ """Overwrite the compressed payload for item_id and update the
+ manifest. If item_id is brand new and pushes the total past
+ max_items, the oldest-created entry is deleted."""
+ self._ensure_dir()
+ manifest = self._load_manifest()
+ is_new = item_id not in manifest
+
+ payload = json.dumps(data).encode("utf-8")
+ with gzip.open(self._file_path(item_id), "wb") as f:
+ f.write(payload)
+
+ now = datetime.now(timezone.utc).isoformat()
+ if is_new:
+ manifest[item_id] = {
+ "title": title or "Untitled",
+ "started_at": now,
+ "updated_at": now,
+ **(extra_meta or {}),
+ }
+ else:
+ manifest[item_id]["updated_at"] = now
+ if title:
+ manifest[item_id]["title"] = title
+ if extra_meta:
+ manifest[item_id].update(extra_meta)
+
+ if is_new and len(manifest) > self.max_items:
+ oldest_id = min(manifest, key=lambda k: manifest[k]["started_at"])
+ if oldest_id != item_id:
+ self.delete(oldest_id, manifest=manifest, persist=False)
+
+ self._save_manifest(manifest)
+
+ def load(self, item_id):
+ path = self._file_path(item_id)
+ if not os.path.exists(path):
+ return None
+ try:
+ with gzip.open(path, "rb") as f:
+ return json.loads(f.read().decode("utf-8"))
+ except (OSError, json.JSONDecodeError):
+ return None
+
+ def list(self):
+ """Manifest entries, newest-updated first."""
+ manifest = self._load_manifest()
+ return sorted(
+ [{"id": iid, **meta} for iid, meta in manifest.items()],
+ key=lambda e: e["updated_at"],
+ reverse=True,
+ )
+
+ def delete(self, item_id, manifest=None, persist=True):
+ own_manifest = manifest is None
+ if own_manifest:
+ manifest = self._load_manifest()
+ manifest.pop(item_id, None)
+ path = self._file_path(item_id)
+ if os.path.exists(path):
+ os.remove(path)
+ if own_manifest and persist:
+ self._save_manifest(manifest)
+
+ def rename(self, item_id, new_title):
+ manifest = self._load_manifest()
+ if item_id not in manifest:
+ return False
+ manifest[item_id]["title"] = new_title[:100]
+ self._save_manifest(manifest)
+ return True
diff --git a/integrations/wazuh-troubleshooting-tool/backend/utils/default_route_utils.py b/integrations/wazuh-troubleshooting-tool/backend/utils/default_route_utils.py
new file mode 100644
index 00000000..5937c89f
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/utils/default_route_utils.py
@@ -0,0 +1,43 @@
+from executor import run_command
+
+DASHBOARD_CONFIG_PATH = "/etc/wazuh-dashboard/opensearch_dashboards.yml"
+EXPECTED_DEFAULT_ROUTE = "/app/wz-home"
+
+
+def get_default_route():
+ """Raw configured line for uiSettings.overrides.defaultRoute, or '' if not set."""
+ return run_command(
+ f"grep 'uiSettings.overrides.defaultRoute' {DASHBOARD_CONFIG_PATH}"
+ ) or ""
+
+
+def is_default_route_ok(raw=None):
+ """
+ After an upgrade, opensearch_dashboards.yml can be left over from the
+ previous version and miss (or keep a stale) defaultRoute override. When
+ that happens the dashboard serves an "Application Not Found" error
+ instead of the home page.
+ """
+ raw = get_default_route() if raw is None else raw
+ return EXPECTED_DEFAULT_ROUTE in raw
+
+
+def set_default_route():
+ """Set uiSettings.overrides.defaultRoute in opensearch_dashboards.yml. Caller restarts wazuh-dashboard afterward."""
+ setting = f"uiSettings.overrides.defaultRoute: {EXPECTED_DEFAULT_ROUTE}"
+
+ has_key = (run_command(
+ f"grep -q 'uiSettings.overrides.defaultRoute' {DASHBOARD_CONFIG_PATH} "
+ "&& echo yes || echo no"
+ ) or "no").strip()
+
+ if has_key == "yes":
+ run_command(
+ "sed -i 's|uiSettings.overrides.defaultRoute:.*|{}|' {}".format(
+ setting, DASHBOARD_CONFIG_PATH
+ )
+ )
+ else:
+ run_command(f"echo '{setting}' >> {DASHBOARD_CONFIG_PATH}")
+
+ return get_default_route()
diff --git a/integrations/wazuh-troubleshooting-tool/backend/utils/embeddings.py b/integrations/wazuh-troubleshooting-tool/backend/utils/embeddings.py
new file mode 100644
index 00000000..96806169
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/utils/embeddings.py
@@ -0,0 +1,39 @@
+"""
+Wraps Ollama's embedding endpoint for the LGTM knowledge base's semantic
+search. nomic-embed-text is asymmetric - it expects a task prefix for best
+retrieval quality: "search_document: " when embedding a stored issue,
+"search_query: " when embedding a user's question.
+"""
+import requests
+
+from config import OLLAMA_URL
+
+EMBED_MODEL = "nomic-embed-text"
+# 15s was too tight on a loaded, low-core-count host - a correctly-working
+# embedding call can simply take longer than that under CPU contention from
+# other services (wazuh-indexer, editor tooling, etc.), not because Ollama
+# is actually broken. 60s gives real headroom without masking a truly dead
+# Ollama for an unreasonable amount of time.
+_TIMEOUT = 60
+
+
+def _embed(text: str):
+ try:
+ resp = requests.post(
+ f"{OLLAMA_URL}/api/embeddings",
+ json={"model": EMBED_MODEL, "prompt": text[:8000]},
+ timeout=_TIMEOUT,
+ )
+ if resp.status_code != 200:
+ return None
+ return resp.json().get("embedding")
+ except requests.exceptions.RequestException:
+ return None
+
+
+def embed_document(text: str):
+ return _embed(f"search_document: {text}")
+
+
+def embed_query(text: str):
+ return _embed(f"search_query: {text}")
diff --git a/integrations/wazuh-troubleshooting-tool/backend/utils/filebeat_utils.py b/integrations/wazuh-troubleshooting-tool/backend/utils/filebeat_utils.py
new file mode 100644
index 00000000..3d2aae9d
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/utils/filebeat_utils.py
@@ -0,0 +1,121 @@
+import re
+
+from executor import run_command
+from utils.service_utils import restart_service_and_wait
+
+FILEBEAT_LOG_PATH = "/var/log/filebeat/filebeat"
+
+# Wazuh only ships/supports this exact Filebeat build - see:
+# https://documentation.wazuh.com/current/upgrade-guide/index.html#wazuh-components-compatibility
+SUPPORTED_FILEBEAT_VERSION = "7.10.2"
+FILEBEAT_DEB_URL = "https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-oss-7.10.2-amd64.deb"
+FILEBEAT_RPM_URL = "https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-oss-7.10.2-x86_64.rpm"
+
+
+def run_filebeat_output_test():
+ """Run `filebeat test output` and judge OK/failed from the actual text, not just presence of the word OK."""
+ out = run_command("filebeat test output") or ""
+ upper = out.upper()
+ ok = "OK" in upper and "ERROR" not in upper
+ return {"raw": out, "ok": ok}
+
+
+def get_filebeat_log_errors(lines=200):
+ return run_command(f"tail -n {lines} {FILEBEAT_LOG_PATH} | grep -i -E 'error|warn'") or ""
+
+
+def get_filebeat_version(test_raw=""):
+ """Pull the version out of `filebeat test output`'s own report (e.g.
+ 'version: 7.10.2') if we already have it, else ask Filebeat directly."""
+ match = re.search(r"version:\s*([\d.]+)", test_raw or "")
+ if match:
+ return match.group(1)
+ out = run_command("filebeat version") or ""
+ match = re.search(r"(\d+\.\d+\.\d+)", out)
+ return match.group(1) if match else ""
+
+
+def classify_filebeat_failure(test_raw, log_errors=""):
+ """
+ Best-effort classification of why `filebeat test output` failed, based on
+ the actual text rather than a guess. Returns one of: "unsupported_version",
+ "tls_cert_error", "auth_failure", "indexer_unreachable", "unknown".
+ """
+ text = f"{test_raw}\n{log_errors}".lower()
+
+ if "invalid_index_name_exception" in text or "could not connect to a compatible version" in text:
+ return "unsupported_version"
+ if any(kw in text for kw in ["x509", "certificate", "tls", "ssl", "handshake"]):
+ return "tls_cert_error"
+ if any(kw in text for kw in ["unauthorized", "authentication", "401"]):
+ return "auth_failure"
+ if any(kw in text for kw in [
+ "connection refused", "no route to host", "i/o timeout",
+ "network is unreachable", "dial up... error", "talk to server... error",
+ ]):
+ return "indexer_unreachable"
+ return "unknown"
+
+
+def fix_unsupported_filebeat_version():
+ """
+ Wazuh only supports Filebeat-OSS 7.10.2. Deploys the Wazuh Filebeat
+ module + alerts template (the documented fix for the version-mismatch
+ error), and reinstalls Filebeat-OSS 7.10.2 itself if the version is
+ still wrong afterwards. Returns what ran and the final state.
+ """
+ log = []
+ log.append(run_command("systemctl stop filebeat") or "")
+ log.append(run_command(
+ "curl -s https://packages.wazuh.com/4.x/filebeat/wazuh-filebeat-0.5.tar.gz "
+ "| tar -xvz -C /usr/share/filebeat/module"
+ ) or "")
+ log.append(run_command(
+ "curl -so /etc/filebeat/wazuh-template.json "
+ "https://raw.githubusercontent.com/wazuh/wazuh/v4.14.6/extensions/elasticsearch/7.x/wazuh-template.json"
+ ) or "")
+ log.append(run_command("chmod go+r /etc/filebeat/wazuh-template.json") or "")
+
+ status = restart_service_and_wait("filebeat")
+ version = get_filebeat_version()
+
+ if version != SUPPORTED_FILEBEAT_VERSION:
+ if run_command("command -v dpkg") :
+ log.append(run_command(f"curl -so /tmp/filebeat-oss.deb {FILEBEAT_DEB_URL}") or "")
+ log.append(run_command("dpkg -i /tmp/filebeat-oss.deb") or "")
+ elif run_command("command -v rpm"):
+ log.append(run_command(f"curl -so /tmp/filebeat-oss.rpm {FILEBEAT_RPM_URL}") or "")
+ log.append(run_command("rpm -Uvh /tmp/filebeat-oss.rpm") or "")
+ status = restart_service_and_wait("filebeat")
+ version = get_filebeat_version()
+
+ return {
+ "ok": version == SUPPORTED_FILEBEAT_VERSION,
+ "version": version or "unknown",
+ "status": status,
+ "log": "\n".join(l for l in log if l),
+ }
+
+
+def manual_unsupported_version_instructions():
+ return (
+ f"Wazuh is only compatible with Filebeat-OSS {SUPPORTED_FILEBEAT_VERSION}. "
+ "Manually upgrading to a newer version is not recommended - it can break "
+ "alert forwarding and index integration.\n"
+ "https://documentation.wazuh.com/current/upgrade-guide/index.html#wazuh-components-compatibility\n\n"
+ "Stop Filebeat:\n\n"
+ " systemctl stop filebeat\n\n"
+ "Download the Wazuh Filebeat module:\n\n"
+ " curl -s https://packages.wazuh.com/4.x/filebeat/wazuh-filebeat-0.5.tar.gz | sudo tar -xvz -C /usr/share/filebeat/module\n\n"
+ "Download the alerts template:\n\n"
+ " curl -so /etc/filebeat/wazuh-template.json https://raw.githubusercontent.com/wazuh/wazuh/v4.14.6/extensions/elasticsearch/7.x/wazuh-template.json\n"
+ " chmod go+r /etc/filebeat/wazuh-template.json\n\n"
+ "Restart Filebeat, then check the version again:\n\n"
+ " filebeat version\n\n"
+ f"It should report Filebeat-OSS {SUPPORTED_FILEBEAT_VERSION}. If it still doesn't, reinstall Filebeat "
+ f"with the OSS {SUPPORTED_FILEBEAT_VERSION} package directly:\n\n"
+ f" curl -so /tmp/filebeat-oss.deb {FILEBEAT_DEB_URL} # Debian/Ubuntu\n"
+ " dpkg -i /tmp/filebeat-oss.deb\n\n"
+ f" curl -so /tmp/filebeat-oss.rpm {FILEBEAT_RPM_URL} # RHEL/CentOS\n"
+ " rpm -Uvh /tmp/filebeat-oss.rpm"
+ )
diff --git a/integrations/wazuh-troubleshooting-tool/backend/utils/fix_engine.py b/integrations/wazuh-troubleshooting-tool/backend/utils/fix_engine.py
new file mode 100644
index 00000000..5ce1630b
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/utils/fix_engine.py
@@ -0,0 +1,591 @@
+from executor import run_command, run_command_argv, replace_in_file
+from config import KIBANA_USERNAME, INDEXER_URL
+from utils.cache_utils import cached
+from utils.archive_utils import extract_from_archive
+from utils.service_utils import restart_service_and_wait, get_service_status
+
+import re
+import secrets
+import string
+
+import requests
+import urllib3
+
+urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
+
+
+class FixEngine:
+
+ # -----------------------------------------
+ # GET IP FROM config.yml (control file)
+ # -----------------------------------------
+ @staticmethod
+ def get_control_ip():
+
+ # Cached (utils/cache_utils.py): avoid re-reading this on every
+ # single troubleshooting step within the same session. The read
+ # itself uses utils/archive_utils.py, which copies the archive to
+ # local disk once before extracting — much faster than extracting
+ # directly from /home/vagrant if that's a slow shared folder.
+ output = cached(
+ "control_ip_raw",
+ lambda: extract_from_archive(
+ "/home/vagrant/wazuh-install-files.tar",
+ "wazuh-install-files/config.yml",
+ ),
+ )
+
+ in_indexer = False
+
+ for line in output.splitlines():
+ if "indexer:" in line:
+ in_indexer = True
+ continue
+ if in_indexer and line.strip().endswith(":") and "ip:" not in line:
+ in_indexer = False
+ if in_indexer and "ip:" in line:
+ return line.strip()
+
+ return ""
+
+ # -----------------------------------------
+ # GET IP FROM INDEXER CONFIG
+ # -----------------------------------------
+ @staticmethod
+ def get_indexer_ip():
+ return run_command(
+ "grep network.host /etc/wazuh-indexer/opensearch.yml"
+ ) or ""
+
+ # -----------------------------------------
+ # GET IP FROM DASHBOARD CONFIG
+ # -----------------------------------------
+ @staticmethod
+ def get_dashboard_ip():
+ return run_command(
+ "grep opensearch.hosts /etc/wazuh-dashboard/opensearch_dashboards.yml"
+ ) or ""
+
+ # -----------------------------------------
+ # EXTRACT IP (helper)
+ # -----------------------------------------
+ @staticmethod
+ def extract_ip(text):
+ if not text:
+ return None
+ match = re.search(r'(\d+\.\d+\.\d+\.\d+)', text)
+ return match.group(1) if match else None
+
+ # -----------------------------------------
+ # COMPARE IPS
+ # -----------------------------------------
+ @staticmethod
+ def compare_ips():
+ control = FixEngine.get_control_ip()
+ indexer = FixEngine.get_indexer_ip()
+ dashboard = FixEngine.get_dashboard_ip()
+
+ c_ip = FixEngine.extract_ip(control)
+ i_ip = FixEngine.extract_ip(indexer)
+ d_ip = FixEngine.extract_ip(dashboard)
+
+ return {
+ "control": c_ip,
+ "indexer": i_ip,
+ "dashboard": d_ip,
+ "match": (c_ip == i_ip == d_ip),
+ }
+
+ # -----------------------------------------
+ # FULL IP CHECK
+ # -----------------------------------------
+ @staticmethod
+ def check_ips():
+
+ data = FixEngine.compare_ips()
+
+ result = (
+ f"Control IP: {data['control']}\n"
+ f"Indexer IP: {data['indexer']}\n"
+ f"Dashboard IP: {data['dashboard']}"
+ )
+
+ if not data["match"]:
+ result += "\n\n[ERROR] IP mismatch detected."
+ else:
+ result += "\n\n[OK] IP configuration looks correct."
+
+ return result
+
+ # -----------------------------------------
+ # GET CERT PATHS FROM DASHBOARD CONFIG
+ # -----------------------------------------
+ @staticmethod
+ def get_cert_paths():
+ return run_command(
+ "grep -E 'certificate|key|ca' "
+ "/etc/wazuh-dashboard/opensearch_dashboards.yml"
+ ) or ""
+
+ # -----------------------------------------
+ # LIST CERT FILES
+ # -----------------------------------------
+ @staticmethod
+ def list_cert_files():
+ return run_command("ls -lrt /etc/wazuh-dashboard/certs") or ""
+
+ # -----------------------------------------
+ # CHECK CERT PERMISSIONS
+ # -----------------------------------------
+ @staticmethod
+ def check_cert_permissions():
+
+ perms = run_command(
+ "ls -ld /etc/wazuh-dashboard/certs"
+ ) or ""
+
+ files = run_command(
+ "ls -l /etc/wazuh-dashboard/certs"
+ ) or ""
+
+ return (
+ f"Directory permissions:\n{perms}\n\n"
+ f"Certificate files:\n{files}"
+ )
+
+ # -----------------------------------------
+ # CHECK CERT PATHS
+ # -----------------------------------------
+ @staticmethod
+ def check_cert_paths():
+
+ paths = FixEngine.get_cert_paths()
+ files = FixEngine.list_cert_files()
+
+ return (
+ f"Configured cert paths:\n{paths}\n\n"
+ f"Available cert files:\n{files}"
+ )
+
+ # -----------------------------------------
+ # FIX CERT PERMISSIONS
+ # -----------------------------------------
+ @staticmethod
+ def fix_cert_permissions():
+ cmds = [
+ "chmod 500 /etc/wazuh-dashboard/certs",
+ "chmod 400 /etc/wazuh-dashboard/certs/*",
+ "chown -R wazuh-dashboard:wazuh-dashboard /etc/wazuh-dashboard/certs",
+ ]
+ output = ""
+ for cmd in cmds:
+ output += (run_command(cmd) or "") + "\n"
+ return output
+
+ # -----------------------------------------
+ # RESTART INDEXER (Bug fixed: out was undefined)
+ # -----------------------------------------
+ @staticmethod
+ def restart_indexer():
+ status = restart_service_and_wait("wazuh-indexer")
+ return f"Status after restart: {status}"
+
+ # -----------------------------------------
+ # RESTART INDEXER AND WAIT FOR ACTIVE STATE
+ # Returns the final status string ("active", "failed", "activating", etc.)
+ #
+ # Delegates to utils/service_utils.py, which uses "--no-block" so the
+ # restart command returns immediately instead of blocking indefinitely
+ # while a slow-starting service (e.g. JVM-based wazuh-indexer) comes up,
+ # then polls "is-active" itself with a bounded, predictable window.
+ # -----------------------------------------
+ @staticmethod
+ def restart_indexer_and_wait(max_attempts=20, delay=3):
+ return restart_service_and_wait("wazuh-indexer", max_attempts=max_attempts, delay=delay)
+
+ # -----------------------------------------
+ # DASHBOARD STATUS
+ # -----------------------------------------
+ @staticmethod
+ def status_dashboard():
+ return get_service_status("wazuh-dashboard") or "unknown"
+
+ # -----------------------------------------
+ # INDEXER STATUS
+ # -----------------------------------------
+ @staticmethod
+ def status_indexer():
+ return run_command("systemctl is-active wazuh-indexer") or "unknown"
+
+ # -----------------------------------------
+ # CONNECTIVITY CHECK
+ # -----------------------------------------
+ @staticmethod
+ def check_connectivity(password):
+ try:
+ resp = requests.get(
+ f"{INDEXER_URL}/_cluster/health",
+ auth=(KIBANA_USERNAME, password),
+ verify=False,
+ timeout=10,
+ )
+ return resp.text
+ except requests.RequestException as e:
+ return str(e)
+
+ # -----------------------------------------
+ # GENERATE NEW PASSWORD
+ # -----------------------------------------
+ @staticmethod
+ def generate_password(length=16):
+ chars = string.ascii_letters + string.digits + ".*+?-"
+ return ''.join(secrets.choice(chars) for _ in range(length))
+
+ # -----------------------------------------
+ # APPLY NEW PASSWORD (INDEXER + DASHBOARD)
+ # -----------------------------------------
+ @staticmethod
+ def apply_new_password(password):
+ out1 = run_command_argv([
+ "/usr/share/wazuh-indexer/plugins/opensearch-security/tools/wazuh-passwords-tool.sh",
+ "-u", "kibanaserver",
+ "-p", password,
+ ]) or ""
+ out2 = run_command_argv(
+ [
+ "/usr/share/wazuh-dashboard/bin/opensearch-dashboards-keystore",
+ "--allow-root", "add", "-f", "--stdin", "opensearch.password",
+ ],
+ input=password,
+ ) or ""
+ return f"{out1}\n{out2}"
+
+ # -----------------------------------------
+ # VERIFY PASSWORD
+ # -----------------------------------------
+ @staticmethod
+ def verify_password(password):
+ try:
+ resp = requests.get(
+ INDEXER_URL,
+ auth=(KIBANA_USERNAME, password),
+ verify=False,
+ timeout=10,
+ )
+ return resp.text
+ except requests.RequestException as e:
+ return str(e)
+
+ # -----------------------------------------
+ # HEAP FIX STEPS (manual instructions)
+ # -----------------------------------------
+ @staticmethod
+ def heap_steps():
+ return (
+ "Edit file:\n"
+ " /etc/wazuh-indexer/jvm.options\n\n"
+ "Set heap to 50% of your RAM.\n"
+ "Example for 8 GB system:\n"
+ " -Xms4g\n"
+ " -Xmx4g\n\n"
+ "Then restart:\n"
+ " systemctl restart wazuh-indexer"
+ )
+
+ # -------------------------------------------------------------------------
+ # FIX JVM HEAP
+ # -------------------------------------------------------------------------
+ @staticmethod
+ def fix_jvm_heap(heap_gb):
+
+ heap_gb = int(heap_gb)
+ jvm_options_path = "/etc/wazuh-indexer/jvm.options"
+
+ replace_in_file(jvm_options_path, r"^-Xms.*", f"-Xms{heap_gb}g", flags=re.MULTILINE)
+ replace_in_file(jvm_options_path, r"^-Xmx.*", f"-Xmx{heap_gb}g", flags=re.MULTILINE)
+
+ # Restart and actually wait for the service to come back up, the
+ # same way fix_indexer_ip() and fix_indexer_cert_paths() do.
+ # A bare run_command("systemctl restart ...") returns immediately
+ # once the restart is *issued*, not once it's actually active, so
+ # it can look like nothing happened if the service takes a moment
+ # or fails to come back up.
+ status = FixEngine.restart_indexer_and_wait()
+
+ updated = run_command(
+ "grep -E '^-Xms|^-Xmx' "
+ "/etc/wazuh-indexer/jvm.options"
+ ) or "(could not read)"
+
+ return {"updated": updated, "status": status}
+
+ # -----------------------------------------
+ # SECURITY INIT COMMAND
+ # -----------------------------------------
+ @staticmethod
+ def init_command():
+ return (
+ "/usr/share/wazuh-indexer/bin/indexer-security-init.sh"
+ )
+
+ # -----------------------------------------
+ # PERMISSION FIX STEPS (manual instructions)
+ # -----------------------------------------
+ @staticmethod
+ def permission_fix():
+ return (
+ "Run the following commands:\n"
+ " chmod 600 /usr/share/wazuh-indexer/config/jvm.options\n"
+ " chmod 600 /usr/share/wazuh-indexer/config/opensearch.yml\n"
+ " chmod 600 /usr/share/wazuh-indexer/config/opensearch-security/*.yml\n\n"
+ "Then restart:\n"
+ " systemctl restart wazuh-indexer"
+ )
+
+ # -----------------------------------------
+ # DISK CHECK
+ # -----------------------------------------
+ @staticmethod
+ def check_disk():
+ return run_command("df -h") or ""
+
+ # -----------------------------------------
+ # MANUAL COMMAND SETS (for "give me commands" path)
+ # -----------------------------------------
+ @staticmethod
+ def commands_ip_fix(c_ip):
+ return (
+ f"sed -i 's|https://.*:9200|https://{c_ip}:9200|' "
+ "/etc/wazuh-dashboard/opensearch_dashboards.yml\n"
+ "systemctl restart wazuh-dashboard"
+ )
+
+ @staticmethod
+ def commands_cert_permissions():
+ return (
+ "chmod 500 /etc/wazuh-dashboard/certs\n"
+ "chmod 400 /etc/wazuh-dashboard/certs/*\n"
+ "chown -R wazuh-dashboard:wazuh-dashboard /etc/wazuh-dashboard/certs\n"
+ "systemctl restart wazuh-dashboard"
+ )
+
+ @staticmethod
+ def commands_restart_indexer():
+ return "systemctl restart wazuh-indexer"
+
+ @staticmethod
+ def commands_get_indexer_logs():
+ return (
+ "journalctl -u wazuh-indexer --since '1 hour ago' "
+ "| grep -i -E 'error|warn'"
+ )
+
+ # -----------------------------------------
+ # CHECK INDEXER IP (control vs opensearch.yml)
+ # -----------------------------------------
+ @staticmethod
+ def check_indexer_ip():
+ control = FixEngine.get_control_ip()
+ indexer = FixEngine.get_indexer_ip()
+
+ c_ip = FixEngine.extract_ip(control)
+ i_ip = FixEngine.extract_ip(indexer)
+
+ return {
+ "c_ip": c_ip,
+ "i_ip": i_ip,
+ "match": bool(c_ip and i_ip and c_ip == i_ip),
+ }
+
+ # -----------------------------------------
+ # FIX INDEXER IP (auto correct) + restart, waits for active
+ # -----------------------------------------
+ @staticmethod
+ def fix_indexer_ip(c_ip):
+ replace_in_file(
+ "/etc/wazuh-indexer/opensearch.yml",
+ r"^network\.host:.*",
+ f"network.host: {c_ip}",
+ flags=re.MULTILINE,
+ )
+ return FixEngine.restart_indexer_and_wait()
+
+ # -----------------------------------------
+ # CHECK INDEXER CERT PATHS
+ # -----------------------------------------
+ @staticmethod
+ def check_indexer_cert_paths():
+ paths_raw = run_command(
+ "grep -E 'pemkey_filepath|pemcert_filepath|pemtrustedcas_filepath' "
+ "/etc/wazuh-indexer/opensearch.yml"
+ ) or ""
+
+ files_raw = run_command("ls /etc/wazuh-indexer/certs") or ""
+
+ configured = []
+ for line in paths_raw.splitlines():
+ if ":" in line:
+ val = line.split(":", 1)[1].strip()
+ configured.append(val.split("/")[-1])
+
+ actual = [f.strip() for f in files_raw.splitlines() if f.strip()]
+ missing = [f for f in configured if f not in actual]
+
+ return {
+ "paths_raw": paths_raw,
+ "files_raw": files_raw,
+ "missing": missing,
+ }
+
+ # -----------------------------------------
+ # FIX INDEXER CERT PATHS (auto correct) + restart, waits for active
+ # -----------------------------------------
+ @staticmethod
+ def fix_indexer_cert_paths():
+ actual_files = run_command("ls /etc/wazuh-indexer/certs") or ""
+ actual = [f.strip() for f in actual_files.splitlines() if f.strip()]
+
+ key = next((f for f in actual if "key" in f and "admin" not in f), None)
+ cert = next((f for f in actual if "key" not in f and "root" not in f
+ and "admin" not in f), None)
+ ca = next((f for f in actual if "root-ca" in f), None)
+
+ if not (key and cert and ca):
+ return {"success": False}
+
+ base = "/etc/wazuh-indexer/certs"
+ opensearch_yml = "/etc/wazuh-indexer/opensearch.yml"
+ replace_in_file(opensearch_yml, r"pemcert_filepath:.*", f"pemcert_filepath: {base}/{cert}")
+ replace_in_file(opensearch_yml, r"pemkey_filepath:.*", f"pemkey_filepath: {base}/{key}")
+ replace_in_file(opensearch_yml, r"pemtrustedcas_filepath:.*", f"pemtrustedcas_filepath: {base}/{ca}")
+
+ status = FixEngine.restart_indexer_and_wait()
+
+ return {"success": True, "cert": cert, "key": key, "ca": ca, "status": status}
+
+ # -----------------------------------------
+ # CHECK JVM HEAP (current vs recommended)
+ # -----------------------------------------
+ @staticmethod
+ def check_jvm_heap():
+ current = run_command(
+ "grep -E '^-Xms|^-Xmx' /etc/wazuh-indexer/jvm.options"
+ ) or "(could not read)"
+
+ total_kb = run_command(
+ "grep MemTotal /proc/meminfo | awk '{print $2}'"
+ ) or "0"
+
+ try:
+ total_gb = round(int(total_kb.strip()) / 1024 / 1024)
+ except ValueError:
+ total_gb = 0
+
+ heap_gb = max(1, total_gb // 2)
+
+ return {"current": current, "total_gb": total_gb, "recommended_heap": heap_gb}
+
+ # -----------------------------------------
+ # CHECK DASHBOARD IP
+ # -----------------------------------------
+ @staticmethod
+ def check_dashboard_ip():
+ dash_raw = FixEngine.get_dashboard_ip()
+ control = FixEngine.get_control_ip()
+
+ d_ip = FixEngine.extract_ip(dash_raw)
+ c_ip = FixEngine.extract_ip(control)
+
+ return {
+ "d_ip": d_ip,
+ "c_ip": c_ip,
+ "match": bool(d_ip and c_ip and d_ip == c_ip),
+ }
+
+ # -----------------------------------------
+ # FIX DASHBOARD IP (auto correct) + restart, waits for active
+ # -----------------------------------------
+ @staticmethod
+ def fix_dashboard_ip(c_ip):
+ replace_in_file(
+ "/etc/wazuh-dashboard/opensearch_dashboards.yml",
+ r"https://\S*:9200",
+ f"https://{c_ip}:9200",
+ )
+ return restart_service_and_wait("wazuh-dashboard")
+
+ # -----------------------------------------
+ # CHECK DASHBOARD CERT PATHS
+ # -----------------------------------------
+ @staticmethod
+ def check_dashboard_cert_paths():
+ paths_raw = run_command(
+ "grep -E 'ssl.certificate|ssl.key|certificateAuthorities' "
+ "/etc/wazuh-dashboard/opensearch_dashboards.yml"
+ ) or ""
+
+ files_raw = run_command("ls /etc/wazuh-dashboard/certs") or ""
+
+ configured = []
+ for line in paths_raw.splitlines():
+ if ":" in line:
+ val = line.split(":", 1)[1].strip().strip('"').strip("'").strip("[]")
+ val = val.strip('"').strip("'")
+ filename = val.split("/")[-1]
+ if filename:
+ configured.append(filename)
+
+ actual = [f.strip() for f in files_raw.splitlines() if f.strip()]
+ missing = [f for f in configured if f not in actual]
+
+ return {
+ "paths_raw": paths_raw,
+ "files_raw": files_raw,
+ "missing": missing,
+ }
+
+ # -----------------------------------------
+ # FIX DASHBOARD CERT PATHS (auto correct) + restart, waits for active
+ # -----------------------------------------
+ @staticmethod
+ def fix_dashboard_cert_paths():
+ actual_files = run_command("ls /etc/wazuh-dashboard/certs") or ""
+ actual = [f.strip() for f in actual_files.splitlines() if f.strip()]
+
+ key = next((f for f in actual if "key" in f and "admin" not in f), None)
+ cert = next((f for f in actual if "key" not in f and "root" not in f
+ and "admin" not in f and "ca" not in f.lower()), None)
+ ca = next((f for f in actual if "root-ca" in f or
+ ("ca" in f.lower() and "key" not in f)), None)
+
+ if not (key and cert and ca):
+ return {"success": False}
+
+ base = "/etc/wazuh-dashboard/certs"
+ dashboard_yml = "/etc/wazuh-dashboard/opensearch_dashboards.yml"
+ replace_in_file(dashboard_yml, r"server\.ssl\.certificate:.*", f"server.ssl.certificate: {base}/{cert}")
+ replace_in_file(dashboard_yml, r"server\.ssl\.key:.*", f"server.ssl.key: {base}/{key}")
+ replace_in_file(
+ dashboard_yml,
+ r"opensearch\.ssl\.certificateAuthorities:.*",
+ f'opensearch.ssl.certificateAuthorities: ["{base}/{ca}"]',
+ )
+
+ status = restart_service_and_wait("wazuh-dashboard")
+
+ return {"success": True, "cert": cert, "key": key, "ca": ca, "status": status}
+
+ # -----------------------------------------
+ # DASHBOARD CERT PATH MANUAL STEPS
+ # -----------------------------------------
+ @staticmethod
+ def dashboard_cert_path_steps():
+ return (
+ "Update the cert paths in:\n"
+ " /etc/wazuh-dashboard/opensearch_dashboards.yml\n\n"
+ "Keys to fix:\n"
+ " server.ssl.certificate\n"
+ " server.ssl.key\n"
+ " opensearch.ssl.certificateAuthorities\n\n"
+ "Match them to the files in /etc/wazuh-dashboard/certs/"
+ )
diff --git a/integrations/wazuh-troubleshooting-tool/backend/utils/github_discussions.py b/integrations/wazuh-troubleshooting-tool/backend/utils/github_discussions.py
new file mode 100644
index 00000000..4efa7492
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/utils/github_discussions.py
@@ -0,0 +1,131 @@
+"""
+Shared GitHub Discussions fetcher - used by both sync_lgtm_issues.py
+(wazuh/community discussions) and sync_public_wazuh_issues.py (wazuh/wazuh
+discussions, e.g. https://github.com/wazuh/wazuh/discussions).
+
+Discussions have no labels the way issues do, and no REST search endpoint at
+all - only GraphQL, via the repository's own discussions connection (not the
+Search API, so no 1000-result cap here, just normal cursor pagination).
+isAnswered is used as the proxy for "resolved" since that's the closest
+equivalent to LGTM/review-quality/resolved that Discussions has.
+
+GraphQL always requires a token (no anonymous calls at all), but for a
+PUBLIC repo any basic token works fine, even one scoped to a different repo,
+since fine-grained PATs get implicit read access to all public repos.
+"""
+import time
+import requests
+
+DISCUSSIONS_QUERY = """
+query($owner: String!, $name: String!, $after: String) {
+ repository(owner: $owner, name: $name) {
+ discussions(first: 50, after: $after, orderBy: {field: UPDATED_AT, direction: DESC}) {
+ pageInfo { hasNextPage endCursor }
+ nodes {
+ number
+ title
+ bodyText
+ url
+ isAnswered
+ updatedAt
+ answer { bodyText }
+ comments(first: 50) { nodes { bodyText } }
+ }
+ }
+ }
+}
+"""
+
+
+def fetch_answered_discussions(repo: str, token: str, label: str = None, max_items: int = None, min_updated_at: str = None) -> list:
+ """
+ repo: "owner/name", e.g. "wazuh/wazuh" or "wazuh/community"
+ token: any GitHub token - required (GraphQL has no anonymous mode)
+ label: just used for print statements, defaults to `repo`
+ max_items: stop once this many *answered* discussions are collected (None = no cap).
+ Results are already ordered most-recently-updated first, so a
+ cap stays biased toward current relevance, same idea as
+ sync_public_wazuh_issues.py's WAZUH_MAX_ISSUES.
+ min_updated_at: ISO 8601 date string, e.g. "2023-07-18". Since results are
+ ordered newest-updated-first, the moment a discussion older
+ than this cutoff shows up, everything after it is guaranteed
+ to be even older - we drop it and stop paginating entirely
+ rather than just filtering it out, saving the extra requests.
+ """
+ if not token:
+ print(f" Skipping {repo} discussions: no token available (GraphQL requires one, even for public repos).", flush=True)
+ return []
+
+ label = label or repo
+ owner, name = repo.split("/")
+ answered = []
+ after = None
+ page = 1
+ while True:
+ print(f"Fetching {label} discussions (page {page})...", flush=True)
+ resp = None
+ for attempt in range(3):
+ try:
+ resp = requests.post(
+ "https://api.github.com/graphql",
+ headers={"Authorization": f"Bearer {token}"},
+ json={"query": DISCUSSIONS_QUERY, "variables": {"owner": owner, "name": name, "after": after}},
+ timeout=30,
+ )
+ break
+ except requests.exceptions.RequestException as e:
+ if attempt == 2:
+ print(f" WARNING: {label} discussions fetch failed after 3 attempts ({e}) - stopping here", flush=True)
+ return answered
+ time.sleep(2 * (attempt + 1))
+ if resp.status_code != 200:
+ print(f" WARNING: GraphQL error {resp.status_code}: {resp.text[:200]} - stopping {label} discussions fetch here", flush=True)
+ break
+ data = resp.json().get("data", {}).get("repository", {}).get("discussions", {})
+ nodes = data.get("nodes", [])
+
+ hit_cutoff = False
+ if min_updated_at:
+ in_range = []
+ for d in nodes:
+ if d.get("updatedAt", "") < min_updated_at:
+ hit_cutoff = True
+ break
+ in_range.append(d)
+ nodes = in_range
+
+ new_answered = [d for d in nodes if d.get("isAnswered")]
+ answered.extend(new_answered)
+ print(f" {len(nodes)} discussions on this page, {len(new_answered)} answered ({len(answered)} answered so far)", flush=True)
+
+ if max_items and len(answered) >= max_items:
+ answered = answered[:max_items]
+ print(f" reached max_items cap ({max_items}) - stopping here", flush=True)
+ break
+ if hit_cutoff:
+ print(f" reached the {min_updated_at} cutoff - stopping here", flush=True)
+ break
+ page_info = data.get("pageInfo", {})
+ if not page_info.get("hasNextPage"):
+ break
+ after = page_info.get("endCursor")
+ page += 1
+ time.sleep(1)
+ return answered
+
+
+def discussion_to_issue_dict(d: dict) -> dict:
+ """Reshape a GraphQL discussion node into the same dict shape upsert_issue() expects."""
+ comments = [c.get("bodyText", "") for c in d.get("comments", {}).get("nodes", [])]
+ answer = d.get("answer")
+ if answer and answer.get("bodyText"):
+ comments.insert(0, f"ACCEPTED ANSWER: {answer['bodyText']}")
+ return {
+ "number": d["number"],
+ "title": d["title"],
+ "body": d.get("bodyText") or "",
+ "comments": comments,
+ "external_community": [],
+ "url": d["url"],
+ "labels": [],
+ }
diff --git a/integrations/wazuh-troubleshooting-tool/backend/utils/index_utils.py b/integrations/wazuh-troubleshooting-tool/backend/utils/index_utils.py
new file mode 100644
index 00000000..7bb3e813
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/utils/index_utils.py
@@ -0,0 +1,95 @@
+
+import re
+import datetime
+from utils.api_utils import indexer_api_get, indexer_api_delete
+
+_INDEX_DATE_RE = re.compile(r"(\d{4})\.(\d{2})\.(\d{2})")
+
+
+def list_indices(pattern="wazuh-alerts-*"):
+ raw = indexer_api_get(
+ f"/_cat/indices/{pattern}?h=index,health,status,docs.count,store.size"
+ ) or ""
+ rows = []
+ for line in raw.splitlines():
+ parts = line.split()
+ if len(parts) >= 5:
+ rows.append({
+ "index": parts[0], "health": parts[1], "status": parts[2],
+ "docs_count": parts[3], "store_size": parts[4],
+ })
+ return rows
+
+
+def index_has_todays_date(pattern="wazuh-alerts-*"):
+ today = datetime.date.today().strftime("%Y.%m.%d")
+ return any(today in row["index"] for row in list_indices(pattern))
+
+
+def _index_date(index_name):
+ m = _INDEX_DATE_RE.search(index_name)
+ if not m:
+ return None
+ try:
+ return datetime.date(int(m.group(1)), int(m.group(2)), int(m.group(3)))
+ except ValueError:
+ return None
+
+
+def _freshness_result(index_name, index_date):
+ if not index_name or not index_date:
+ return {"index": index_name, "date": None, "days_old": None, "is_today": False}
+ days_old = (datetime.date.today() - index_date).days
+ return {"index": index_name, "date": index_date, "days_old": days_old, "is_today": days_old <= 0}
+
+
+def check_most_recent_index(pattern="wazuh-alerts-*"):
+ """
+ Find the newest index matching `pattern` by its date suffix (not creation
+ time - the suffix is what tells us whether TODAY's alerts are landing).
+ Returns {"index", "date", "days_old", "is_today"} - all None/False if no
+ index in the pattern has a parseable date suffix.
+ """
+ best_name, best_date = None, None
+ for row in list_indices(pattern):
+ d = _index_date(row["index"])
+ if d and (best_date is None or d > best_date):
+ best_name, best_date = row["index"], d
+ return _freshness_result(best_name, best_date)
+
+
+def check_index_name_freshness(index_name):
+ """Same result shape as check_most_recent_index(), for a single index name
+ a user typed/pasted in manually rather than one we looked up ourselves."""
+ return _freshness_result(index_name, _index_date(index_name or ""))
+
+
+def select_indices_by_age(pattern="wazuh-alerts-*", older_than_days=None, start_date=None, end_date=None):
+ """
+ Resolve a request like "older than 30 days" or "2026-01-01 to
+ 2026-01-07" into a concrete list of index rows, WITHOUT deleting
+ anything. Callers must show this list to the user and get explicit
+ confirmation before calling delete_indices().
+ """
+ indices = list_indices(pattern)
+ today = datetime.date.today()
+ cutoff = today - datetime.timedelta(days=older_than_days) if older_than_days else None
+
+ matched = []
+ for row in indices:
+ d = _index_date(row["index"])
+ if d is None:
+ continue
+ if cutoff and d >= cutoff:
+ continue
+ if start_date and d < start_date:
+ continue
+ if end_date and d > end_date:
+ continue
+ matched.append(row)
+ return matched
+
+
+def delete_indices(index_names):
+ """Delete an explicit list of indices. Caller must have already shown & confirmed this exact list."""
+ return {name: indexer_api_delete(f"/{name}") for name in index_names}
diff --git a/integrations/wazuh-troubleshooting-tool/backend/utils/lgtm_db.py b/integrations/wazuh-troubleshooting-tool/backend/utils/lgtm_db.py
new file mode 100644
index 00000000..224fbe43
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/utils/lgtm_db.py
@@ -0,0 +1,148 @@
+"""
+SQLite-backed store for the unified Wazuh knowledge base, with embeddings for
+semantic search. Covers three sources, all searched together as one pool:
+ - wazuh/community issues labeled LGTM or review/quality
+ - wazuh/community Discussions (isAnswered)
+ - wazuh/wazuh (public repo) closed issues
+
+Replaces the old lgtm_issues.json + fuzzy-string-matching approach - fuzzy
+matching only catches questions textually similar to a stored issue; cosine
+similarity over embeddings also catches semantically similar questions worded
+completely differently.
+
+Embeddings are computed once per item at sync time (not per query), so the
+per-query cost is just one embedding call for the user's question plus an
+in-memory cosine-similarity scan - fast even on CPU (a few thousand items'
+worth of 768-dim float32 vectors is a few MB, trivial to hold in memory).
+
+Primary key is "source:number" (not just number) since community issues,
+community discussions, and public wazuh/wazuh issues each have their own
+independent numbering and would otherwise collide.
+"""
+import json
+import os
+import sqlite3
+import struct
+
+import numpy as np
+
+from utils.embeddings import embed_document, embed_query
+
+_DB_PATH = os.path.join(os.path.dirname(__file__), "..", "knowledge", "lgtm.db")
+
+
+def _connect():
+ os.makedirs(os.path.dirname(_DB_PATH), exist_ok=True)
+ conn = sqlite3.connect(_DB_PATH)
+ conn.execute("""
+ CREATE TABLE IF NOT EXISTS issues (
+ id TEXT PRIMARY KEY,
+ source TEXT,
+ number INTEGER,
+ title TEXT,
+ body TEXT,
+ comments TEXT,
+ external_community TEXT,
+ url TEXT,
+ labels TEXT,
+ embedding BLOB
+ )
+ """)
+ return conn
+
+
+def _to_blob(vector):
+ return struct.pack(f"{len(vector)}f", *vector)
+
+
+def _from_blob(blob):
+ n = len(blob) // 4
+ return np.array(struct.unpack(f"{n}f", blob), dtype=np.float32)
+
+
+def _embedding_text(issue):
+ return (
+ issue["title"] + "\n"
+ + issue["body"][:1000] + "\n"
+ + "\n".join(issue.get("comments", []))[:2000]
+ )
+
+
+def upsert_issue(issue: dict, source: str = "community_issue") -> bool:
+ """issue: {number, title, body, comments, external_community, url, labels}.
+ source: "community_issue" | "community_discussion" | "public_wazuh_issue".
+ Returns False (and doesn't write) if the embedding call fails, so a
+ flaky Ollama request during sync doesn't corrupt the DB with a null vector."""
+ embedding = embed_document(_embedding_text(issue))
+ if embedding is None:
+ return False
+
+ item_id = f"{source}:{issue['number']}"
+ conn = _connect()
+ conn.execute(
+ """INSERT INTO issues (id, source, number, title, body, comments, external_community, url, labels, embedding)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ ON CONFLICT(id) DO UPDATE SET
+ title=excluded.title, body=excluded.body, comments=excluded.comments,
+ external_community=excluded.external_community, url=excluded.url,
+ labels=excluded.labels, embedding=excluded.embedding""",
+ (
+ item_id, source, issue["number"], issue["title"], issue["body"],
+ json.dumps(issue.get("comments", [])),
+ json.dumps(issue.get("external_community", [])),
+ issue.get("url", ""), json.dumps(issue.get("labels", [])),
+ _to_blob(embedding),
+ ),
+ )
+ conn.commit()
+ conn.close()
+ return True
+
+
+def _row_to_issue(row):
+ return {
+ "source": row[0], "number": row[1], "title": row[2], "body": row[3],
+ "comments": json.loads(row[4]), "external_community": json.loads(row[5]),
+ "url": row[6], "labels": json.loads(row[7]),
+ }
+
+
+def count(source: str = None) -> int:
+ if not os.path.exists(_DB_PATH):
+ return 0
+ conn = _connect()
+ if source:
+ n = conn.execute("SELECT COUNT(*) FROM issues WHERE source=?", (source,)).fetchone()[0]
+ else:
+ n = conn.execute("SELECT COUNT(*) FROM issues").fetchone()[0]
+ conn.close()
+ return n
+
+
+def search(query: str, top_n: int = 3, min_similarity: float = 0.5) -> list:
+ if not query or not os.path.exists(_DB_PATH):
+ return []
+
+ query_embedding = embed_query(query)
+ if query_embedding is None:
+ return []
+
+ q = np.array(query_embedding, dtype=np.float32)
+ q = q / (np.linalg.norm(q) or 1)
+
+ conn = _connect()
+ rows = conn.execute(
+ "SELECT source, number, title, body, comments, external_community, url, labels, embedding FROM issues"
+ ).fetchall()
+ conn.close()
+
+ scored = []
+ for row in rows:
+ emb = _from_blob(row[8])
+ emb = emb / (np.linalg.norm(emb) or 1)
+ similarity = float(np.dot(q, emb))
+ if similarity >= min_similarity:
+ scored.append((similarity, _row_to_issue(row[:8])))
+
+ scored.sort(key=lambda x: x[0], reverse=True)
+ return [issue for _, issue in scored[:top_n]]
diff --git a/integrations/wazuh-troubleshooting-tool/backend/utils/lgtm_utils.py b/integrations/wazuh-troubleshooting-tool/backend/utils/lgtm_utils.py
new file mode 100644
index 00000000..98cbe773
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/utils/lgtm_utils.py
@@ -0,0 +1,40 @@
+"""
+Reads the locally-synced LGTM/review-quality knowledge base (backend/knowledge/lgtm.db,
+a SQLite DB with per-issue embeddings) and semantically matches it against a
+user's question via cosine similarity. Needs Ollama running locally with
+nomic-embed-text pulled. No GitHub credentials — the DB must already exist,
+produced by backend/knowledge/sync_lgtm_issues.py.
+"""
+from utils import lgtm_db
+
+
+def find_relevant_issues(query: str, top_n: int = 3, min_similarity: float = 0.5) -> list:
+ """Return up to top_n issues whose content is semantically closest to the query."""
+ return lgtm_db.search(query, top_n=top_n, min_similarity=min_similarity)
+
+
+def format_lgtm_context(issues: list) -> str:
+ if not issues:
+ return ""
+ parts = ["=== Internal knowledge base: previously resolved, verified Wazuh issues ==="]
+ for issue in issues:
+ resolution_bits = issue.get("comments", []) + issue.get("external_community", [])
+ # 2000 chars, not 1200 - some threads have a first-draft answer followed
+ # by a reviewer's correction, and the correction matters more than the
+ # original; truncating too early risks cutting off exactly that part.
+ resolution = "\n---\n".join(resolution_bits)[:2000]
+ parts.append(
+ f"- Issue #{issue['number']}: {issue['title']}\n"
+ f" Question: {issue['body'][:500]}\n"
+ f" Discussion (in order - later comments may correct or refine earlier ones,\n"
+ f" e.g. a reviewer pointing out what the first answer got wrong or missed):\n"
+ f" {resolution}"
+ )
+ parts.append(
+ "Instructions: use the above to ground your answer. When a discussion thread contains "
+ "a correction or refinement to an earlier answer, follow the corrected version, not the "
+ "original. Do not quote this verbatim or mention it is from an internal issue tracker — "
+ "this content is confidential and for grounding only."
+ )
+ parts.append("=================================================")
+ return "\n".join(parts)
diff --git a/integrations/wazuh-troubleshooting-tool/backend/utils/log_analyzer.py b/integrations/wazuh-troubleshooting-tool/backend/utils/log_analyzer.py
new file mode 100644
index 00000000..af16c11e
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/utils/log_analyzer.py
@@ -0,0 +1,58 @@
+class LogAnalyzer:
+
+ # -----------------------------------------
+ # DETECT KNOWN ISSUES FROM LOG TEXT
+ # Returns a list of issue keys found in the logs
+ # e.g. ["heap", "auth"]
+ # -----------------------------------------
+ @staticmethod
+ def get_issues(logs):
+ if not logs:
+ return []
+
+ text = logs.lower()
+ issues = []
+
+ # NOT INITIALIZED
+ # Only relevant on fresh/new installations
+ if "not yet initialized" in text:
+ issues.append("init")
+
+ # HEAP / MEMORY
+ # Can be fixed auto or manual — handled in dashboard_error_flow
+ if any(kw in text for kw in [
+ "circuit_breaking_exception",
+ "data too large",
+ "high heap usage",
+ "gc did bring memory usage down",
+ "g1gc",
+ "heap usage",
+ ]):
+ issues.append("heap")
+
+ # AUTH FAILURE
+ # Flag only — fix steps to be added later
+ if "authentication finally failed for kibanaserver" in text:
+ issues.append("auth")
+
+ # DISK WATERMARK
+ # Inform only — user must free up disk manually
+ if any(kw in text for kw in [
+ "low disk watermark",
+ "high disk watermark",
+ "flood stage disk watermark",
+ "disk usage exceeded",
+ ]):
+ issues.append("watermark")
+
+ # FILE PERMISSIONS
+ # Flag only — fix steps to be added later
+ if "insecure file permissions" in text:
+ issues.append("permission")
+ if any(kw in text for kw in [
+ "econnrefused",
+ "connectionerror",
+ "connect econnrefused",
+ ]):
+ issues.append("dashboard_connection_refused")
+ return issues
diff --git a/integrations/wazuh-troubleshooting-tool/backend/utils/log_handler.py b/integrations/wazuh-troubleshooting-tool/backend/utils/log_handler.py
new file mode 100644
index 00000000..b01028c0
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/utils/log_handler.py
@@ -0,0 +1,57 @@
+from executor import run_command
+import re
+
+
+class LogHandler:
+
+ # -----------------------------------------
+ # GET INDEXER LOGS
+ # Reads from /var/log/wazuh-indexer/wazuh-cluster.log
+ # Uses awk to filter lines from the last X hours,
+ # then grep to keep only error/warn lines.
+ # Fixed: ($1" "$2) compares only the timestamp portion
+ # of each line — not the entire line — so filtering
+ # works correctly regardless of log content.
+ # -----------------------------------------
+ @staticmethod
+ def get_indexer_logs(hours=2):
+ cmd = (
+ f"awk -v d1=\"$(date --date='{hours} hours ago' '+%Y-%m-%d %H:%M:%S')\" "
+ f"'($1\" \"$2) >= d1' /var/log/wazuh-indexer/wazuh-cluster.log "
+ "| grep -i -E 'error|warn'"
+ )
+ return run_command(cmd) or ""
+
+ # -----------------------------------------
+ # GET DASHBOARD LOGS
+ # Uses journalctl for the wazuh-dashboard service
+ # -----------------------------------------
+ @staticmethod
+ def get_dashboard_logs(hours=2):
+ return run_command(
+ f"journalctl -u wazuh-dashboard --since '{hours} hours ago' "
+ "| grep -i -E 'error|warn'"
+ ) or ""
+
+ # -----------------------------------------
+ # CLEAN LOGS
+ # Deduplicates lines, strips timestamps for
+ # comparison only, returns max 50 unique lines
+ # -----------------------------------------
+ @staticmethod
+ def clean_logs(log_text):
+ if not log_text:
+ return "(no logs found)"
+
+ lines = log_text.splitlines()
+ seen = set()
+ unique = []
+
+ for line in lines:
+ # strip HH:MM:SS for dedup comparison only
+ cleaned = re.sub(r"\d{2}:\d{2}:\d{2}", "", line).strip()
+ if cleaned and cleaned not in seen:
+ seen.add(cleaned)
+ unique.append(line) # keep original line for display
+
+ return "\n".join(unique[:50])
diff --git a/integrations/wazuh-troubleshooting-tool/backend/utils/manager_config_utils.py b/integrations/wazuh-troubleshooting-tool/backend/utils/manager_config_utils.py
new file mode 100644
index 00000000..97fd5a16
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/utils/manager_config_utils.py
@@ -0,0 +1,44 @@
+import re
+from executor import run_command
+
+OSSEC_CONF_PATH = "/var/ossec/etc/ossec.conf"
+
+
+def get_log_alert_level():
+ """Configured log_alert_level as an int, or None if not set/found."""
+ raw = run_command(f"grep 'log_alert_level' {OSSEC_CONF_PATH}") or ""
+ match = re.search(r"\s*(\d+)\s*", raw)
+ return int(match.group(1)) if match else None
+
+
+def is_log_alert_level_ok(level=None):
+ """
+ Alerts with a rule level below log_alert_level are never logged by the
+ manager. A value above 15 means most/all alerts get silently dropped
+ before they ever reach alerts.json.
+ """
+ level = get_log_alert_level() if level is None else level
+ return level is not None and level <= 15
+
+
+def set_log_alert_level(value=3):
+ """Set log_alert_level in ossec.conf. Caller is responsible for restarting wazuh-manager afterward."""
+ replacement = "{}<\\/log_alert_level>".format(value)
+ cmd = "sed -i 's/.*<\\/log_alert_level>/{}/' {}".format(replacement, OSSEC_CONF_PATH)
+ run_command(cmd)
+ return get_log_alert_level()
+
+
+def get_jsonout_output_enabled():
+ """True if jsonout_output is set to 'yes' (required for alerts.json to be written)."""
+ raw = run_command(f"grep 'jsonout_output' {OSSEC_CONF_PATH}") or ""
+ match = re.search(r"\s*(yes|no)\s*", raw, re.IGNORECASE)
+ return bool(match and match.group(1).lower() == "yes")
+
+
+def enable_jsonout_output():
+ """Set jsonout_output to yes in ossec.conf. Caller is responsible for restarting wazuh-manager afterward."""
+ replacement = "yes<\\/jsonout_output>"
+ cmd = "sed -i 's/.*<\\/jsonout_output>/{}/' {}".format(replacement, OSSEC_CONF_PATH)
+ run_command(cmd)
+ return get_jsonout_output_enabled()
diff --git a/integrations/wazuh-troubleshooting-tool/backend/utils/manager_log_utils.py b/integrations/wazuh-troubleshooting-tool/backend/utils/manager_log_utils.py
new file mode 100644
index 00000000..11161fe2
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/utils/manager_log_utils.py
@@ -0,0 +1,38 @@
+import time
+from executor import run_command
+
+OSSEC_LOG_PATH = "/var/ossec/logs/ossec.log"
+
+
+def tail_manager_log(lines=200):
+ """Raw last N lines of ossec.log, no filtering."""
+ return run_command(f"tail -n {lines} {OSSEC_LOG_PATH}") or ""
+
+
+def get_manager_log_errors(lines=200):
+ """Last N lines of ossec.log filtered to error/warn only."""
+ return run_command(f"tail -n {lines} {OSSEC_LOG_PATH} | grep -i -E 'error|warn'") or ""
+
+
+def has_manager_log_errors(lines=200):
+ """True/False, so callers don't have to test truthiness of the string themselves."""
+ return bool(get_manager_log_errors(lines).strip())
+
+
+def manager_log_age_seconds():
+ """Seconds since ossec.log was last written to, or None if the file doesn't exist."""
+ exists = (run_command(f"test -f {OSSEC_LOG_PATH} && echo yes || echo no") or "").strip()
+ if exists != "yes":
+ return None
+ mtime_raw = (run_command(f"stat -c %Y {OSSEC_LOG_PATH}") or "").strip()
+ try:
+ return int(time.time()) - int(mtime_raw)
+ except ValueError:
+ return None
+
+
+def get_manager_disk_usage():
+ """`df -h` for the manager's data directory - checked when the pipeline test
+ (restarting an agent and looking for its event) comes back empty, since a
+ full disk on the manager is a common silent cause of that."""
+ return run_command("df -h /var/ossec") or ""
diff --git a/integrations/wazuh-troubleshooting-tool/backend/utils/pipeline_utils.py b/integrations/wazuh-troubleshooting-tool/backend/utils/pipeline_utils.py
new file mode 100644
index 00000000..4a24016c
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/utils/pipeline_utils.py
@@ -0,0 +1,181 @@
+"""
+Reusable helpers for diagnosing the full Wazuh alert pipeline:
+
+ Wazuh Agent -> Wazuh Manager -> alerts.json -> Filebeat -> Wazuh Indexer -> Dashboard
+
+Centralized here so any use case (no_alerts_are_showing, indexing_error,
+cluster_issues, etc.) can reuse the same checks instead of each one
+re-implementing ossec.conf parsing / alerts.json staleness / shard
+analysis by hand.
+"""
+
+import re
+import time
+
+from executor import run_command
+from utils.api_utils import indexer_api_get, indexer_api_get_json
+
+OSSEC_CONF_PATH = "/var/ossec/etc/ossec.conf"
+ALERTS_JSON_PATH = "/var/ossec/logs/alerts/alerts.json"
+OSSEC_LOG_PATH = "/var/ossec/logs/ossec.log"
+FILEBEAT_LOG_PATH = "/var/log/filebeat/filebeat"
+
+
+# ---------------------------------------------------------------------------
+# STEP 1/2 — AGENT STATUS
+# ---------------------------------------------------------------------------
+def get_agent_status(identifier=None):
+ """
+ Run `agent_control -l` and, if `identifier` (name or id) is given,
+ try to find that specific agent's line and report whether it's Active.
+
+ Returns {"raw": , "is_active": True/False/None}
+ `is_active` is None when we can't determine a clear answer
+ (e.g. identifier not found in the output).
+ """
+ raw = run_command("/var/ossec/bin/agent_control -l") or ""
+
+ is_active = None
+ if identifier:
+ for line in raw.splitlines():
+ if identifier.lower() in line.lower():
+ is_active = "active" in line.lower()
+ break
+ else:
+ is_active = bool(re.search(r"\bActive\b", raw))
+
+ return {"raw": raw, "is_active": is_active}
+
+
+# ---------------------------------------------------------------------------
+# STEP 3/4 — MANAGER CONFIG (ossec.conf)
+# ---------------------------------------------------------------------------
+def check_manager_config():
+ """
+ Verify the two ossec.conf settings that silently swallow alerts when
+ misconfigured: log_alert_level (must be <= 15) and jsonout_output
+ (must be "yes", otherwise alerts.json is never written).
+ """
+ level_raw = run_command(f"grep 'log_alert_level' {OSSEC_CONF_PATH}") or ""
+ jsonout_raw = run_command(f"grep 'jsonout_output' {OSSEC_CONF_PATH}") or ""
+
+ level_match = re.search(r"\s*(\d+)\s*", level_raw)
+ level = int(level_match.group(1)) if level_match else None
+
+ jsonout_match = re.search(
+ r"\s*(yes|no)\s*", jsonout_raw, re.IGNORECASE
+ )
+ jsonout_enabled = bool(jsonout_match and jsonout_match.group(1).lower() == "yes")
+
+ return {
+ "log_alert_level": level,
+ "log_alert_level_ok": level is not None and level <= 15,
+ "jsonout_output_enabled": jsonout_enabled,
+ "raw": "\n".join(x for x in [level_raw.strip(), jsonout_raw.strip()] if x),
+ }
+
+
+# ---------------------------------------------------------------------------
+# STEP 5 — alerts.json FRESHNESS
+# ---------------------------------------------------------------------------
+def get_alerts_json_status(max_age_seconds=300, tail_lines=5):
+ """
+ Check whether the manager is actively writing new alerts to
+ alerts.json (the file Filebeat reads from). `age_seconds` is how long
+ ago the file was last modified; if it's older than `max_age_seconds`
+ (default 5 min) we consider the pipeline stalled at the manager.
+ """
+ exists = (run_command(f"test -f {ALERTS_JSON_PATH} && echo yes || echo no") or "").strip()
+ if exists != "yes":
+ return {"exists": False, "age_seconds": None, "is_fresh": False, "tail": ""}
+
+ mtime_raw = (run_command(f"stat -c %Y {ALERTS_JSON_PATH}") or "").strip()
+ try:
+ age = int(time.time()) - int(mtime_raw)
+ except ValueError:
+ age = None
+
+ tail = run_command(f"tail -n {tail_lines} {ALERTS_JSON_PATH}") or ""
+
+ return {
+ "exists": True,
+ "age_seconds": age,
+ "is_fresh": age is not None and age <= max_age_seconds,
+ "tail": tail,
+ }
+
+
+# ---------------------------------------------------------------------------
+# STEP 6 — MANAGER LOGS (ossec.log)
+# ---------------------------------------------------------------------------
+def get_manager_log_errors(lines=200):
+ return run_command(f"tail -n {lines} {OSSEC_LOG_PATH} | grep -i -E 'error|warn'") or ""
+
+
+# ---------------------------------------------------------------------------
+# STEP 7 — FILEBEAT
+# ---------------------------------------------------------------------------
+def run_filebeat_output_test():
+ out = run_command("filebeat test output") or ""
+ upper = out.upper()
+ ok = "OK" in upper and "ERROR" not in upper
+ return {"raw": out, "ok": ok}
+
+
+def get_filebeat_log_errors(lines=200):
+ return run_command(f"tail -n {lines} {FILEBEAT_LOG_PATH} | grep -i -E 'error|warn'") or ""
+
+
+# ---------------------------------------------------------------------------
+# STEP 8/9 — INDEXER: CLUSTER HEALTH + SHARDS
+# ---------------------------------------------------------------------------
+def check_cluster_shards():
+ """
+ Pull /_cluster/health and, if the status isn't green and there are
+ unassigned shards, also pull the unassigned shard list so the caller
+ can show *why* they're unassigned (disk watermark, no replica node, etc).
+ """
+ health, raw_health = indexer_api_get_json("/_cluster/health")
+
+ if not health:
+ return {"reachable": False, "raw": raw_health}
+
+ result = {
+ "reachable": True,
+ "status": health.get("status", "unknown"),
+ "number_of_nodes": health.get("number_of_nodes", 0),
+ "active_shards": health.get("active_shards", 0),
+ "unassigned_shards": health.get("unassigned_shards", 0),
+ "raw": raw_health,
+ }
+
+ if result["status"] != "green" and result["unassigned_shards"]:
+ shards_raw = indexer_api_get("/_cat/shards?h=index,shard,state,unassigned.reason") or ""
+ result["unassigned_detail"] = "\n".join(
+ line for line in shards_raw.splitlines() if "UNASSIGNED" in line
+ )[:2000]
+
+ return result
+
+
+# ---------------------------------------------------------------------------
+# STEP 10 — INDEXER: wazuh-alerts-* INDICES
+# ---------------------------------------------------------------------------
+def check_alert_indices():
+ """
+ Confirm that wazuh-alerts-* indices exist, are healthy, and that one
+ matching today's date is present (i.e. new data is actually landing
+ in the indexer, not just old indices from before the issue started).
+ """
+ raw = indexer_api_get("/_cat/indices/wazuh-alerts-*?h=index,health,status,docs.count") or ""
+ today = (run_command("date +%Y.%m.%d") or "").strip()
+
+ lines = [l for l in raw.splitlines() if l.strip()]
+ todays_index = next((l for l in lines if today and today in l), None)
+
+ return {
+ "indices": lines,
+ "todays_index_present": bool(todays_index),
+ "todays_index": todays_index,
+ "raw": raw,
+ }
diff --git a/integrations/wazuh-troubleshooting-tool/backend/utils/public_repo_search.py b/integrations/wazuh-troubleshooting-tool/backend/utils/public_repo_search.py
new file mode 100644
index 00000000..2a2c51f3
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/utils/public_repo_search.py
@@ -0,0 +1,141 @@
+"""
+Live GitHub search against the public wazuh/wazuh repo — no local file, no
+private token required, called at chat time by the copilot.
+
+Optionally set GITHUB_TOKEN in the backend's own environment (not in any
+project file) to raise the rate limit from 60 requests/hour to 5,000/hour,
+and to enable discussion search — GitHub's GraphQL API has no anonymous
+mode, so discussions are skipped silently if no token is set. Issue search
+works fine with no token at all, just at the lower rate limit.
+"""
+import os
+import re
+import requests
+
+REPO = os.environ.get("WAZUH_REPO", "wazuh/wazuh")
+TOKEN = os.environ.get("GITHUB_TOKEN")
+
+_ISSUE_HEADERS = {"Accept": "application/vnd.github+json"}
+if TOKEN:
+ _ISSUE_HEADERS["Authorization"] = f"Bearer {TOKEN}"
+
+_TIMEOUT = 6 # keep the copilot responsive even if GitHub is slow or down
+
+
+def _sanitize_query(query: str) -> str:
+ """Strip characters that carry special meaning in GitHub search syntax
+ (e.g. a stray ':' or '"' from a user's question turning into a qualifier)."""
+ return re.sub(r'["\':]', ' ', query).strip()
+
+
+def _fetch_issue_comments(issue_number, limit=5):
+ try:
+ resp = requests.get(
+ f"https://api.github.com/repos/{REPO}/issues/{issue_number}/comments",
+ headers=_ISSUE_HEADERS,
+ params={"per_page": limit},
+ timeout=_TIMEOUT,
+ )
+ if resp.status_code != 200:
+ return []
+ return [c.get("body") or "" for c in resp.json()]
+ except Exception:
+ return []
+
+
+def search_public_issues(query: str, top_n: int = 2) -> list:
+ """Live keyword search against GitHub issues in wazuh/wazuh."""
+ q = _sanitize_query(query)
+ if not q:
+ return []
+ try:
+ resp = requests.get(
+ "https://api.github.com/search/issues",
+ headers=_ISSUE_HEADERS,
+ params={"q": f"repo:{REPO} is:issue {q}", "per_page": top_n},
+ timeout=_TIMEOUT,
+ )
+ if resp.status_code != 200:
+ return []
+ except Exception:
+ return []
+
+ results = []
+ for item in resp.json().get("items", [])[:top_n]:
+ results.append({
+ "number": item["number"],
+ "title": item["title"],
+ "body": (item.get("body") or "")[:500],
+ "comments": _fetch_issue_comments(item["number"]),
+ "url": item["html_url"],
+ })
+ return results
+
+
+_DISCUSSION_SEARCH_QUERY = """
+query($searchQuery: String!) {
+ search(query: $searchQuery, type: DISCUSSION, first: 3) {
+ nodes {
+ ... on Discussion {
+ number
+ title
+ bodyText
+ url
+ isAnswered
+ answer { bodyText }
+ }
+ }
+ }
+}
+"""
+
+
+def search_public_discussions(query: str, top_n: int = 2) -> list:
+ """Live search against GitHub Discussions in wazuh/wazuh. Requires a
+ token (GraphQL has no anonymous mode) - any basic token works fine for
+ a public repo. Returns [] silently if no token is configured."""
+ if not TOKEN:
+ return []
+ q = _sanitize_query(query)
+ if not q:
+ return []
+ try:
+ resp = requests.post(
+ "https://api.github.com/graphql",
+ headers={"Authorization": f"Bearer {TOKEN}"},
+ json={
+ "query": _DISCUSSION_SEARCH_QUERY,
+ "variables": {"searchQuery": f"repo:{REPO} {q}"},
+ },
+ timeout=_TIMEOUT,
+ )
+ if resp.status_code != 200:
+ return []
+ except Exception:
+ return []
+
+ nodes = resp.json().get("data", {}).get("search", {}).get("nodes", [])
+ results = []
+ for d in nodes[:top_n]:
+ answer = d.get("answer")
+ results.append({
+ "number": d.get("number"),
+ "title": d.get("title"),
+ "body": (d.get("bodyText") or "")[:500],
+ "answer": answer.get("bodyText") if answer else "",
+ "url": d.get("url"),
+ })
+ return results
+
+
+def format_public_context(issues: list, discussions: list) -> str:
+ if not issues and not discussions:
+ return ""
+ parts = ["=== Public wazuh/wazuh issues & discussions (live GitHub search) ==="]
+ for issue in issues:
+ resolution = "\n".join(issue.get("comments", []))[:800]
+ parts.append(f"- Issue #{issue['number']}: {issue['title']}\n {issue['body']}\n Discussion: {resolution}")
+ for d in discussions:
+ parts.append(f"- Discussion #{d['number']}: {d['title']}\n {d['body']}\n Answer: {d.get('answer', '')}")
+ parts.append("=================================================")
+ return "\n".join(parts)
diff --git a/integrations/wazuh-troubleshooting-tool/backend/utils/reindex_utils.py b/integrations/wazuh-troubleshooting-tool/backend/utils/reindex_utils.py
new file mode 100644
index 00000000..e999127b
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/utils/reindex_utils.py
@@ -0,0 +1,64 @@
+import json
+
+from utils.api_utils import indexer_api_post, indexer_api_delete
+
+
+def _step_ok(raw):
+ """
+ Did this indexer API call actually succeed? A non-2xx/error response, an
+ unparsable body, or a _reindex response with non-empty "failures" all
+ count as failed - checking only "truthy response text" previously let a
+ blocked/failed step through, and the next step would run anyway.
+ """
+ if not raw:
+ return False
+ try:
+ data = json.loads(raw)
+ except (ValueError, TypeError):
+ return False
+ if not isinstance(data, dict):
+ return False
+ if "error" in data:
+ return False
+ if data.get("failures"):
+ return False
+ return True
+
+
+def reindex_for_mapping_conflict(index_name):
+ """
+ 1. reindex -> -backup
+ 2. delete original
+ 3. reindex -backup -> (recreated with a clean mapping)
+ 4. delete -backup
+
+ Each step only runs if the previous one actually succeeded. This matters
+ because delete_original and delete_backup are irreversible - if step 1
+ (the backup) silently failed (e.g. index creation blocked cluster-wide)
+ and step 2 ran anyway, the original would be gone with no copy to
+ restore from. Stops and reports exactly which step failed instead.
+ """
+ backup_name = f"{index_name}-backup"
+ steps = {}
+
+ steps["backup"] = indexer_api_post(
+ "/_reindex", {"source": {"index": index_name}, "dest": {"index": backup_name}}
+ )
+ if not _step_ok(steps["backup"]):
+ steps["aborted_after"] = "backup"
+ return steps
+
+ steps["delete_original"] = indexer_api_delete(f"/{index_name}")
+ if not _step_ok(steps["delete_original"]):
+ steps["aborted_after"] = "delete_original"
+ return steps
+
+ steps["restore"] = indexer_api_post(
+ "/_reindex", {"source": {"index": backup_name}, "dest": {"index": index_name}}
+ )
+ if not _step_ok(steps["restore"]):
+ steps["aborted_after"] = "restore"
+ return steps
+
+ steps["delete_backup"] = indexer_api_delete(f"/{backup_name}")
+ return steps
diff --git a/integrations/wazuh-troubleshooting-tool/backend/utils/replica_utils.py b/integrations/wazuh-troubleshooting-tool/backend/utils/replica_utils.py
new file mode 100644
index 00000000..6c26bc4e
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/utils/replica_utils.py
@@ -0,0 +1,17 @@
+from utils.api_utils import indexer_api_put
+
+
+def recommend_replica_count(node_count):
+ """0 replicas for a single node (nowhere to put a copy); 1 for 2+ nodes."""
+ return 0 if node_count <= 1 else 1
+
+
+def set_replica_count(index_pattern, replicas):
+ """
+ Update number_of_replicas on an existing index/pattern. This is a
+ dynamic setting - it applies to existing indices immediately, no
+ reindex required (reindexing is a separate procedure for field-mapping
+ conflicts - see utils/reindex_utils.py).
+ """
+ body = {"index": {"number_of_replicas": replicas}}
+ return indexer_api_put(f"/{index_pattern}/_settings", body)
diff --git a/integrations/wazuh-troubleshooting-tool/backend/utils/response_utils.py b/integrations/wazuh-troubleshooting-tool/backend/utils/response_utils.py
new file mode 100644
index 00000000..a525761c
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/utils/response_utils.py
@@ -0,0 +1,30 @@
+"""
+Generic response-dict builder.
+
+Every use-case flow needs to return the same shape of dict back to the
+frontend: {"display": ..., "ask": ..., "done": ..., "context": ...}. Without
+a shared helper, every flow ends up hand-building this slightly differently
+(missing a key, forgetting to default "ask" to a list, etc). Use this
+instead of constructing the dict by hand.
+"""
+
+
+def make_response(display, ask=None, context=None, done=False, handoff=False):
+ """
+ Build a standard flow response.
+
+ display : str - the message to show the user.
+ ask : list - the question(s) being asked (empty list if none).
+ context : dict - the flow's context/state, carried forward.
+ done : bool - True if the troubleshooting flow is finished.
+ handoff : bool - True if control should be handed back to a parent
+ flow to continue at context["stage"] (used by
+ sub-flows like utils/step_flow.py).
+ """
+ return {
+ "display": display,
+ "ask": ask or [],
+ "done": done,
+ "context": context if context is not None else {},
+ "handoff": handoff,
+ }
diff --git a/integrations/wazuh-troubleshooting-tool/backend/utils/service_utils.py b/integrations/wazuh-troubleshooting-tool/backend/utils/service_utils.py
new file mode 100644
index 00000000..bff02248
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/utils/service_utils.py
@@ -0,0 +1,48 @@
+"""
+Generic systemd service helpers.
+
+Reusable by ANY use case that needs to check or restart a service
+(wazuh-indexer, wazuh-manager, wazuh-dashboard, filebeat, etc.) instead of
+each use case writing its own run_command("systemctl ...") + sleep loop by
+hand.
+
+IMPORTANT: restart/start use "--no-block" so the systemctl command returns
+immediately instead of blocking until the service is fully up. For
+JVM-based services (like wazuh-indexer) that startup can take a long time,
+and a plain blocking "systemctl restart" gives zero feedback while it waits
+- it just looks stuck. Firing the command with --no-block and then polling
+"is-active" ourselves, with a bounded window, avoids that.
+"""
+
+import time
+from executor import run_command
+
+
+def get_service_status(service_name):
+ """Return the current systemd status string: 'active', 'inactive', 'failed', etc."""
+ return (run_command(f"systemctl is-active {service_name}") or "").strip()
+
+
+def restart_service_and_wait(service_name, max_attempts=20, delay=3):
+ """
+ Restart a systemd service and wait for it to actually come back up.
+ Returns the final status string once active, or after max_attempts.
+ """
+ run_command(f"systemctl --no-block restart {service_name}")
+ return _poll_until_active(service_name, max_attempts, delay)
+
+
+def start_service_and_wait(service_name, max_attempts=20, delay=3):
+ """Same as restart_service_and_wait, but for starting a stopped service."""
+ run_command(f"systemctl --no-block start {service_name}")
+ return _poll_until_active(service_name, max_attempts, delay)
+
+
+def _poll_until_active(service_name, max_attempts, delay):
+ status = ""
+ for _ in range(max_attempts):
+ time.sleep(delay)
+ status = get_service_status(service_name)
+ if status == "active":
+ break
+ return status
diff --git a/integrations/wazuh-troubleshooting-tool/backend/utils/session_store.py b/integrations/wazuh-troubleshooting-tool/backend/utils/session_store.py
new file mode 100644
index 00000000..39920086
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/utils/session_store.py
@@ -0,0 +1,45 @@
+"""
+Persists Wazuh Copilot chat sessions to local, gzip-compressed files so
+conversation history survives a backend restart. Keeps only the 6 most
+recent chats — see utils/compressed_history.py for the underlying mechanism.
+
+Layout:
+ backend/sessions/manifest.json - {chat_id: {title, started_at, updated_at}}
+ backend/sessions/.json.gz - gzip-compressed JSON turn list
+"""
+import os
+
+from utils.compressed_history import CompressedHistoryStore
+
+MAX_SESSIONS = 6
+
+_SESSIONS_DIR = os.path.join(os.path.dirname(__file__), "..", "sessions")
+_store = CompressedHistoryStore(_SESSIONS_DIR, max_items=MAX_SESSIONS)
+
+
+def _derive_title(turns):
+ for t in turns:
+ if t.get("role") == "user" and t.get("text"):
+ text = t["text"].strip().replace("\n", " ")
+ return text[:60] + ("..." if len(text) > 60 else "")
+ return "New conversation"
+
+
+def save_session(chat_id, turns, brain=None):
+ _store.save(chat_id, turns, title=_derive_title(turns), extra_meta={"brain": brain} if brain else None)
+
+
+def load_session(chat_id):
+ return _store.load(chat_id)
+
+
+def list_sessions():
+ return [{"chat_id": e["id"], **{k: v for k, v in e.items() if k != "id"}} for e in _store.list()]
+
+
+def delete_session(chat_id):
+ _store.delete(chat_id)
+
+
+def rename_session(chat_id, new_title):
+ return _store.rename(chat_id, new_title)
diff --git a/integrations/wazuh-troubleshooting-tool/backend/utils/shard_utils.py b/integrations/wazuh-troubleshooting-tool/backend/utils/shard_utils.py
new file mode 100644
index 00000000..489a955a
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/utils/shard_utils.py
@@ -0,0 +1,64 @@
+from utils.api_utils import indexer_api_get, indexer_api_get_json, indexer_api_post_json
+
+DEFAULT_MAX_SHARDS_PER_NODE = 1000 # OpenSearch/Elasticsearch default cluster.max_shards_per_node
+
+
+def get_node_count():
+ """
+ Live node count from _cat/nodes rather than parsed out of a config.yml
+ on disk - that file's path/format isn't consistent across every
+ install type (single-node OVA, multi-node cluster, Docker, etc.), the
+ cluster itself always knows how many nodes it has.
+ """
+ raw = indexer_api_get("/_cat/nodes?h=name") or ""
+ names = [l.strip() for l in raw.splitlines() if l.strip()]
+ return len(names), names
+
+
+def get_total_shard_count():
+ """(active_shards + unassigned_shards) from _cluster/health. Returns (None, raw) on failure."""
+ health, raw = indexer_api_get_json("/_cluster/health")
+ if not health:
+ return None, raw
+ return health.get("active_shards", 0) + health.get("unassigned_shards", 0), raw
+
+
+def get_shard_limit(node_count=None):
+ node_count = node_count if node_count is not None else get_node_count()[0]
+ return DEFAULT_MAX_SHARDS_PER_NODE * max(node_count, 1)
+
+
+def get_shard_capacity_percent():
+ """Percent of the cluster's total shard capacity currently in use, or None if unreachable."""
+ node_count, _ = get_node_count()
+ total, _ = get_total_shard_count()
+ if total is None:
+ return None
+ limit = get_shard_limit(node_count)
+ return round((total / limit) * 100, 1) if limit else 0
+
+
+def is_near_shard_limit(threshold_percent=90):
+ percent = get_shard_capacity_percent()
+ return percent is not None and percent >= threshold_percent
+
+
+def get_unassigned_shards():
+ """List every currently-unassigned shard: index, shard #, prirep, reason code."""
+ raw = indexer_api_get("/_cat/shards?h=index,shard,prirep,state,unassigned.reason") or ""
+ rows = []
+ for line in raw.splitlines():
+ parts = line.split()
+ if len(parts) >= 4 and parts[3] == "UNASSIGNED":
+ rows.append({
+ "index": parts[0], "shard": parts[1], "prirep": parts[2],
+ "reason": parts[4] if len(parts) > 4 else "unknown",
+ })
+ return rows
+
+
+def explain_allocation(index, shard, primary=False):
+ """Raw _cluster/allocation/explain result for one specific unassigned shard."""
+ body = {"index": index, "shard": int(shard), "primary": primary}
+ data, raw = indexer_api_post_json("/_cluster/allocation/explain", body)
+ return data or {"error": raw}
diff --git a/integrations/wazuh-troubleshooting-tool/backend/utils/step_flow.py b/integrations/wazuh-troubleshooting-tool/backend/utils/step_flow.py
new file mode 100644
index 00000000..f739d60e
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/utils/step_flow.py
@@ -0,0 +1,292 @@
+"""
+Generic, reusable, PERMISSION-GATED sequential check/fix engine.
+
+Nothing here is specific to the Wazuh indexer, or to any one use case. Any
+use case can define an ordered list of "steps" (e.g. IP address, cert
+paths, heap memory - or, for a different use case, disk space, cluster
+health, filebeat status, agent connectivity, whatever) and get the exact
+same interaction pattern for free, instead of re-writing the same
+ask/check/fix/restart state machine by hand every time.
+
+THE PATTERN (identical for every step, in order):
+
+ 1. ASK: "Should I check the ? (yes / manual)"
+ yes -> run the step's own check_fn() and report the real result.
+ manual -> show manual_check_instructions_fn(), then ask the user to
+ self-report: "good to go" or "incorrect".
+
+ 2. If there's an issue (either from check_fn, or self-reported "incorrect"):
+ ASK: "Do you want me to fix this? (yes / manually)"
+ yes -> run auto_fix_fn(), then ask "fixed or ongoing?"
+ manually -> show manual_fix_instructions_fn(), wait for the user to
+ confirm they made the change, THEN restart (via
+ restart_fn, if provided), then ask "fixed or ongoing?"
+
+ 3. "fixed" -> stop, done.
+ "ongoing" -> move to the next step. If this was the last step, hand
+ off to whatever stage `next_stage_after_ongoing` points
+ at - the caller's own flow takes over from there.
+
+Nothing runs silently or gets batched - every question is its own turn,
+and only one check or fix action ever happens per turn.
+
+HOW TO DEFINE A STEP
+---------------------
+Each step is a dict:
+
+ {
+ "key": "ip", # short id, used in stage names
+ "title": "indexer IP address", # shown in questions to the user
+
+ "check_fn": fn(context) -> (ok: bool, details: str)
+ # Runs the real check. May read/write `context` to stash data
+ # needed later (e.g. the correct IP, so auto_fix_fn can use it).
+
+ "manual_check_instructions_fn": fn(context) -> str
+ # Commands/steps the user can run themselves to check this.
+
+ "auto_fix_fn": fn(context) -> (status: str, details: str)
+ # Applies the fix. If the fix itself already restarts whatever
+ # needs restarting (recommended - keeps status accurate), just
+ # report that status here.
+
+ "manual_fix_instructions_fn": fn(context) -> str
+ # Steps the user can follow themselves to apply the fix.
+
+ "restart_fn": fn(context) -> str, # OPTIONAL
+ # Called after the user confirms they made a manual fix, since
+ # manual fixes don't restart anything themselves. Should
+ # restart whatever's needed and return the resulting status
+ # string. If omitted, no restart happens after a manual fix.
+ }
+
+USAGE
+-----
+ from utils.step_flow import stage_names, start_flow, run_step_flow
+
+ MY_STEPS = [ {...}, {...} ]
+ PREFIX = "myflow"
+ MY_STAGES = stage_names(PREFIX, MY_STEPS) # for routing in your use case
+
+ def my_flow(user_choice=None, context=None):
+ ...
+ if context.get("stage") == "my_entry_stage":
+ return start_flow(PREFIX, MY_STEPS, context)
+
+ if context.get("stage") in MY_STAGES:
+ return run_step_flow(PREFIX, MY_STEPS, "next_stage_name",
+ user_choice=user_choice, context=context)
+"""
+
+from utils.response_utils import make_response
+from utils.unresolved_help import conclude
+
+
+def stage_names(prefix, steps):
+ """
+ All stage names this engine will use for a given prefix + step list.
+ Use this to build the routing set in the calling use-case flow, e.g.:
+
+ SEQ_STAGES = stage_names("seq", STEPS)
+ if context.get("stage") in SEQ_STAGES:
+ return run_step_flow("seq", STEPS, "fetch_logs", ...)
+ """
+ names = set()
+ for s in steps:
+ key = s["key"]
+ names.update({
+ f"{prefix}_{key}_permission",
+ f"{prefix}_{key}_manual_check",
+ f"{prefix}_{key}_fix_permission",
+ f"{prefix}_{key}_manual_fix_wait",
+ f"{prefix}_{key}_fix_result",
+ })
+ return names
+
+
+def start_flow(prefix, steps, context):
+ """
+ Kick off the flow at its first step. Call this once, from the calling
+ use-case's own entry point, to begin the sequence.
+ """
+ return _start_step(prefix, steps, steps[0]["key"], context)
+
+
+def run_step_flow(prefix, steps, next_stage_after_ongoing, user_choice=None, context=None):
+ """
+ Advance the flow by one turn based on context["stage"] and the user's
+ answer. Returns a response built with make_response().
+ """
+ if context is None:
+ context = {}
+
+ choice = (user_choice or "").lower().strip()
+ stage = context.get("stage")
+
+ for step in steps:
+ key = step["key"]
+ title = step["title"]
+
+ # -----------------------------------------------------------
+ # "Should I check the ? (yes / manual)"
+ # -----------------------------------------------------------
+ if stage == f"{prefix}_{key}_permission":
+
+ if "manual" in choice:
+ context["stage"] = f"{prefix}_{key}_manual_check"
+ instructions = step["manual_check_instructions_fn"](context)
+ return make_response(
+ display=(
+ instructions
+ + "\n\nOnce you've checked, let me know: good to go, or incorrect?"
+ ),
+ ask=["Good to go, or incorrect? (good to go / incorrect)"],
+ context=context,
+ )
+
+ # "yes" (default) -> run the real check ourselves
+ ok, details = step["check_fn"](context)
+ header = f"Checking the {title}.\n\n{details}"
+
+ if ok:
+ return _advance_after_pass(prefix, steps, key, header, context, next_stage_after_ongoing)
+
+ context["stage"] = f"{prefix}_{key}_fix_permission"
+ return make_response(
+ display=(
+ header + "\n\n[ISSUE] This doesn't look right.\n\n"
+ "Do you want me to fix this? (yes / manually)"
+ ),
+ ask=["Fix this? (yes / manually)"],
+ context=context,
+ )
+
+ # -----------------------------------------------------------
+ # "Good to go, or incorrect?" (after manual check instructions)
+ # -----------------------------------------------------------
+ if stage == f"{prefix}_{key}_manual_check":
+ if "incorrect" in choice:
+ context["stage"] = f"{prefix}_{key}_fix_permission"
+ return make_response(
+ display="Do you want me to fix this? (yes / manually)",
+ ask=["Fix this? (yes / manually)"],
+ context=context,
+ )
+
+ # "good to go"
+ return _advance_after_pass(prefix, steps, key, "", context, next_stage_after_ongoing)
+
+ # -----------------------------------------------------------
+ # "Do you want me to fix this? (yes / manually)"
+ # -----------------------------------------------------------
+ if stage == f"{prefix}_{key}_fix_permission":
+ if "manual" in choice:
+ context["stage"] = f"{prefix}_{key}_manual_fix_wait"
+ instructions = step["manual_fix_instructions_fn"](context)
+ return make_response(
+ display=instructions + "\n\nLet me know once you've made the change.",
+ ask=["Done making the change? (done)"],
+ context=context,
+ )
+
+ # "yes" -> apply the fix ourselves
+ status, details = step["auto_fix_fn"](context)
+ context["stage"] = f"{prefix}_{key}_fix_result"
+ return make_response(
+ display=details + "\n\nIs the issue fixed now, or still ongoing?",
+ ask=["Fixed or ongoing? (fixed / ongoing)"],
+ context=context,
+ )
+
+ # -----------------------------------------------------------
+ # User confirmed they made the manual fix -> restart if configured
+ # -----------------------------------------------------------
+ if stage == f"{prefix}_{key}_manual_fix_wait":
+ restart_fn = step.get("restart_fn")
+ if restart_fn:
+ status = restart_fn(context)
+ restart_msg = f"Restarted (status: {status.upper()}).\n\n"
+ else:
+ restart_msg = ""
+
+ context["stage"] = f"{prefix}_{key}_fix_result"
+ return make_response(
+ display=restart_msg + "Is the issue fixed now, or still ongoing?",
+ ask=["Fixed or ongoing? (fixed / ongoing)"],
+ context=context,
+ )
+
+ # -----------------------------------------------------------
+ # "Fixed or ongoing?"
+ # -----------------------------------------------------------
+ if stage == f"{prefix}_{key}_fix_result":
+ if "fixed" in choice:
+ return conclude(resolved=True, display="Great! The issue is resolved.", context=context)
+
+ # "ongoing" -> move to the next step
+ return _advance(prefix, steps, key, "Understood, still ongoing.", context, next_stage_after_ongoing)
+
+ return make_response(display="Unexpected step.", done=True, context=context)
+
+
+# ---------------------------------------------------------------------------
+# internal helpers
+# ---------------------------------------------------------------------------
+def _step_by_key(steps, key):
+ for s in steps:
+ if s["key"] == key:
+ return s
+ return None
+
+
+def _next_step_key(steps, key):
+ idx = [s["key"] for s in steps].index(key)
+ return steps[idx + 1]["key"] if idx + 1 < len(steps) else None
+
+
+def _start_step(prefix, steps, key, context):
+ step = _step_by_key(steps, key)
+ context["stage"] = f"{prefix}_{key}_permission"
+ return make_response(
+ display=f"Should I check the {step['title']}? (yes / manual)",
+ ask=[f"Check the {step['title']}? (yes / manual)"],
+ context=context,
+ )
+
+
+def _advance_after_pass(prefix, steps, key, header, context, next_stage_after_ongoing):
+ """A check just passed (auto or self-reported) - move to the next step,
+ or hand off to the caller's next stage if this was the last step."""
+ nxt = _next_step_key(steps, key)
+ if nxt:
+ nxt_resp = _start_step(prefix, steps, nxt, context)
+ prefix_msg = (header + "\n\n[OK] This looks correct.\n\n") if header else ""
+ nxt_resp["display"] = prefix_msg + nxt_resp["display"]
+ return nxt_resp
+
+ # last step passed cleanly -> hand off to the caller's next stage
+ # (e.g. the dashboard IP/cert flow), instead of just stopping here.
+ tail = (header + "\n\n[OK] This looks correct. ") if header else ""
+ context["stage"] = next_stage_after_ongoing
+ return make_response(
+ display=tail + "Everything checks out here! Let's move on to the next step.",
+ context=context,
+ handoff=True,
+ )
+
+
+def _advance(prefix, steps, key, prefix_msg, context, next_stage_after_ongoing):
+ """A fix just happened and the issue is still ongoing - move to the next step."""
+ nxt = _next_step_key(steps, key)
+ if nxt:
+ nxt_resp = _start_step(prefix, steps, nxt, context)
+ nxt_resp["display"] = prefix_msg + "\n\n" + nxt_resp["display"]
+ return nxt_resp
+
+ # last step still ongoing -> hand off to the caller's next stage
+ context["stage"] = next_stage_after_ongoing
+ return make_response(
+ display=prefix_msg + " Let's move on to the next step.",
+ context=context,
+ handoff=True,
+ )
diff --git a/integrations/wazuh-troubleshooting-tool/backend/utils/unresolved_help.py b/integrations/wazuh-troubleshooting-tool/backend/utils/unresolved_help.py
new file mode 100644
index 00000000..adcdcf60
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/utils/unresolved_help.py
@@ -0,0 +1,69 @@
+"""
+Shared "end of a troubleshooting flow" helper.
+
+Every use-case flow eventually reaches a point where it either confirms
+the user's problem is actually fixed, or runs out of automated steps
+without confirming that. Call conclude() at that point instead of
+building the final make_response(..., done=True) by hand, so both cases
+are handled the same way everywhere: a clean success message on
+resolution, or a best-effort suggestion (pulled from the local knowledge
+base and a live public GitHub search) plus a pointer to the Wazuh
+community when the automated steps didn't fix it.
+"""
+
+from utils.response_utils import make_response
+from utils.lgtm_utils import find_relevant_issues, format_lgtm_context
+from utils.public_repo_search import search_public_issues, search_public_discussions, format_public_context
+
+COMMUNITY_POINTER = (
+ "\n\nIf this didn't resolve your issue, you can also check the Wazuh community "
+ "for similar reports and discussions: https://github.com/wazuh/wazuh/issues and "
+ "https://github.com/wazuh/wazuh/discussions"
+)
+
+
+def _best_effort_suggestion(topic):
+ """
+ Look up related known issues (local knowledge base + live public GitHub
+ search) for `topic` and format them into a short suggestion block.
+ Never raises - a lookup failure just means no suggestion gets added.
+ """
+ try:
+ lgtm_context = format_lgtm_context(find_relevant_issues(topic))
+ except Exception:
+ lgtm_context = ""
+
+ try:
+ public_context = format_public_context(
+ search_public_issues(topic), search_public_discussions(topic)
+ )
+ except Exception:
+ public_context = ""
+
+ parts = [p for p in (lgtm_context, public_context) if p]
+ if not parts:
+ return ""
+ return "\n\nHere's what I found from similar reports:\n\n" + "\n\n".join(parts)
+
+
+def conclude(resolved, display, context, topic=None):
+ """
+ End a troubleshooting flow.
+
+ resolved : bool - True only if a check actually confirmed the issue
+ is gone, not just "we ran out of steps to try".
+ display : str - the message built so far this turn.
+ context : dict - the flow's context, carried forward.
+ topic : str - short description of the problem (e.g. "wazuh
+ dashboard not loading"), used to search for related known
+ issues when unresolved. Required when resolved is False.
+ """
+ if resolved:
+ return make_response(display=display, done=True, context=context)
+
+ suggestion = _best_effort_suggestion(topic or "")
+ return make_response(
+ display=display + suggestion + COMMUNITY_POINTER,
+ done=True,
+ context=context,
+ )
diff --git a/integrations/wazuh-troubleshooting-tool/backend/utils/wazuh_docs.py b/integrations/wazuh-troubleshooting-tool/backend/utils/wazuh_docs.py
new file mode 100644
index 00000000..5ffc90f9
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/utils/wazuh_docs.py
@@ -0,0 +1,80 @@
+"""
+Curated, human-verified Wazuh documentation URLs, fetched live and fed into
+the copilot's context - this exists because letting a small local LLM recall
+exact doc URLs from memory produces confident-looking but wrong/404 links.
+Every URL in KNOWN_DOCS must be a real page a human actually checked; the
+model is never asked to invent one.
+
+To add a new one: verify the URL actually loads (not a 404), add an entry
+below with a few keywords that should trigger it, done.
+"""
+import re
+import requests
+
+KNOWN_DOCS = {
+ "agent_install_linux": {
+ "keywords": ["install agent", "add agent", "add an agent", "deploy agent", "agent installation", "install wazuh-agent"],
+ "url": "https://documentation.wazuh.com/current/installation-guide/wazuh-agent/wazuh-agent-package-linux.html",
+ "title": "Wazuh Agent installation on Linux",
+ },
+ "cloud_trial": {
+ "keywords": ["cloud trial", "cloud sign up", "cloud signup", "trial credentials", "wazuh cloud login"],
+ "url": "https://documentation.wazuh.com/current/cloud-service/getting-started/sign-up-trial.html",
+ "title": "Wazuh Cloud trial sign-up",
+ },
+}
+
+_cache = {}
+
+
+def find_matching_doc(query: str):
+ """Return the first KNOWN_DOCS entry whose keywords appear in the query, or None."""
+ q = query.lower()
+ for key, doc in KNOWN_DOCS.items():
+ if any(kw in q for kw in doc["keywords"]):
+ return key, doc
+ return None, None
+
+
+def fetch_doc_content(url: str) -> str:
+ """Fetch and clean a doc page's text. Cached in-memory so repeat questions
+ on the same topic don't re-fetch every time."""
+ if url in _cache:
+ return _cache[url]
+ try:
+ resp = requests.get(url, timeout=10)
+ if resp.status_code != 200:
+ return ""
+ html = resp.text
+ # Wazuh's doc site wraps real content in ...; without
+ # this, stripping tags on the full page grabs the sidebar table-of-
+ # contents (hundreds of unrelated menu links) instead of the article.
+ main_match = re.search(r"]*>(.*?)", html, flags=re.DOTALL)
+ html = main_match.group(1) if main_match else html
+ clean_text = re.sub(r"", " ", html, flags=re.DOTALL)
+ clean_text = re.sub(r"", " ", clean_text, flags=re.DOTALL)
+ clean_text = re.sub(r"<[^>]+>", " ", clean_text)
+ clean_text = re.sub(r"\s+", " ", clean_text).strip()
+ content = clean_text[:6000]
+ _cache[url] = content
+ return content
+ except requests.exceptions.RequestException:
+ return ""
+
+
+def format_doc_context(query: str) -> str:
+ """If the query matches a known topic, return grounded context with the
+ verified URL and an explicit instruction to cite only that exact URL."""
+ key, doc = find_matching_doc(query)
+ if not doc:
+ return ""
+ content = fetch_doc_content(doc["url"])
+ if not content:
+ return ""
+ return (
+ f"=== Official Wazuh documentation: {doc['title']} ===\n"
+ f"{content}\n\n"
+ f"Instructions: if you reference documentation for this topic, cite exactly this URL "
+ f"and no other: {doc['url']} - do not modify it or invent a different path.\n"
+ f"================================================="
+ )
diff --git a/integrations/wazuh-troubleshooting-tool/backend/utils/wizard_history.py b/integrations/wazuh-troubleshooting-tool/backend/utils/wizard_history.py
new file mode 100644
index 00000000..4209af2b
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/utils/wizard_history.py
@@ -0,0 +1,50 @@
+"""
+Download-only history for the Troubleshooting Library's guided wizard flows.
+Unlike session_store.py (Wazuh Copilot chat), these are never resumed/continued
+- only saved once a flow reaches a resolution (done: true) and made available
+to download/review. Same compressed-storage mechanism, capped at 6, oldest
+evicted automatically. See utils/compressed_history.py.
+
+Layout:
+ backend/wizard_history/manifest.json - {run_id: {title, started_at, updated_at}}
+ backend/wizard_history/.json.gz - gzip-compressed JSON transcript
+"""
+import os
+
+from utils.compressed_history import CompressedHistoryStore
+
+MAX_RUNS = 6
+
+_WIZARD_DIR = os.path.join(os.path.dirname(__file__), "..", "wizard_history")
+_store = CompressedHistoryStore(_WIZARD_DIR, max_items=MAX_RUNS)
+
+
+def _derive_title(transcript):
+ if transcript and transcript[0].get("user"):
+ text = transcript[0]["user"].strip().replace("\n", " ")
+ return text[:60] + ("..." if len(text) > 60 else "")
+ return "Troubleshooting session"
+
+
+def save_run(run_id, transcript):
+ _store.save(run_id, transcript, title=_derive_title(transcript))
+
+
+def load_run(run_id):
+ return _store.load(run_id)
+
+
+def list_runs():
+ return [{"run_id": e["id"], **{k: v for k, v in e.items() if k != "id"}} for e in _store.list()]
+
+
+def format_transcript_text(transcript, title):
+ """Plain-text rendering for download - readable outside the app."""
+ lines = [f"Wazuh Troubleshooting Library — {title}", "=" * 60, ""]
+ for step in transcript:
+ if step.get("user"):
+ lines.append(f"> {step['user']}")
+ if step.get("assistant"):
+ lines.append(step["assistant"])
+ lines.append("")
+ return "\n".join(lines)
diff --git a/integrations/wazuh-troubleshooting-tool/backend/wazuh_api.py b/integrations/wazuh-troubleshooting-tool/backend/wazuh_api.py
new file mode 100644
index 00000000..83578eba
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/backend/wazuh_api.py
@@ -0,0 +1,40 @@
+import requests
+from config import WAZUH_API_URL, API_USERNAME, API_PASSWORD
+
+requests.packages.urllib3.disable_warnings()
+
+def get_token():
+ try:
+ url = f"{WAZUH_API_URL}/security/user/authenticate?raw=true"
+
+ res = requests.post(
+ url,
+ auth=(API_USERNAME, API_PASSWORD),
+ verify=False,
+ timeout=5,
+ )
+
+ return res.text.strip()
+ except requests.RequestException:
+ return None
+
+
+def check_api():
+ token = get_token()
+
+ if not token:
+ return "API AUTH FAILED"
+
+ try:
+ headers = {"Authorization": f"Bearer {token}"}
+
+ res = requests.get(
+ f"{WAZUH_API_URL}/",
+ headers=headers,
+ verify=False,
+ timeout=5,
+ )
+
+ return res.text
+ except requests.RequestException:
+ return "API CONNECTION FAILED"
diff --git a/integrations/wazuh-troubleshooting-tool/config b/integrations/wazuh-troubleshooting-tool/config
new file mode 100644
index 00000000..3e413eea
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/config
@@ -0,0 +1,29 @@
+# Replace the placeholder credentials below according to your own environment.
+
+wazuh_api:
+ host: "https://localhost:55000"
+ username: "wazuh"
+ password: "YOUR_WAZUH_PASSWORD"
+ verify_ssl: false
+
+indexer:
+ url: "https://localhost:9200"
+ username: "admin"
+ password: "YOUR_INDEXER_PASSWORD"
+
+kibana:
+ username: "kibanaserver"
+ password: "YOUR_KIBANA_PASSWORD"
+
+ollama:
+ url: "http://localhost:11434"
+ model: "qwen3:1.7b"
+
+anthropic:
+ api_key: ""
+ model: "claude-sonnet-5"
+
+server:
+ host: "localhost"
+ backend_port: "8000"
+ frontend_port: "3000"
diff --git a/integrations/wazuh-troubleshooting-tool/frontend/agent.js b/integrations/wazuh-troubleshooting-tool/frontend/agent.js
new file mode 100644
index 00000000..abde5487
--- /dev/null
+++ b/integrations/wazuh-troubleshooting-tool/frontend/agent.js
@@ -0,0 +1,562 @@
+/* agent.js
+ * Wazuh Copilot — the single unified AI assistant, backed by the agentic
+ * tool-calling loop (agent_engine.py). Handles freeform Q&A (falls straight
+ * to a text answer when it has nothing to call) and full investigate/fix
+ * flows with an approval gate on anything that changes system state.
+ * Talks to /agent/message, /agent/approve, /agent/reset, /agent/tools, /agent/brains.
+ * Plain window.* globals, no framework, BASE_URL resolved by app.js's loadConfig().
+ */
+
+const AgentState = {
+ sessionId: null,
+ brain: "ollama",
+ ollamaModels: [],
+ model: null,
+ sending: false,
+ initialized: false,
+};
+
+function escapeHtml(s) {
+ return String(s).replace(/[&<>"']/g, c => ({
+ "&": "&", "<": "<", ">": ">", '"': """, "'": "'",
+ }[c]));
+}
+
+function agentAvatarSvg() {
+ return '';
+}
+
+// ─────────────────────────────────────────────────────────────────────────────
+// MARKDOWN RENDERER (lightweight, no external deps) — ported from copilot.js
+// ─────────────────────────────────────────────────────────────────────────────
+
+function renderMarkdown(text) {
+ // Escape the raw text FIRST, then apply markdown formatting on the
+ // escaped string — otherwise a compromised backend response or a
+ // prompt-injected tool result could inject live HTML/JS via innerHTML.
+ // None of the regexes below match &<>"', so escaping first doesn't
+ // change how any of them match.
+ text = escapeHtml(text);
+
+ text = text.replace(/```(\w*)\n?([\s\S]*?)```/g, (_, lang, code) => {
+ const langLabel = lang ? `${lang}` : "";
+ return `
+
${langLabel}
+
+
+
${code.trim()}
+
`;
+ });
+
+ text = text.replace(/`([^`]+)`/g, '$1');
+ text = text.replace(/\*\*(.+?)\*\*/g, "$1");
+ text = text.replace(/(?$1");
+ text = text.replace(/^### (.+)$/gm, '
$1
');
+ text = text.replace(/^## (.+)$/gm, '
$1
');
+ text = text.replace(/^# (.+)$/gm, '
$1
');
+ text = text.replace(/^─{3,}$/gm, '');
+ text = text.replace(/^-{3,}$/gm, '');
+ text = text.replace(/^[•\-\*] (.+)$/gm, '
$1
');
+ text = text.replace(/(
[\s\S]*?<\/li>)/g, '
$1
');
+ text = text.replace(/<\/ul>\s*
/g, "");
+ text = text.replace(/^\d+\. (.+)$/gm, '
Describe what's wrong ` +
+ `(e.g. "no alerts are showing on the dashboard") and I'll investigate — checking services, ` +
+ `logs, cluster health and configuration — before proposing any fix. I'll always show you the ` +
+ `exact action and ask before restarting a service or changing anything.
For when no alerts are showing at all, or today's wazuh-alerts-* index isn't being created - walks the full pipeline from the Wazuh Manager through Filebeat to the Indexer.
+
+
+
+
+
+
+
+
Alerts Not Indexing
+
Diagnose active write blocks, check disk space watermark settings, and inspect cluster shard state.
+
+
+
+
+
+
+
+
Could Not Connect to API
+
Resolve credentials mismatch, incorrect API username/password keys in configuration, and listener errors.
Troubleshoot multi-node setups, indexer cluster health status red/yellow, and unassigned replica shards.
+
+
+
+
+
+
+
+
Filebeat Not Working
+
Having an issue in `filebeat test output` - diagnose service status, unsupported Filebeat versions, and TLS/certificate errors.
+
+
+
+
+
+
+
+
Filebeat Mapping Issue
+
Diagnose field-mapping/index-template conflicts - illegal_argument_exception, mapper_parsing_exception, or dashboard "N of M shards failed" errors.
+
+
+
+
+
+
+
+
+
+ Library Diagnostics Wizard
+
+
+
+
+
+
+
+
+
+ Welcome to the Wazuh Library Diagnostics Wizard. Select a known Wazuh scenario card above to start interactive diagnostics, or ask any troubleshooting question below for AI-powered assistance.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Operations Reporting Center
+
Generate environment health reports, deployment insights, and Wazuh operational intelligence metrics
+
+
+
+
+
+
+
+
+
+
+
Agent Health Report
+
Review connected fleet status, disconnected agents, communication issues, and activity summaries.
+
+
+
+
+
+
+
+
+
Dashboard Health Report
+
Analyze availability, uptime, connection performance, API endpoints, and configuration errors.