334 lines
18 KiB
Python
334 lines
18 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Crée le dashboard "Securite - API Backend & WAF" dans OpenSearch Dashboards (Wazuh).
|
|
|
|
Usage:
|
|
python3 create-dashboard-api.py
|
|
|
|
Le script se connecte depuis l'intérieur du container wazuh_dashboard via:
|
|
docker exec wazuh_dashboard python3 /tmp/create-dashboard-api.py
|
|
|
|
Données utilisées :
|
|
- rules 100101 (WAF alerté) / 100102 (WAF bloqué) → ModSecurity
|
|
- rules 100600-100613 → Brute-force login API
|
|
- fields: data.transaction.client_ip, .request.uri,
|
|
.response.http_code, .is_interrupted,
|
|
.messages.details.ruleId
|
|
"""
|
|
|
|
import json
|
|
import urllib.request
|
|
import urllib.error
|
|
import ssl
|
|
import base64
|
|
import sys
|
|
import os
|
|
|
|
# ── Configuration ─────────────────────────────────────────────────────────────
|
|
DASHBOARD_HOST = "https://localhost:5601"
|
|
DASHBOARD_USER = "kibanaserver"
|
|
DASHBOARD_PASS = os.environ.get("DASHBOARD_PASSWORD", "E9Jpr6586kQ3wYrCS2!")
|
|
INDEX_PATTERN = "wazuh-alerts-*"
|
|
|
|
# ── Queries ───────────────────────────────────────────────────────────────────
|
|
Q_WAF = "rule.id: 100101 OR rule.id: 100102"
|
|
Q_BLOCKED = "rule.id: 100102"
|
|
Q_ALERTED = "rule.id: 100101"
|
|
Q_BFORCE = "rule.groups: api_brute_force"
|
|
Q_ALL = Q_WAF + " OR " + Q_BFORCE
|
|
|
|
# ── Client HTTP ───────────────────────────────────────────────────────────────
|
|
ctx = ssl.create_default_context()
|
|
ctx.check_hostname = False
|
|
ctx.verify_mode = ssl.CERT_NONE
|
|
|
|
_auth = base64.b64encode(f"{DASHBOARD_USER}:{DASHBOARD_PASS}".encode()).decode()
|
|
_headers = {
|
|
"Content-Type": "application/json",
|
|
"osd-xsrf": "true",
|
|
"Authorization": f"Basic {_auth}",
|
|
}
|
|
|
|
def api(method, path, body=None):
|
|
data = json.dumps(body).encode() if body else None
|
|
req = urllib.request.Request(DASHBOARD_HOST + path, data=data, headers=_headers, method=method)
|
|
try:
|
|
with urllib.request.urlopen(req, context=ctx, timeout=15) as r:
|
|
return json.loads(r.read())
|
|
except urllib.error.HTTPError as e:
|
|
return {"error": e.code, "msg": e.read().decode()[:300]}
|
|
|
|
def search_src(query):
|
|
return json.dumps({
|
|
"index": INDEX_PATTERN,
|
|
"filter": [],
|
|
"query": {"language": "kuery", "query": query},
|
|
})
|
|
|
|
# ── Visualisations ────────────────────────────────────────────────────────────
|
|
VISUALIZATIONS = [
|
|
# ── 1. Métrique : total alertes WAF ──────────────────────────────────────
|
|
{
|
|
"id": "api-waf-total",
|
|
"title": "[API] WAF - Total Alertes",
|
|
"type": "metric",
|
|
"query": Q_WAF,
|
|
"visState": {
|
|
"type": "metric",
|
|
"params": {
|
|
"metric": {
|
|
"percentageMode": False,
|
|
"useRanges": False,
|
|
"colorSchema": "Green to Red",
|
|
"metricColorMode": "Labels",
|
|
"colorsRange": [{"from": 0, "to": 50}, {"from": 50, "to": 500}, {"from": 500, "to": 99999}],
|
|
"labels": {"show": True},
|
|
"invertColors": False,
|
|
"style": {"bgFill": "#000", "bgColor": False, "labelColor": False, "subText": "alertes WAF", "fontSize": 60},
|
|
}
|
|
},
|
|
"aggs": [{"id": "1", "enabled": True, "type": "count", "schema": "metric", "params": {}}],
|
|
},
|
|
},
|
|
# ── 2. Métrique : requêtes bloquées ──────────────────────────────────────
|
|
{
|
|
"id": "api-waf-blocked",
|
|
"title": "[API] WAF - Requetes Bloquees",
|
|
"type": "metric",
|
|
"query": Q_BLOCKED,
|
|
"visState": {
|
|
"type": "metric",
|
|
"params": {
|
|
"metric": {
|
|
"percentageMode": False,
|
|
"useRanges": False,
|
|
"colorSchema": "Green to Red",
|
|
"metricColorMode": "Labels",
|
|
"colorsRange": [{"from": 0, "to": 10}, {"from": 10, "to": 100}, {"from": 100, "to": 99999}],
|
|
"labels": {"show": True},
|
|
"invertColors": False,
|
|
"style": {"bgFill": "#000", "bgColor": False, "labelColor": False, "subText": "bloquees par WAF", "fontSize": 60},
|
|
}
|
|
},
|
|
"aggs": [{"id": "1", "enabled": True, "type": "count", "schema": "metric", "params": {}}],
|
|
},
|
|
},
|
|
# ── 3. Métrique : brute-force login ──────────────────────────────────────
|
|
{
|
|
"id": "api-bf-count",
|
|
"title": "[API] Brute-Force Login",
|
|
"type": "metric",
|
|
"query": Q_BFORCE,
|
|
"visState": {
|
|
"type": "metric",
|
|
"params": {
|
|
"metric": {
|
|
"percentageMode": False,
|
|
"useRanges": False,
|
|
"colorSchema": "Green to Red",
|
|
"metricColorMode": "Labels",
|
|
"colorsRange": [{"from": 0, "to": 5}, {"from": 5, "to": 50}, {"from": 50, "to": 99999}],
|
|
"labels": {"show": True},
|
|
"invertColors": False,
|
|
"style": {"bgFill": "#000", "bgColor": False, "labelColor": False, "subText": "alertes brute-force", "fontSize": 60},
|
|
}
|
|
},
|
|
"aggs": [{"id": "1", "enabled": True, "type": "count", "schema": "metric", "params": {}}],
|
|
},
|
|
},
|
|
# ── 4. Timeline : activite WAF ────────────────────────────────────────────
|
|
{
|
|
"id": "api-waf-timeline",
|
|
"title": "[API] Timeline Activite WAF",
|
|
"type": "histogram",
|
|
"query": Q_ALL,
|
|
"visState": {
|
|
"type": "histogram",
|
|
"params": {
|
|
"type": "histogram",
|
|
"grid": {"categoryLines": False},
|
|
"categoryAxes": [{"id": "CategoryAxis-1", "type": "category", "position": "bottom", "show": True, "style": {}, "scale": {"type": "linear"}, "labels": {"show": True, "truncate": 100}, "title": {}}],
|
|
"valueAxes": [{"id": "ValueAxis-1", "name": "LeftAxis-1", "type": "value", "position": "left", "show": True, "style": {}, "scale": {"type": "linear", "mode": "normal"}, "labels": {"show": True, "rotate": 0, "filter": False, "truncate": 100}, "title": {"text": "Requetes"}}],
|
|
"seriesParams": [{"show": True, "type": "histogram", "mode": "stacked", "data": {"label": "Requetes", "id": "1"}, "valueAxis": "ValueAxis-1", "drawLinesBetweenPoints": True, "showCircles": True}],
|
|
"addTooltip": True, "addLegend": True, "legendPosition": "right", "times": [], "addTimeMarker": False,
|
|
},
|
|
"aggs": [
|
|
{"id": "1", "enabled": True, "type": "count", "schema": "metric", "params": {}},
|
|
{"id": "2", "enabled": True, "type": "date_histogram", "schema": "segment", "params": {"field": "@timestamp", "interval": "auto", "customInterval": "2h", "min_doc_count": 1, "extended_bounds": {}}},
|
|
{"id": "3", "enabled": True, "type": "terms", "schema": "group", "params": {"field": "rule.id", "size": 5, "order": "desc", "orderBy": "1", "otherBucket": False}},
|
|
],
|
|
},
|
|
},
|
|
# ── 5. Bar horizontal : top endpoints ciblés ─────────────────────────────
|
|
{
|
|
"id": "api-top-endpoints",
|
|
"title": "[API] Top Endpoints Cibles",
|
|
"type": "horizontal_bar",
|
|
"query": Q_WAF,
|
|
"visState": {
|
|
"type": "horizontal_bar",
|
|
"params": {
|
|
"type": "horizontal_bar",
|
|
"grid": {"categoryLines": False},
|
|
"categoryAxes": [{"id": "CategoryAxis-1", "type": "category", "position": "left", "show": True, "style": {}, "scale": {"type": "linear"}, "labels": {"show": True, "rotate": 0, "filter": True, "truncate": 250}, "title": {}}],
|
|
"valueAxes": [{"id": "ValueAxis-1", "name": "LeftAxis-1", "type": "value", "position": "bottom", "show": True, "style": {}, "scale": {"type": "linear", "mode": "normal"}, "labels": {"show": True, "rotate": 75, "filter": False, "truncate": 100}, "title": {"text": "Requetes"}}],
|
|
"seriesParams": [{"show": True, "type": "horizontal_bar", "mode": "normal", "data": {"label": "Requetes", "id": "1"}, "valueAxis": "ValueAxis-1", "drawLinesBetweenPoints": True, "showCircles": True}],
|
|
"addTooltip": True, "addLegend": True, "legendPosition": "right", "times": [], "addTimeMarker": False,
|
|
},
|
|
"aggs": [
|
|
{"id": "1", "enabled": True, "type": "count", "schema": "metric", "params": {}},
|
|
{"id": "2", "enabled": True, "type": "terms", "schema": "segment", "params": {"field": "data.transaction.request.uri", "size": 15, "order": "desc", "orderBy": "1", "otherBucket": True, "otherBucketLabel": "Autres", "missingBucket": False}},
|
|
],
|
|
},
|
|
},
|
|
# ── 6. Bar horizontal : top IPs attaquantes ───────────────────────────────
|
|
{
|
|
"id": "api-top-ips",
|
|
"title": "[API] Top IPs Attaquantes",
|
|
"type": "horizontal_bar",
|
|
"query": Q_ALL,
|
|
"visState": {
|
|
"type": "horizontal_bar",
|
|
"params": {
|
|
"type": "horizontal_bar",
|
|
"grid": {"categoryLines": False},
|
|
"categoryAxes": [{"id": "CategoryAxis-1", "type": "category", "position": "left", "show": True, "style": {}, "scale": {"type": "linear"}, "labels": {"show": True, "rotate": 0, "filter": True, "truncate": 200}, "title": {}}],
|
|
"valueAxes": [{"id": "ValueAxis-1", "name": "LeftAxis-1", "type": "value", "position": "bottom", "show": True, "style": {}, "scale": {"type": "linear", "mode": "normal"}, "labels": {"show": True, "rotate": 75, "filter": False, "truncate": 100}, "title": {"text": "Alertes"}}],
|
|
"seriesParams": [{"show": True, "type": "horizontal_bar", "mode": "normal", "data": {"label": "Alertes", "id": "1"}, "valueAxis": "ValueAxis-1", "drawLinesBetweenPoints": True, "showCircles": True}],
|
|
"addTooltip": True, "addLegend": True, "legendPosition": "right", "times": [], "addTimeMarker": False,
|
|
},
|
|
"aggs": [
|
|
{"id": "1", "enabled": True, "type": "count", "schema": "metric", "params": {}},
|
|
{"id": "2", "enabled": True, "type": "terms", "schema": "segment", "params": {"field": "data.transaction.client_ip", "size": 15, "order": "desc", "orderBy": "1", "otherBucket": False, "missingBucket": False}},
|
|
],
|
|
},
|
|
},
|
|
# ── 7. Pie : distribution codes HTTP ─────────────────────────────────────
|
|
{
|
|
"id": "api-http-codes",
|
|
"title": "[API] Distribution Codes HTTP",
|
|
"type": "pie",
|
|
"query": Q_WAF,
|
|
"visState": {
|
|
"type": "pie",
|
|
"params": {
|
|
"type": "pie", "addTooltip": True, "addLegend": True,
|
|
"legendPosition": "right", "isDonut": True,
|
|
"labels": {"show": True, "values": True, "last_level": True, "truncate": 100},
|
|
},
|
|
"aggs": [
|
|
{"id": "1", "enabled": True, "type": "count", "schema": "metric", "params": {}},
|
|
{"id": "2", "enabled": True, "type": "terms", "schema": "segment", "params": {"field": "data.transaction.response.http_code", "size": 10, "order": "desc", "orderBy": "1", "otherBucket": True, "otherBucketLabel": "Autres", "missingBucket": False}},
|
|
],
|
|
},
|
|
},
|
|
# ── 8. Pie : bloqué vs passé ──────────────────────────────────────────────
|
|
{
|
|
"id": "api-blocked-ratio",
|
|
"title": "[API] Bloque vs Alerte WAF",
|
|
"type": "pie",
|
|
"query": Q_WAF,
|
|
"visState": {
|
|
"type": "pie",
|
|
"params": {
|
|
"type": "pie", "addTooltip": True, "addLegend": True,
|
|
"legendPosition": "right", "isDonut": False,
|
|
"labels": {"show": True, "values": True, "last_level": True, "truncate": 100},
|
|
},
|
|
"aggs": [
|
|
{"id": "1", "enabled": True, "type": "count", "schema": "metric", "params": {}},
|
|
{"id": "2", "enabled": True, "type": "terms", "schema": "segment", "params": {"field": "data.transaction.is_interrupted", "size": 5, "order": "desc", "orderBy": "1", "otherBucket": False, "missingBucket": False}},
|
|
],
|
|
},
|
|
},
|
|
# ── 9. Table : top règles CRS déclenchées ────────────────────────────────
|
|
{
|
|
"id": "api-crs-rules",
|
|
"title": "[API] Top Regles CRS Declenchees",
|
|
"type": "table",
|
|
"query": Q_WAF,
|
|
"visState": {
|
|
"type": "table",
|
|
"params": {
|
|
"perPage": 15, "showPartialRows": False, "showMetricsAtAllLevels": False,
|
|
"sort": {"columnIndex": None, "direction": None}, "showTotal": False, "totalFunc": "sum",
|
|
},
|
|
"aggs": [
|
|
{"id": "1", "enabled": True, "type": "count", "schema": "metric", "params": {}},
|
|
{"id": "2", "enabled": True, "type": "terms", "schema": "bucket", "params": {"field": "data.transaction.messages.details.ruleId", "size": 15, "order": "desc", "orderBy": "1", "otherBucket": False, "missingBucket": False}},
|
|
{"id": "3", "enabled": True, "type": "terms", "schema": "bucket", "params": {"field": "data.transaction.messages.message", "size": 1, "order": "desc", "orderBy": "1", "otherBucket": False, "missingBucket": False}},
|
|
],
|
|
},
|
|
"uiStateJSON": json.dumps({"vis": {"params": {"sort": {"columnIndex": 0, "direction": "desc"}}}}),
|
|
},
|
|
]
|
|
|
|
# ── Layout dashboard (grille 48 colonnes) ─────────────────────────────────────
|
|
# Ligne 0 : 3 métriques (total | bloquées | brute-force)
|
|
# Ligne 6 : timeline pleine largeur
|
|
# Ligne 18: top endpoints (gauche) + top IPs (droite)
|
|
# Ligne 32: codes HTTP (gauche) + bloqué/passé (milieu) + règles CRS (droite)
|
|
PANELS = [
|
|
# Métriques
|
|
{"panelIndex": "1", "gridData": {"x": 0, "y": 0, "w": 16, "h": 6, "i": "1"}, "version": "2.19.5", "type": "visualization", "id": "api-waf-total", "embeddableConfig": {}},
|
|
{"panelIndex": "2", "gridData": {"x": 16, "y": 0, "w": 16, "h": 6, "i": "2"}, "version": "2.19.5", "type": "visualization", "id": "api-waf-blocked", "embeddableConfig": {}},
|
|
{"panelIndex": "3", "gridData": {"x": 32, "y": 0, "w": 16, "h": 6, "i": "3"}, "version": "2.19.5", "type": "visualization", "id": "api-bf-count", "embeddableConfig": {}},
|
|
# Timeline
|
|
{"panelIndex": "4", "gridData": {"x": 0, "y": 6, "w": 48, "h": 12, "i": "4"}, "version": "2.19.5", "type": "visualization", "id": "api-waf-timeline", "embeddableConfig": {}},
|
|
# Top endpoints + Top IPs
|
|
{"panelIndex": "5", "gridData": {"x": 0, "y": 18, "w": 28, "h": 16, "i": "5"}, "version": "2.19.5", "type": "visualization", "id": "api-top-endpoints", "embeddableConfig": {}},
|
|
{"panelIndex": "6", "gridData": {"x": 28, "y": 18, "w": 20, "h": 16, "i": "6"}, "version": "2.19.5", "type": "visualization", "id": "api-top-ips", "embeddableConfig": {}},
|
|
# Codes HTTP + bloqué/passé + règles CRS
|
|
{"panelIndex": "7", "gridData": {"x": 0, "y": 34, "w": 16, "h": 14, "i": "7"}, "version": "2.19.5", "type": "visualization", "id": "api-http-codes", "embeddableConfig": {}},
|
|
{"panelIndex": "8", "gridData": {"x": 16, "y": 34, "w": 16, "h": 14, "i": "8"}, "version": "2.19.5", "type": "visualization", "id": "api-blocked-ratio", "embeddableConfig": {}},
|
|
{"panelIndex": "9", "gridData": {"x": 32, "y": 34, "w": 16, "h": 14, "i": "9"}, "version": "2.19.5", "type": "visualization", "id": "api-crs-rules", "embeddableConfig": {}},
|
|
]
|
|
|
|
# ── Main ──────────────────────────────────────────────────────────────────────
|
|
def main():
|
|
print("=== Creation dashboard: Securite - API Backend & WAF ===\n")
|
|
|
|
for viz in VISUALIZATIONS:
|
|
vis_state = dict(viz["visState"])
|
|
vis_state["title"] = viz["title"]
|
|
body = {
|
|
"attributes": {
|
|
"title": viz["title"],
|
|
"visState": json.dumps(vis_state),
|
|
"uiStateJSON": viz.get("uiStateJSON", "{}"),
|
|
"description": "",
|
|
"kibanaSavedObjectMeta": {"searchSourceJSON": search_src(viz["query"])},
|
|
}
|
|
}
|
|
r = api("POST", f"/api/saved_objects/visualization/{viz['id']}?overwrite=true", body)
|
|
if "id" in r:
|
|
print(f" OK {viz['title']}")
|
|
else:
|
|
print(f" ERR {viz['title']}: {r}")
|
|
sys.exit(1)
|
|
|
|
dashboard_body = {
|
|
"attributes": {
|
|
"title": "Securite - API Backend et WAF",
|
|
"description": "ModSecurity WAF alertes et blocages, endpoints cibles, top IPs, codes HTTP, regles CRS",
|
|
"panelsJSON": json.dumps(PANELS),
|
|
"optionsJSON": json.dumps({"useMargins": True, "hidePanelTitles": False}),
|
|
"timeRestore": False,
|
|
"kibanaSavedObjectMeta": {
|
|
"searchSourceJSON": json.dumps({"query": {"language": "kuery", "query": ""}, "filter": []})
|
|
},
|
|
}
|
|
}
|
|
r = api("POST", "/api/saved_objects/dashboard/api-backend-waf-dashboard?overwrite=true", dashboard_body)
|
|
if "id" in r:
|
|
print(f"\n OK Dashboard: {r['id']}")
|
|
print(f"\n URL: https://<monitoring-ip>/#/app/dashboards#/view/{r['id']}")
|
|
else:
|
|
print(f"\n ERR Dashboard: {r}")
|
|
sys.exit(1)
|
|
|
|
print("\n=== Termine ===")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|