1007 lines
30 KiB
Python
1007 lines
30 KiB
Python
#!/usr/bin/env python3
|
|
import os
|
|
from pathlib import Path
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
import requests
|
|
import urllib3
|
|
from dotenv import load_dotenv
|
|
from mcp.server.fastmcp import FastMCP
|
|
|
|
BASE_DIR = Path(__file__).resolve().parent
|
|
load_dotenv(BASE_DIR / ".env")
|
|
|
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
|
|
WAZUH_URL = os.getenv("WAZUH_URL", "https://192.168.1.52:55000").rstrip("/")
|
|
WAZUH_USER = os.getenv("WAZUH_USER", "wazuh-wui")
|
|
WAZUH_PASSWORD = os.getenv("WAZUH_PASSWORD")
|
|
|
|
INDEXER_URL = os.getenv("INDEXER_URL", "https://192.168.1.52:9200").rstrip("/")
|
|
INDEXER_USER = os.getenv("INDEXER_USER")
|
|
INDEXER_PASSWORD = os.getenv("INDEXER_PASSWORD")
|
|
INDEXER_ALERT_INDEX = os.getenv("INDEXER_ALERT_INDEX", "wazuh-alerts-*")
|
|
INDEXER_VULN_INDEX = os.getenv("INDEXER_VULN_INDEX", "wazuh-states-vulnerabilities-*")
|
|
|
|
mcp = FastMCP("wazuh")
|
|
|
|
_token = None
|
|
|
|
|
|
def utc_since(hours: int) -> str:
|
|
return (datetime.now(timezone.utc) - timedelta(hours=hours)).isoformat()
|
|
|
|
|
|
def get_token() -> str:
|
|
global _token
|
|
|
|
if _token:
|
|
return _token
|
|
|
|
if not WAZUH_PASSWORD:
|
|
raise RuntimeError("WAZUH_PASSWORD manquant dans .env")
|
|
|
|
r = requests.post(
|
|
f"{WAZUH_URL}/security/user/authenticate?raw=true",
|
|
auth=(WAZUH_USER, WAZUH_PASSWORD),
|
|
verify=False,
|
|
timeout=15,
|
|
)
|
|
r.raise_for_status()
|
|
_token = r.text.strip()
|
|
return _token
|
|
|
|
|
|
def wazuh_get(endpoint: str) -> dict:
|
|
token = get_token()
|
|
r = requests.get(
|
|
f"{WAZUH_URL}{endpoint}",
|
|
headers={"Authorization": f"Bearer {token}"},
|
|
verify=False,
|
|
timeout=30,
|
|
)
|
|
r.raise_for_status()
|
|
return r.json()
|
|
|
|
|
|
def indexer_search(index: str, body: dict) -> dict:
|
|
if not INDEXER_USER or not INDEXER_PASSWORD:
|
|
raise RuntimeError("INDEXER_USER ou INDEXER_PASSWORD manquant dans .env")
|
|
|
|
r = requests.post(
|
|
f"{INDEXER_URL}/{index}/_search",
|
|
auth=(INDEXER_USER, INDEXER_PASSWORD),
|
|
json=body,
|
|
verify=False,
|
|
timeout=30,
|
|
)
|
|
r.raise_for_status()
|
|
return r.json()
|
|
|
|
|
|
def simple_time_query(hours: int, size: int = 20, extra_filters=None) -> dict:
|
|
filters = [
|
|
{"range": {"@timestamp": {"gte": utc_since(hours)}}}
|
|
]
|
|
|
|
if extra_filters:
|
|
filters.extend(extra_filters)
|
|
|
|
return {
|
|
"size": size,
|
|
"sort": [{"@timestamp": {"order": "desc"}}],
|
|
"query": {
|
|
"bool": {
|
|
"filter": filters
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
# -------------------------
|
|
# Wazuh Manager API
|
|
# -------------------------
|
|
|
|
@mcp.tool()
|
|
def manager_status() -> dict:
|
|
"""Retourne l'état du Wazuh Manager."""
|
|
return wazuh_get("/manager/status")
|
|
|
|
|
|
@mcp.tool()
|
|
def manager_info() -> dict:
|
|
"""Retourne les informations du Wazuh Manager."""
|
|
return wazuh_get("/manager/info")
|
|
|
|
|
|
@mcp.tool()
|
|
def list_agents(limit: int = 100) -> dict:
|
|
"""Liste les agents Wazuh."""
|
|
return wazuh_get(f"/agents?limit={limit}")
|
|
|
|
|
|
@mcp.tool()
|
|
def list_active_agents(limit: int = 100) -> dict:
|
|
"""Liste les agents actifs."""
|
|
return wazuh_get(f"/agents?status=active&limit={limit}")
|
|
|
|
|
|
@mcp.tool()
|
|
def list_disconnected_agents(limit: int = 100) -> dict:
|
|
"""Liste les agents déconnectés."""
|
|
return wazuh_get(f"/agents?status=disconnected&limit={limit}")
|
|
|
|
|
|
@mcp.tool()
|
|
def list_never_connected_agents(limit: int = 100) -> dict:
|
|
"""Liste les agents jamais connectés."""
|
|
return wazuh_get(f"/agents?status=never_connected&limit={limit}")
|
|
|
|
|
|
@mcp.tool()
|
|
def get_agent(agent_id: str) -> dict:
|
|
"""Retourne le détail d'un agent Wazuh par son ID."""
|
|
return wazuh_get(f"/agents?agents_list={agent_id}")
|
|
|
|
|
|
@mcp.tool()
|
|
def get_agent_config(agent_id: str) -> dict:
|
|
"""Retourne la configuration effective d'un agent."""
|
|
return wazuh_get(f"/agents/{agent_id}/config")
|
|
|
|
|
|
@mcp.tool()
|
|
def search_agents(query: str, limit: int = 100) -> dict:
|
|
"""Recherche des agents par nom, IP ou information associée."""
|
|
return wazuh_get(f"/agents?q={query}&limit={limit}")
|
|
|
|
|
|
# -------------------------
|
|
# Wazuh Indexer / Alerts
|
|
# -------------------------
|
|
|
|
@mcp.tool()
|
|
def list_alerts(hours: int = 24, limit: int = 20) -> dict:
|
|
"""Liste les dernières alertes Wazuh sur une période donnée."""
|
|
body = simple_time_query(hours=hours, size=limit)
|
|
return indexer_search(INDEXER_ALERT_INDEX, body)
|
|
|
|
|
|
@mcp.tool()
|
|
def list_critical_alerts(hours: int = 24, limit: int = 20) -> dict:
|
|
"""Liste les alertes critiques, niveau 12 ou supérieur."""
|
|
body = simple_time_query(
|
|
hours=hours,
|
|
size=limit,
|
|
extra_filters=[
|
|
{"range": {"rule.level": {"gte": 12}}}
|
|
],
|
|
)
|
|
return indexer_search(INDEXER_ALERT_INDEX, body)
|
|
|
|
|
|
@mcp.tool()
|
|
def top_alerting_agents(hours: int = 24, limit: int = 10) -> dict:
|
|
"""Retourne les agents générant le plus d'alertes."""
|
|
body = {
|
|
"size": 0,
|
|
"query": {
|
|
"range": {
|
|
"@timestamp": {
|
|
"gte": utc_since(hours)
|
|
}
|
|
}
|
|
},
|
|
"aggs": {
|
|
"agents": {
|
|
"terms": {
|
|
"field": "agent.name",
|
|
"size": limit
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return indexer_search(INDEXER_ALERT_INDEX, body)
|
|
|
|
|
|
@mcp.tool()
|
|
def search_alerts(query: str, hours: int = 24, limit: int = 20) -> dict:
|
|
"""Recherche libre dans les alertes Wazuh."""
|
|
body = {
|
|
"size": limit,
|
|
"sort": [{"@timestamp": {"order": "desc"}}],
|
|
"query": {
|
|
"bool": {
|
|
"filter": [
|
|
{"range": {"@timestamp": {"gte": utc_since(hours)}}}
|
|
],
|
|
"must": [
|
|
{
|
|
"query_string": {
|
|
"query": query
|
|
}
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
return indexer_search(INDEXER_ALERT_INDEX, body)
|
|
|
|
|
|
# -------------------------
|
|
# Auth / SSH / sudo
|
|
# -------------------------
|
|
|
|
@mcp.tool()
|
|
def get_auth_failures(hours: int = 24, limit: int = 30) -> dict:
|
|
"""Liste les échecs d'authentification récents."""
|
|
body = simple_time_query(
|
|
hours=hours,
|
|
size=limit,
|
|
extra_filters=[
|
|
{
|
|
"bool": {
|
|
"should": [
|
|
{"match_phrase": {"rule.groups": "authentication_failed"}},
|
|
{"match_phrase": {"rule.groups": "sshd"}},
|
|
{"match_phrase": {"full_log": "Failed password"}},
|
|
{"match_phrase": {"full_log": "authentication failure"}}
|
|
],
|
|
"minimum_should_match": 1
|
|
}
|
|
}
|
|
],
|
|
)
|
|
return indexer_search(INDEXER_ALERT_INDEX, body)
|
|
|
|
|
|
@mcp.tool()
|
|
def get_sudo_events(hours: int = 24, limit: int = 30) -> dict:
|
|
"""Liste les événements sudo récents."""
|
|
body = simple_time_query(
|
|
hours=hours,
|
|
size=limit,
|
|
extra_filters=[
|
|
{
|
|
"bool": {
|
|
"should": [
|
|
{"match_phrase": {"rule.groups": "sudo"}},
|
|
{"match_phrase": {"full_log": "sudo"}}
|
|
],
|
|
"minimum_should_match": 1
|
|
}
|
|
}
|
|
],
|
|
)
|
|
return indexer_search(INDEXER_ALERT_INDEX, body)
|
|
|
|
|
|
# -------------------------
|
|
# FIM
|
|
# -------------------------
|
|
|
|
@mcp.tool()
|
|
def get_fim_events(hours: int = 24, limit: int = 30) -> dict:
|
|
"""Liste les événements d'intégrité fichiers FIM/syscheck."""
|
|
body = simple_time_query(
|
|
hours=hours,
|
|
size=limit,
|
|
extra_filters=[
|
|
{
|
|
"bool": {
|
|
"should": [
|
|
{"match_phrase": {"rule.groups": "syscheck"}},
|
|
{"exists": {"field": "syscheck.path"}}
|
|
],
|
|
"minimum_should_match": 1
|
|
}
|
|
}
|
|
],
|
|
)
|
|
return indexer_search(INDEXER_ALERT_INDEX, body)
|
|
|
|
|
|
@mcp.tool()
|
|
def get_fim_events_for_path(path: str, hours: int = 24, limit: int = 30) -> dict:
|
|
"""Liste les modifications FIM pour un chemin donné."""
|
|
body = simple_time_query(
|
|
hours=hours,
|
|
size=limit,
|
|
extra_filters=[
|
|
{"wildcard": {"syscheck.path": f"*{path}*"}}
|
|
],
|
|
)
|
|
return indexer_search(INDEXER_ALERT_INDEX, body)
|
|
|
|
|
|
# -------------------------
|
|
# SCA / compliance
|
|
# -------------------------
|
|
|
|
@mcp.tool()
|
|
def get_sca_failures(hours: int = 24, limit: int = 30) -> dict:
|
|
"""Liste les contrôles SCA en échec."""
|
|
body = simple_time_query(
|
|
hours=hours,
|
|
size=limit,
|
|
extra_filters=[
|
|
{
|
|
"bool": {
|
|
"should": [
|
|
{"match_phrase": {"rule.groups": "sca"}},
|
|
{"exists": {"field": "data.sca"}}
|
|
],
|
|
"minimum_should_match": 1
|
|
}
|
|
},
|
|
{
|
|
"bool": {
|
|
"should": [
|
|
{"match_phrase": {"data.sca.check.result": "failed"}},
|
|
{"match_phrase": {"full_log": "failed"}}
|
|
],
|
|
"minimum_should_match": 1
|
|
}
|
|
}
|
|
],
|
|
)
|
|
return indexer_search(INDEXER_ALERT_INDEX, body)
|
|
|
|
|
|
# -------------------------
|
|
# Vulnerabilities
|
|
# -------------------------
|
|
|
|
@mcp.tool()
|
|
def list_vulnerabilities(limit: int = 50) -> dict:
|
|
"""Liste les vulnérabilités présentes dans l'index Wazuh."""
|
|
body = {
|
|
"size": limit,
|
|
"sort": [{"@timestamp": {"order": "desc"}}],
|
|
"query": {
|
|
"match_all": {}
|
|
}
|
|
}
|
|
return indexer_search(INDEXER_VULN_INDEX, body)
|
|
|
|
|
|
@mcp.tool()
|
|
def list_critical_vulnerabilities(limit: int = 50) -> dict:
|
|
"""Liste les vulnérabilités critiques."""
|
|
body = {
|
|
"size": limit,
|
|
"sort": [{"@timestamp": {"order": "desc"}}],
|
|
"query": {
|
|
"bool": {
|
|
"should": [
|
|
{"match_phrase": {"vulnerability.severity": "Critical"}},
|
|
{"match_phrase": {"vulnerability.severity": "CRITICAL"}},
|
|
{"range": {"vulnerability.score.base": {"gte": 9}}}
|
|
],
|
|
"minimum_should_match": 1
|
|
}
|
|
}
|
|
}
|
|
return indexer_search(INDEXER_VULN_INDEX, body)
|
|
|
|
|
|
@mcp.tool()
|
|
def get_agent_vulnerabilities(agent_name: str, limit: int = 50) -> dict:
|
|
"""Liste les vulnérabilités d'un agent par nom."""
|
|
body = {
|
|
"size": limit,
|
|
"sort": [{"@timestamp": {"order": "desc"}}],
|
|
"query": {
|
|
"bool": {
|
|
"filter": [
|
|
{"match_phrase": {"agent.name": agent_name}}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
return indexer_search(INDEXER_VULN_INDEX, body)
|
|
|
|
|
|
@mcp.tool()
|
|
def top_vulnerable_agents(limit: int = 10) -> dict:
|
|
"""Retourne les agents avec le plus de vulnérabilités."""
|
|
body = {
|
|
"size": 0,
|
|
"query": {"match_all": {}},
|
|
"aggs": {
|
|
"agents": {
|
|
"terms": {
|
|
"field": "agent.name",
|
|
"size": limit
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return indexer_search(INDEXER_VULN_INDEX, body)
|
|
|
|
|
|
# -------------------------
|
|
# Résumé sécurité
|
|
# -------------------------
|
|
|
|
@mcp.tool()
|
|
def security_summary(hours: int = 24) -> dict:
|
|
"""Produit un résumé sécurité global sur les dernières heures."""
|
|
summary = {}
|
|
|
|
try:
|
|
summary["manager_status"] = manager_status()
|
|
except Exception as e:
|
|
summary["manager_status_error"] = str(e)
|
|
|
|
try:
|
|
summary["disconnected_agents"] = list_disconnected_agents(limit=100)
|
|
except Exception as e:
|
|
summary["disconnected_agents_error"] = str(e)
|
|
|
|
try:
|
|
summary["critical_alerts"] = list_critical_alerts(hours=hours, limit=10)
|
|
except Exception as e:
|
|
summary["critical_alerts_error"] = str(e)
|
|
|
|
try:
|
|
summary["top_alerting_agents"] = top_alerting_agents(hours=hours, limit=10)
|
|
except Exception as e:
|
|
summary["top_alerting_agents_error"] = str(e)
|
|
|
|
try:
|
|
summary["auth_failures"] = get_auth_failures(hours=hours, limit=10)
|
|
except Exception as e:
|
|
summary["auth_failures_error"] = str(e)
|
|
|
|
try:
|
|
summary["critical_vulnerabilities"] = list_critical_vulnerabilities(limit=10)
|
|
except Exception as e:
|
|
summary["critical_vulnerabilities_error"] = str(e)
|
|
|
|
try:
|
|
summary["fim_events"] = get_fim_events(hours=hours, limit=10)
|
|
except Exception as e:
|
|
summary["fim_events_error"] = str(e)
|
|
|
|
return summary
|
|
|
|
@mcp.tool()
|
|
def vulnerability_report(limit_hosts: int = 100) -> dict:
|
|
"""Rapport des vulnérabilités par host et par criticité."""
|
|
body = {
|
|
"size": 0,
|
|
"query": {
|
|
"match_all": {}
|
|
},
|
|
"aggs": {
|
|
"hosts": {
|
|
"terms": {
|
|
"field": "agent.name",
|
|
"size": limit_hosts
|
|
},
|
|
"aggs": {
|
|
"severity": {
|
|
"terms": {
|
|
"field": "vulnerability.severity",
|
|
"size": 10
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
result = indexer_search(INDEXER_VULN_INDEX, body)
|
|
|
|
report = []
|
|
buckets = result.get("aggregations", {}).get("hosts", {}).get("buckets", [])
|
|
|
|
for host in buckets:
|
|
counts = {
|
|
"host": host.get("key"),
|
|
"critical": 0,
|
|
"high": 0,
|
|
"medium": 0,
|
|
"low": 0,
|
|
"total": host.get("doc_count", 0),
|
|
}
|
|
|
|
for sev in host.get("severity", {}).get("buckets", []):
|
|
severity = str(sev.get("key", "")).lower()
|
|
count = sev.get("doc_count", 0)
|
|
|
|
if severity == "critical":
|
|
counts["critical"] = count
|
|
elif severity == "high":
|
|
counts["high"] = count
|
|
elif severity == "medium":
|
|
counts["medium"] = count
|
|
elif severity == "low":
|
|
counts["low"] = count
|
|
|
|
report.append(counts)
|
|
|
|
report.sort(
|
|
key=lambda x: (
|
|
x["critical"],
|
|
x["high"],
|
|
x["medium"],
|
|
x["low"],
|
|
x["total"],
|
|
),
|
|
reverse=True,
|
|
)
|
|
|
|
return {
|
|
"summary": {
|
|
"hosts": len(report),
|
|
"total_vulnerabilities": sum(h["total"] for h in report),
|
|
"critical": sum(h["critical"] for h in report),
|
|
"high": sum(h["high"] for h in report),
|
|
"medium": sum(h["medium"] for h in report),
|
|
"low": sum(h["low"] for h in report),
|
|
},
|
|
"hosts": report,
|
|
}
|
|
|
|
@mcp.tool()
|
|
def cve_report_by_host(limit_hosts: int = 100, limit_cves_per_host: int = 50) -> dict:
|
|
"""
|
|
Rapport détaillé des CVE par host.
|
|
|
|
Exemple de sortie :
|
|
host1:
|
|
CVE-2024-XXXX : faille sur le paquet openssl - Critical
|
|
CVE-2025-YYYY : faille sur le paquet curl - High
|
|
"""
|
|
|
|
body = {
|
|
"size": 0,
|
|
"query": {
|
|
"match_all": {}
|
|
},
|
|
"aggs": {
|
|
"hosts": {
|
|
"terms": {
|
|
"field": "agent.name",
|
|
"size": limit_hosts
|
|
},
|
|
"aggs": {
|
|
"cves": {
|
|
"terms": {
|
|
"field": "vulnerability.id",
|
|
"size": limit_cves_per_host
|
|
},
|
|
"aggs": {
|
|
"sample": {
|
|
"top_hits": {
|
|
"size": 1,
|
|
"_source": {
|
|
"includes": [
|
|
"agent.name",
|
|
"vulnerability.id",
|
|
"vulnerability.title",
|
|
"vulnerability.description",
|
|
"vulnerability.severity",
|
|
"vulnerability.score.base",
|
|
"package.name",
|
|
"package.version",
|
|
"package.architecture"
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
result = indexer_search(INDEXER_VULN_INDEX, body)
|
|
|
|
report = []
|
|
hosts = result.get("aggregations", {}).get("hosts", {}).get("buckets", [])
|
|
|
|
severity_order = {
|
|
"critical": 4,
|
|
"high": 3,
|
|
"medium": 2,
|
|
"low": 1,
|
|
"unknown": 0,
|
|
}
|
|
|
|
for host_bucket in hosts:
|
|
host_name = host_bucket.get("key")
|
|
cve_entries = []
|
|
|
|
cve_buckets = host_bucket.get("cves", {}).get("buckets", [])
|
|
|
|
for cve_bucket in cve_buckets:
|
|
sample_hits = (
|
|
cve_bucket
|
|
.get("sample", {})
|
|
.get("hits", {})
|
|
.get("hits", [])
|
|
)
|
|
|
|
if not sample_hits:
|
|
continue
|
|
|
|
src = sample_hits[0].get("_source", {})
|
|
|
|
vulnerability = src.get("vulnerability", {})
|
|
package = src.get("package", {})
|
|
|
|
cve_id = vulnerability.get("id", cve_bucket.get("key", "CVE inconnue"))
|
|
title = vulnerability.get("title") or vulnerability.get("description") or "faille détectée"
|
|
severity = vulnerability.get("severity", "Unknown")
|
|
score = vulnerability.get("score", {}).get("base")
|
|
package_name = package.get("name", "paquet inconnu")
|
|
package_version = package.get("version", "version inconnue")
|
|
package_arch = package.get("architecture", "")
|
|
|
|
cve_entries.append({
|
|
"cve": cve_id,
|
|
"package": package_name,
|
|
"version": package_version,
|
|
"architecture": package_arch,
|
|
"severity": severity,
|
|
"score": score,
|
|
"title": title,
|
|
"text": f"{cve_id} : faille sur le paquet {package_name} {package_version} - {severity}",
|
|
})
|
|
|
|
cve_entries.sort(
|
|
key=lambda x: (
|
|
severity_order.get(str(x["severity"]).lower(), 0),
|
|
x["score"] or 0,
|
|
x["cve"]
|
|
),
|
|
reverse=True
|
|
)
|
|
|
|
report.append({
|
|
"host": host_name,
|
|
"total_cves": len(cve_entries),
|
|
"cves": cve_entries,
|
|
})
|
|
|
|
report.sort(
|
|
key=lambda x: x["total_cves"],
|
|
reverse=True
|
|
)
|
|
|
|
return {
|
|
"summary": {
|
|
"hosts": len(report),
|
|
"total_cves": sum(h["total_cves"] for h in report),
|
|
},
|
|
"hosts": report,
|
|
}
|
|
|
|
@mcp.tool()
|
|
def cve_report_by_severity(severity: str = "Critical", limit_hosts: int = 100, limit_cves_per_host: int = 50) -> dict:
|
|
"""
|
|
Rapport des CVE par host filtré par criticité.
|
|
severity peut être : Critical, High, Medium, Low
|
|
"""
|
|
|
|
body = {
|
|
"size": 0,
|
|
"query": {
|
|
"match_phrase": {
|
|
"vulnerability.severity": severity
|
|
}
|
|
},
|
|
"aggs": {
|
|
"hosts": {
|
|
"terms": {
|
|
"field": "agent.name",
|
|
"size": limit_hosts
|
|
},
|
|
"aggs": {
|
|
"cves": {
|
|
"terms": {
|
|
"field": "vulnerability.id",
|
|
"size": limit_cves_per_host
|
|
},
|
|
"aggs": {
|
|
"sample": {
|
|
"top_hits": {
|
|
"size": 1,
|
|
"_source": {
|
|
"includes": [
|
|
"agent.name",
|
|
"vulnerability.id",
|
|
"vulnerability.title",
|
|
"vulnerability.description",
|
|
"vulnerability.severity",
|
|
"vulnerability.score.base",
|
|
"package.name",
|
|
"package.version",
|
|
"package.architecture"
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
result = indexer_search(INDEXER_VULN_INDEX, body)
|
|
|
|
report = []
|
|
hosts = result.get("aggregations", {}).get("hosts", {}).get("buckets", [])
|
|
|
|
for host_bucket in hosts:
|
|
host_name = host_bucket.get("key")
|
|
cve_entries = []
|
|
|
|
for cve_bucket in host_bucket.get("cves", {}).get("buckets", []):
|
|
sample_hits = (
|
|
cve_bucket
|
|
.get("sample", {})
|
|
.get("hits", {})
|
|
.get("hits", [])
|
|
)
|
|
|
|
if not sample_hits:
|
|
continue
|
|
|
|
src = sample_hits[0].get("_source", {})
|
|
vulnerability = src.get("vulnerability", {})
|
|
package = src.get("package", {})
|
|
|
|
cve_id = vulnerability.get("id", cve_bucket.get("key", "CVE inconnue"))
|
|
package_name = package.get("name", "paquet inconnu")
|
|
package_version = package.get("version", "version inconnue")
|
|
title = vulnerability.get("title") or vulnerability.get("description") or "faille détectée"
|
|
score = vulnerability.get("score", {}).get("base")
|
|
|
|
cve_entries.append({
|
|
"cve": cve_id,
|
|
"package": package_name,
|
|
"version": package_version,
|
|
"severity": vulnerability.get("severity", severity),
|
|
"score": score,
|
|
"title": title,
|
|
"text": f"{cve_id} : faille sur le paquet {package_name} {package_version} - {severity}",
|
|
})
|
|
|
|
report.append({
|
|
"host": host_name,
|
|
"severity": severity,
|
|
"total_cves": len(cve_entries),
|
|
"cves": cve_entries,
|
|
})
|
|
|
|
report.sort(key=lambda x: x["total_cves"], reverse=True)
|
|
|
|
return {
|
|
"summary": {
|
|
"severity": severity,
|
|
"hosts": len(report),
|
|
"total_cves": sum(h["total_cves"] for h in report),
|
|
},
|
|
"hosts": report,
|
|
}
|
|
@mcp.tool()
|
|
def sca_summary_by_host(hours: int = 168, limit_hosts: int = 100) -> dict:
|
|
"""
|
|
Résumé de l'évaluation des configurations SCA par host.
|
|
Donne le nombre de checks passés, échoués, non applicables, etc.
|
|
"""
|
|
body = {
|
|
"size": 0,
|
|
"query": {
|
|
"bool": {
|
|
"filter": [
|
|
{"range": {"@timestamp": {"gte": utc_since(hours)}}},
|
|
{
|
|
"bool": {
|
|
"should": [
|
|
{"match_phrase": {"rule.groups": "sca"}},
|
|
{"exists": {"field": "data.sca.check.result"}}
|
|
],
|
|
"minimum_should_match": 1
|
|
}
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"aggs": {
|
|
"hosts": {
|
|
"terms": {
|
|
"field": "agent.name",
|
|
"size": limit_hosts
|
|
},
|
|
"aggs": {
|
|
"results": {
|
|
"terms": {
|
|
"field": "data.sca.check.result",
|
|
"size": 10
|
|
}
|
|
},
|
|
"policies": {
|
|
"terms": {
|
|
"field": "data.sca.policy",
|
|
"size": 10
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
result = indexer_search(INDEXER_ALERT_INDEX, body)
|
|
hosts = result.get("aggregations", {}).get("hosts", {}).get("buckets", [])
|
|
|
|
report = []
|
|
|
|
for host in hosts:
|
|
counts = {
|
|
"host": host.get("key"),
|
|
"passed": 0,
|
|
"failed": 0,
|
|
"not_applicable": 0,
|
|
"unknown": 0,
|
|
"total": host.get("doc_count", 0),
|
|
"policies": []
|
|
}
|
|
|
|
for bucket in host.get("results", {}).get("buckets", []):
|
|
key = str(bucket.get("key", "")).lower()
|
|
count = bucket.get("doc_count", 0)
|
|
|
|
if key in ["passed", "pass"]:
|
|
counts["passed"] += count
|
|
elif key in ["failed", "fail"]:
|
|
counts["failed"] += count
|
|
elif key in ["not applicable", "not_applicable", "n/a"]:
|
|
counts["not_applicable"] += count
|
|
else:
|
|
counts["unknown"] += count
|
|
|
|
counts["policies"] = [
|
|
{
|
|
"policy": p.get("key"),
|
|
"count": p.get("doc_count", 0)
|
|
}
|
|
for p in host.get("policies", {}).get("buckets", [])
|
|
]
|
|
|
|
effective_total = counts["passed"] + counts["failed"]
|
|
counts["compliance_percent"] = round(
|
|
(counts["passed"] / effective_total) * 100, 2
|
|
) if effective_total > 0 else None
|
|
|
|
report.append(counts)
|
|
|
|
report.sort(key=lambda x: (x["failed"], x["total"]), reverse=True)
|
|
|
|
return {
|
|
"summary": {
|
|
"hosts": len(report),
|
|
"total_checks": sum(h["total"] for h in report),
|
|
"passed": sum(h["passed"] for h in report),
|
|
"failed": sum(h["failed"] for h in report),
|
|
"not_applicable": sum(h["not_applicable"] for h in report),
|
|
},
|
|
"hosts": report
|
|
}
|
|
|
|
@mcp.tool()
|
|
def sca_failed_checks_by_host(hours: int = 168, limit_hosts: int = 50, limit_checks_per_host: int = 20) -> dict:
|
|
"""
|
|
Liste les contrôles SCA échoués par host.
|
|
Utile pour demander : résumé de l'évaluation des configurations.
|
|
"""
|
|
body = {
|
|
"size": 0,
|
|
"query": {
|
|
"bool": {
|
|
"filter": [
|
|
{"range": {"@timestamp": {"gte": utc_since(hours)}}},
|
|
{"match_phrase": {"data.sca.check.result": "failed"}}
|
|
]
|
|
}
|
|
},
|
|
"aggs": {
|
|
"hosts": {
|
|
"terms": {
|
|
"field": "agent.name",
|
|
"size": limit_hosts
|
|
},
|
|
"aggs": {
|
|
"failed_checks": {
|
|
"terms": {
|
|
"field": "data.sca.check.title",
|
|
"size": limit_checks_per_host
|
|
},
|
|
"aggs": {
|
|
"sample": {
|
|
"top_hits": {
|
|
"size": 1,
|
|
"_source": {
|
|
"includes": [
|
|
"@timestamp",
|
|
"agent.name",
|
|
"data.sca.policy",
|
|
"data.sca.check.id",
|
|
"data.sca.check.title",
|
|
"data.sca.check.result",
|
|
"data.sca.check.description",
|
|
"data.sca.check.rationale",
|
|
"data.sca.check.remediation",
|
|
"rule.level",
|
|
"rule.description"
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
result = indexer_search(INDEXER_ALERT_INDEX, body)
|
|
hosts = result.get("aggregations", {}).get("hosts", {}).get("buckets", [])
|
|
|
|
report = []
|
|
|
|
for host_bucket in hosts:
|
|
checks = []
|
|
|
|
for check_bucket in host_bucket.get("failed_checks", {}).get("buckets", []):
|
|
hits = (
|
|
check_bucket
|
|
.get("sample", {})
|
|
.get("hits", {})
|
|
.get("hits", [])
|
|
)
|
|
|
|
if not hits:
|
|
continue
|
|
|
|
src = hits[0].get("_source", {})
|
|
sca = src.get("data", {}).get("sca", {})
|
|
check = sca.get("check", {})
|
|
|
|
checks.append({
|
|
"id": check.get("id"),
|
|
"title": check.get("title", check_bucket.get("key")),
|
|
"policy": sca.get("policy"),
|
|
"description": check.get("description"),
|
|
"rationale": check.get("rationale"),
|
|
"remediation": check.get("remediation"),
|
|
"rule_level": src.get("rule", {}).get("level"),
|
|
"count": check_bucket.get("doc_count", 0),
|
|
})
|
|
|
|
report.append({
|
|
"host": host_bucket.get("key"),
|
|
"failed_total": host_bucket.get("doc_count", 0),
|
|
"failed_checks": checks,
|
|
})
|
|
|
|
report.sort(key=lambda x: x["failed_total"], reverse=True)
|
|
|
|
return {
|
|
"summary": {
|
|
"hosts": len(report),
|
|
"total_failed_checks": sum(h["failed_total"] for h in report),
|
|
},
|
|
"hosts": report
|
|
}
|
|
|
|
|
|
if __name__ == "__main__":
|
|
mcp.run()
|