# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Email: Georgescottfoley@proton.me
# Licensed under the MIT License - see LICENSE file for details

"""
Wazuh SIEM Skill

Query alerts, agents, SCA compliance, and vulnerability data from a local Wazuh deployment.

Setup:
    WAZUH_HOST=https://localhost:55000     # Wazuh Manager API
    WAZUH_USER=wazuh-wui                   # API user (default from Wazuh install)
    WAZUH_PASSWORD=your-password
    WAZUH_INDEXER=https://localhost:9200   # OpenSearch indexer (for alerts)
    WAZUH_INDEXER_USER=admin
    WAZUH_INDEXER_PASSWORD=SecretPassword
    WAZUH_VERIFY_SSL=false                 # Disable SSL verification for self-signed certs
"""

import json
import logging
import os
from datetime import datetime, timezone

import httpx

from familiar.core.utils import format_http_error

logger = logging.getLogger(__name__)

REQUEST_TIMEOUT = 30
_jwt_cache: dict = {}  # {"token": str, "expires": float}


# ---------------------------------------------------------------------------
# Configuration helpers
# ---------------------------------------------------------------------------


def _get_config() -> dict:
    return {
        "host": os.environ.get("WAZUH_HOST", "https://localhost:55000").rstrip("/"),
        "user": os.environ.get("WAZUH_USER", "wazuh-wui"),
        "password": os.environ.get("WAZUH_PASSWORD", ""),
        "indexer": os.environ.get("WAZUH_INDEXER", "https://localhost:9200").rstrip("/"),
        "indexer_user": os.environ.get("WAZUH_INDEXER_USER", "admin"),
        "indexer_password": os.environ.get("WAZUH_INDEXER_PASSWORD", "SecretPassword"),
        "verify_ssl": os.environ.get("WAZUH_VERIFY_SSL", "false").lower() != "false",
    }


def _wazuh_available() -> bool:
    cfg = _get_config()
    return bool(cfg["host"] and cfg["password"])


def _get_jwt_token() -> str | None:
    """Obtain (or return cached) Wazuh JWT token."""
    import time

    global _jwt_cache
    now = time.time()
    if _jwt_cache.get("token") and _jwt_cache.get("expires", 0) > now + 30:
        return _jwt_cache["token"]

    cfg = _get_config()
    try:
        resp = httpx.post(
            f"{cfg['host']}/security/user/authenticate",
            auth=(cfg["user"], cfg["password"]),
            verify=cfg["verify_ssl"],
            timeout=REQUEST_TIMEOUT,
        )
        if resp.status_code == 200:
            data = resp.json()
            token = data.get("data", {}).get("token", "")
            if token:
                _jwt_cache = {"token": token, "expires": now + 890}  # 15 min tokens
                return token
        logger.warning("Wazuh auth failed: %s", resp.status_code)
        return None
    except Exception as e:
        logger.warning("Wazuh auth error: %s", e)
        return None


def _api_get(path: str, params: dict | None = None) -> httpx.Response:
    cfg = _get_config()
    token = _get_jwt_token()
    headers = {"Authorization": f"Bearer {token}"} if token else {}
    return httpx.get(
        f"{cfg['host']}{path}",
        headers=headers,
        params=params or {},
        verify=cfg["verify_ssl"],
        timeout=REQUEST_TIMEOUT,
    )


def _indexer_search(index: str, body: dict) -> dict:
    cfg = _get_config()
    try:
        resp = httpx.post(
            f"{cfg['indexer']}/{index}/_search",
            auth=(cfg["indexer_user"], cfg["indexer_password"]),
            json=body,
            verify=cfg["verify_ssl"],
            timeout=REQUEST_TIMEOUT,
        )
        if resp.status_code == 200:
            return resp.json()
        logger.warning("Indexer search failed: %s", resp.status_code)
        return {}
    except Exception as e:
        logger.warning("Indexer search error: %s", e)
        return {}


# ---------------------------------------------------------------------------
# Tool handlers
# ---------------------------------------------------------------------------


def get_wazuh_alerts(data: dict) -> str:
    """Retrieve recent security alerts from the Wazuh indexer."""
    if not _wazuh_available():
        return (
            "Wazuh not configured. Set WAZUH_HOST, WAZUH_USER, and WAZUH_PASSWORD, "
            "or provision the Wazuh stack via the Server tab."
        )

    severity = data.get("severity", "").lower()
    limit = min(int(data.get("limit", 20)), 100)

    # Map severity label to Wazuh rule.level ranges
    level_filter: dict = {}
    if severity == "critical":
        level_filter = {"gte": 13}
    elif severity == "high":
        level_filter = {"gte": 10, "lte": 12}
    elif severity == "medium":
        level_filter = {"gte": 7, "lte": 9}
    elif severity == "low":
        level_filter = {"gte": 1, "lte": 6}

    must_clauses: list = [{"match_all": {}}]
    if level_filter:
        must_clauses = [{"range": {"rule.level": level_filter}}]

    body = {
        "size": limit,
        "sort": [{"timestamp": {"order": "desc"}}],
        "query": {"bool": {"must": must_clauses}},
        "_source": [
            "timestamp",
            "agent.name",
            "agent.id",
            "rule.level",
            "rule.description",
            "rule.id",
            "rule.groups",
            "data.srcip",
            "full_log",
        ],
    }

    result = _indexer_search("wazuh-alerts-*", body)
    hits = result.get("hits", {}).get("hits", [])

    if not hits:
        label = f" {severity}" if severity else ""
        return f"No{label} alerts found in the Wazuh indexer."

    def _severity_label(level: int) -> str:
        if level >= 13:
            return "CRITICAL"
        if level >= 10:
            return "HIGH"
        if level >= 7:
            return "MEDIUM"
        return "LOW"

    lines = [f"Wazuh alerts ({len(hits)} shown):"]
    for hit in hits:
        src = hit.get("_source", {})
        ts = src.get("timestamp", "")[:19]
        agent = src.get("agent", {}).get("name", "unknown")
        level = src.get("rule", {}).get("level", 0)
        desc = src.get("rule", {}).get("description", "")
        sev = _severity_label(level)
        lines.append(f"  [{sev:8}] {ts}  {agent:<20}  {desc}")

    return "\n".join(lines)


def get_wazuh_agents(data: dict) -> str:
    """List enrolled Wazuh agents and their connection status."""
    if not _wazuh_available():
        return "Wazuh not configured. Set WAZUH_HOST, WAZUH_USER, and WAZUH_PASSWORD."

    try:
        resp = _api_get("/agents", params={"limit": 100, "sort": "name"})
        if resp.status_code != 200:
            return format_http_error("Wazuh", status_code=resp.status_code)

        result = resp.json()
        agents = result.get("data", {}).get("affected_items", [])

        if not agents:
            return "No Wazuh agents found. The manager itself counts as agent 000."

        active = [a for a in agents if a.get("status") == "active"]
        disconnected = [a for a in agents if a.get("status") == "disconnected"]
        never = [a for a in agents if a.get("status") == "never_connected"]

        lines = [
            f"Wazuh agents: {len(agents)} total  "
            f"({len(active)} active, {len(disconnected)} disconnected, {len(never)} never connected)",
            "",
        ]

        for agent in agents:
            aid = agent.get("id", "???")
            name = agent.get("name", "unknown")
            status = agent.get("status", "unknown")
            ip = agent.get("ip", "-")
            version = agent.get("version", "-")
            last_keepalive = (agent.get("lastKeepAlive") or "")[:10]

            icon = "●" if status == "active" else "○"
            lines.append(
                f"  {icon} [{aid}] {name:<24} {status:<15} {ip:<16} "
                f"v{version:<10} last: {last_keepalive}"
            )

        return "\n".join(lines)

    except httpx.HTTPError as e:
        return format_http_error("Wazuh", exception=e)


def get_vulnerability_summary(data: dict) -> str:
    """Get a CVE vulnerability summary across all agents, grouped by severity."""
    if not _wazuh_available():
        return "Wazuh not configured. Set WAZUH_HOST, WAZUH_USER, and WAZUH_PASSWORD."

    try:
        # Get list of active agents first
        resp = _api_get("/agents", params={"status": "active", "limit": 100})
        if resp.status_code != 200:
            return format_http_error("Wazuh", status_code=resp.status_code)

        agents = resp.json().get("data", {}).get("affected_items", [])
        if not agents:
            return "No active agents to check vulnerabilities on."

        totals: dict[str, int] = {"critical": 0, "high": 0, "medium": 0, "low": 0, "none": 0}
        agent_summary = []

        for agent in agents[:20]:  # cap at 20 to avoid slow queries
            aid = agent.get("id", "000")
            name = agent.get("name", aid)
            if aid == "000":
                continue  # skip manager itself

            vuln_resp = _api_get(
                f"/vulnerability/{aid}/summary/severity",
                params={"limit": 1},
            )
            if vuln_resp.status_code == 200:
                counts = vuln_resp.json().get("data", {}).get("affected_items", [{}])[0]
                for sev in ("critical", "high", "medium", "low", "none"):
                    n = counts.get(sev, 0)
                    totals[sev] += n
                agent_summary.append(
                    (
                        name,
                        counts.get("critical", 0),
                        counts.get("high", 0),
                        counts.get("medium", 0),
                        counts.get("low", 0),
                    )
                )

        total_open = sum(totals[s] for s in ("critical", "high", "medium", "low"))
        lines = [
            "Vulnerability Summary (across active agents):",
            f"  Total open CVEs: {total_open}",
            f"  Critical: {totals['critical']}  High: {totals['high']}  "
            f"Medium: {totals['medium']}  Low: {totals['low']}",
            "",
        ]

        if agent_summary:
            lines.append("By agent:")
            for name, crit, high, med, low in sorted(
                agent_summary, key=lambda x: x[1], reverse=True
            )[:10]:
                lines.append(f"  {name:<24}  C:{crit}  H:{high}  M:{med}  L:{low}")

        return "\n".join(lines)

    except httpx.HTTPError as e:
        return format_http_error("Wazuh", exception=e)


def get_compliance_posture(data: dict) -> str:
    """Get CIS or NIST compliance posture from Wazuh SCA results."""
    if not _wazuh_available():
        return "Wazuh not configured. Set WAZUH_HOST, WAZUH_USER, and WAZUH_PASSWORD."

    framework = data.get("framework", "cis").lower()

    try:
        # Get active agents
        resp = _api_get("/agents", params={"status": "active", "limit": 50})
        if resp.status_code != 200:
            return format_http_error("Wazuh", status_code=resp.status_code)

        agents = resp.json().get("data", {}).get("affected_items", [])
        if not agents:
            return "No active agents to check compliance for."

        lines = [f"Compliance posture — {framework.upper()} (SCA results):"]
        grand_pass = grand_fail = grand_na = 0

        for agent in agents[:10]:
            aid = agent.get("id", "000")
            name = agent.get("name", aid)
            if aid == "000":
                continue

            sca_resp = _api_get(f"/sca/{aid}", params={"limit": 20})
            if sca_resp.status_code != 200:
                continue

            policies = sca_resp.json().get("data", {}).get("affected_items", [])
            for policy in policies:
                policy_name = policy.get("name", "")
                if framework not in policy_name.lower():
                    continue

                passed = policy.get("pass", 0)
                failed = policy.get("fail", 0)
                na = policy.get("invalid", 0)
                score = policy.get("score", 0)
                grand_pass += passed
                grand_fail += failed
                grand_na += na
                lines.append(
                    f"\n  {name} — {policy_name}",
                )
                lines.append(f"    Pass: {passed}  Fail: {failed}  N/A: {na}  Score: {score}%")

        if grand_pass + grand_fail > 0:
            overall = round(grand_pass / (grand_pass + grand_fail) * 100, 1)
            lines.append(
                f"\nOverall: {grand_pass} passed / {grand_fail} failed ({overall}% compliant)"
            )
        else:
            lines.append(
                f"\nNo {framework.upper()} SCA policies found. "
                "Ensure the SCA module is enabled in Wazuh and policies have been scanned."
            )

        return "\n".join(lines)

    except httpx.HTTPError as e:
        return format_http_error("Wazuh", exception=e)


def get_attack_patterns(data: dict) -> str:
    """
    Correlate Wazuh alerts against MITRE ATT&CK framework.

    Groups recent alerts by tactic and technique, identifies source IPs active
    across multiple tactics (multi-stage attack indicators), and surfaces the
    kill chain progression to help prioritize investigation.
    """
    if not _wazuh_available():
        return "Wazuh not configured. Set WAZUH_HOST, WAZUH_USER, and WAZUH_PASSWORD."

    hours = max(1, min(int(data.get("hours", 24)), 168))  # 1h–7d window
    min_alerts = int(data.get("min_alerts", 2))  # minimum hits to include a pattern

    body = {
        "size": 0,
        "query": {
            "bool": {
                "must": [
                    {"range": {"timestamp": {"gte": f"now-{hours}h"}}},
                    {"exists": {"field": "rule.mitre.tactic"}},
                ],
            }
        },
        "aggs": {
            # Top MITRE tactics seen
            "by_tactic": {
                "terms": {"field": "rule.mitre.tactic", "size": 20},
                "aggs": {
                    # Techniques within each tactic
                    "by_technique": {"terms": {"field": "rule.mitre.technique", "size": 10}},
                    # Source IPs per tactic
                    "by_src_ip": {"terms": {"field": "data.srcip", "size": 5}},
                    # Agents affected per tactic
                    "by_agent": {"terms": {"field": "agent.name", "size": 5}},
                },
            },
            # Source IPs appearing across multiple tactics (campaign indicator)
            "attackers": {
                "terms": {"field": "data.srcip", "size": 20, "min_doc_count": min_alerts},
                "aggs": {
                    "tactics_seen": {"terms": {"field": "rule.mitre.tactic", "size": 10}},
                    "agents_hit": {"terms": {"field": "agent.name", "size": 5}},
                    "first_seen": {"min": {"field": "timestamp"}},
                    "last_seen": {"max": {"field": "timestamp"}},
                },
            },
        },
    }

    result = _indexer_search("wazuh-alerts-*", body)
    if not result:
        return "Could not query Wazuh indexer. Check WAZUH_INDEXER credentials."

    aggs = result.get("aggregations", {})
    total_hits = result.get("hits", {}).get("total", {}).get("value", 0)

    if total_hits == 0:
        return (
            f"No MITRE-mapped alerts found in the last {hours}h. "
            "Either your environment is clean or Wazuh rules lack MITRE mappings."
        )

    # ── Tactic / Technique breakdown ────────────────────────────────────────────
    # MITRE ATT&CK kill chain order for sorting
    _TACTIC_ORDER = {
        "Reconnaissance": 0,
        "Resource Development": 1,
        "Initial Access": 2,
        "Execution": 3,
        "Persistence": 4,
        "Privilege Escalation": 5,
        "Defense Evasion": 6,
        "Credential Access": 7,
        "Discovery": 8,
        "Lateral Movement": 9,
        "Collection": 10,
        "Command and Control": 11,
        "Exfiltration": 12,
        "Impact": 13,
    }

    tactic_buckets = aggs.get("by_tactic", {}).get("buckets", [])
    tactic_buckets.sort(key=lambda b: _TACTIC_ORDER.get(b.get("key", ""), 99))

    lines = [
        f"MITRE ATT&CK Pattern Analysis — last {hours}h  ({total_hits} MITRE-mapped alerts)",
        "",
        "Attack Progression (kill chain order):",
        "─" * 60,
    ]

    stages_seen = []
    for bucket in tactic_buckets:
        tactic = bucket.get("key", "Unknown")
        count = bucket.get("doc_count", 0)
        stage_num = _TACTIC_ORDER.get(tactic, 99)
        stages_seen.append(stage_num)

        techniques = [
            f"{t['key']} ({t['doc_count']})"
            for t in bucket.get("by_technique", {}).get("buckets", [])[:5]
        ]
        agents = [a["key"] for a in bucket.get("by_agent", {}).get("buckets", [])[:3]]
        src_ips = [s["key"] for s in bucket.get("by_src_ip", {}).get("buckets", [])[:3]]

        lines.append(f"\n  [{stage_num:2}] {tactic}  ({count} alerts)")
        if techniques:
            lines.append(f"       Techniques: {', '.join(techniques)}")
        if agents:
            lines.append(f"       Agents hit: {', '.join(agents)}")
        if src_ips and src_ips[0] not in ("", "unknown"):
            lines.append(f"       Source IPs: {', '.join(src_ips)}")

    # Detect multi-stage progression (suspicious if 3+ distinct stages)
    if len(stages_seen) >= 3:
        lines.append(
            "\n⚠  Multi-stage attack pattern detected: "
            f"{len(stages_seen)} distinct tactics observed."
        )
        min_stage = _TACTIC_ORDER.get(
            next(
                (
                    b["key"]
                    for b in tactic_buckets
                    if _TACTIC_ORDER.get(b["key"], 99) == min(stages_seen)
                ),
                "",
            ),
            99,
        )
        max_stage = max(stages_seen)
        stage_names = {v: k for k, v in _TACTIC_ORDER.items()}
        lines.append(
            f"   Progression: {stage_names.get(min_stage, '?')} → {stage_names.get(max_stage, '?')}"
        )

    # ── Active Attackers (multi-tactic source IPs) ───────────────────────────────
    attacker_buckets = [
        b
        for b in aggs.get("attackers", {}).get("buckets", [])
        if b.get("key", "") not in ("", "unknown", "127.0.0.1")
    ]

    multi_stage_attackers = [
        b for b in attacker_buckets if len(b.get("tactics_seen", {}).get("buckets", [])) >= 2
    ]

    if attacker_buckets:
        lines.append("")
        lines.append("Active Threat Actors:")
        lines.append("─" * 60)

        for b in attacker_buckets[:10]:
            ip = b["key"]
            alert_count = b["doc_count"]
            tactics = [t["key"] for t in b.get("tactics_seen", {}).get("buckets", [])]
            agents_hit = [a["key"] for a in b.get("agents_hit", {}).get("buckets", [])]
            first_ts = (b.get("first_seen", {}).get("value_as_string") or "")[:16]
            last_ts = (b.get("last_seen", {}).get("value_as_string") or "")[:16]

            campaign_flag = (
                "🔴 CAMPAIGN"
                if len(tactics) >= 3
                else ("🟡 MULTI-TACTIC" if len(tactics) >= 2 else "")
            )
            lines.append(f"\n  {ip}  {alert_count} alerts  {campaign_flag}")
            if tactics:
                lines.append(f"    Tactics:  {' → '.join(tactics)}")
            if agents_hit:
                lines.append(f"    Targets:  {', '.join(agents_hit)}")
            if first_ts:
                lines.append(f"    Timeline: {first_ts} → {last_ts}")

    # ── Recommendation ───────────────────────────────────────────────────────────
    lines.append("")
    lines.append("Recommendations:")
    if multi_stage_attackers:
        ips = ", ".join(b["key"] for b in multi_stage_attackers[:3])
        lines.append(f"  • Investigate multi-tactic source IPs immediately: {ips}")
        lines.append("    Use get_wazuh_alerts(severity='high') filtered by these IPs.")
        lines.append("    Consider add_crowdsec_decision() to block persistent attackers.")
    if any(b.get("key") in ("Lateral Movement", "Privilege Escalation") for b in tactic_buckets):
        lines.append(
            "  • Lateral movement / privilege escalation detected — isolate affected agents."
        )
    if any(b.get("key") in ("Exfiltration", "Command and Control") for b in tactic_buckets):
        lines.append(
            "  • Exfiltration / C2 activity detected — check outbound traffic immediately."
        )
    if not multi_stage_attackers and len(stages_seen) < 2:
        lines.append("  • No active campaign detected. Monitor for pattern escalation.")

    return "\n".join(lines)


def run_wazuh_sca_scan(data: dict) -> str:
    """Trigger an on-demand SCA scan for a specific agent."""
    if not _wazuh_available():
        return "Wazuh not configured. Set WAZUH_HOST, WAZUH_USER, and WAZUH_PASSWORD."

    agent_id = data.get("agent_id", "").strip()
    if not agent_id:
        return "agent_id is required. Use get_wazuh_agents() to find agent IDs."

    cfg = _get_config()
    token = _get_jwt_token()
    if not token:
        return "Could not authenticate with Wazuh API."

    try:
        resp = httpx.put(
            f"{cfg['host']}/sca/{agent_id}/run",
            headers={"Authorization": f"Bearer {token}"},
            verify=cfg["verify_ssl"],
            timeout=REQUEST_TIMEOUT,
        )
        if resp.status_code in (200, 202):
            return f"SCA scan triggered for agent {agent_id}. Results will appear in the dashboard within a few minutes."
        return format_http_error("Wazuh SCA trigger", status_code=resp.status_code)

    except httpx.HTTPError as e:
        return format_http_error("Wazuh", exception=e)


# ---------------------------------------------------------------------------
# Tool definitions
# ---------------------------------------------------------------------------

TOOLS = [
    {
        "name": "get_wazuh_alerts",
        "description": (
            "Retrieve recent security alerts from the Wazuh SIEM indexer. "
            "Filter by severity (critical, high, medium, low) or omit for all severities. "
            "Returns timestamp, agent name, severity level, and rule description for each alert. "
            "Use this to triage the most urgent security events."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "severity": {
                    "type": "string",
                    "enum": ["critical", "high", "medium", "low", ""],
                    "description": "Filter by severity level. Omit for all.",
                },
                "limit": {
                    "type": "integer",
                    "description": "Max alerts to return (default 20, max 100)",
                    "default": 20,
                },
            },
        },
        "handler": get_wazuh_alerts,
        "category": "wazuh",
    },
    {
        "name": "get_wazuh_agents",
        "description": (
            "List all enrolled Wazuh agents with their connection status, IP, version, "
            "and last keepalive. Shows which agents are active, disconnected, or never connected. "
            "Use this to verify endpoint coverage before hunting or to spot agents that fell offline."
        ),
        "input_schema": {"type": "object", "properties": {}},
        "handler": get_wazuh_agents,
        "category": "wazuh",
    },
    {
        "name": "get_vulnerability_summary",
        "description": (
            "Get a CVE vulnerability summary across all active Wazuh agents, broken down by severity "
            "(critical, high, medium, low). Shows totals and a per-agent breakdown sorted by critical count. "
            "Use this to prioritize patching — focus on critical CVEs on production systems first."
        ),
        "input_schema": {"type": "object", "properties": {}},
        "handler": get_vulnerability_summary,
        "category": "wazuh",
    },
    {
        "name": "get_compliance_posture",
        "description": (
            "Get CIS or NIST CSF compliance posture from Wazuh SCA (Security Configuration Assessment) results. "
            "Shows pass/fail/N-A counts and overall compliance score per agent. "
            "Use framework='cis' for CIS Benchmarks or framework='nist' for NIST CSF controls."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "framework": {
                    "type": "string",
                    "enum": ["cis", "nist", "pci", "hipaa", "gdpr"],
                    "description": "Compliance framework to report on",
                    "default": "cis",
                },
            },
        },
        "handler": get_compliance_posture,
        "category": "wazuh",
    },
    {
        "name": "get_attack_patterns",
        "description": (
            "Correlate Wazuh alerts against the MITRE ATT&CK framework to identify attack campaigns "
            "and kill chain progression. Groups alerts by tactic (Reconnaissance → Initial Access → "
            "Execution → Persistence → Privilege Escalation → Lateral Movement → Exfiltration) and "
            "technique, identifies source IPs active across multiple tactics (campaign indicators), "
            "and surfaces the attack timeline. Use this when you see a spike in alerts and want to "
            "understand if it's a coordinated attack or isolated incidents."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "hours": {
                    "type": "integer",
                    "description": "Look-back window in hours (1–168, default 24)",
                    "default": 24,
                },
                "min_alerts": {
                    "type": "integer",
                    "description": "Minimum alert count to include a source IP in attacker analysis (default 2)",
                    "default": 2,
                },
            },
        },
        "handler": get_attack_patterns,
        "category": "wazuh",
    },
    {
        "name": "run_wazuh_sca_scan",
        "description": (
            "Trigger an on-demand Security Configuration Assessment (SCA) scan for a specific agent. "
            "Results will be available in the compliance posture within a few minutes. "
            "Use get_wazuh_agents() first to find the agent_id."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "agent_id": {
                    "type": "string",
                    "description": "Wazuh agent ID (e.g. '001', '002'). Use get_wazuh_agents() to list.",
                },
            },
            "required": ["agent_id"],
        },
        "handler": run_wazuh_sca_scan,
        "confirm": True,
        "confirm_message": "Trigger SCA scan on Wazuh agent?",
        "risk_level": "medium",
        "category": "wazuh",
    },
]
