ol-117"> 232
+        if isinstance(nested, dict):
233
+            merged = dict(nested)
234
+            merged.update(flat)
235
+            flat = merged
236
+
237
+        confidence = self._to_float(flat.get("confidence"), 0.0)
238
+        matched_raw = flat.get("matched")
239
+        if isinstance(matched_raw, bool):
240
+            matched = matched_raw
241
+            reason = "shuffle_explicit"
242
+        else:
243
+            matched = confidence >= 0.7
244
+            reason = "confidence_threshold_fallback"
245
+
246
+        severity_raw = str(flat.get("severity", "")).lower()
247
+        severity = severity_raw if severity_raw in {"low", "medium", "high", "critical"} else self._severity_from_confidence(confidence)
248
+
249
+        return {
250
+            "matched": matched,
251
+            "confidence": confidence,
252
+            "severity": severity,
253
+            "evidence": str(flat.get("evidence", "")),
254
+            "iocs": flat.get("iocs", []),
255
+            "reason": reason,
256
+            "raw": shuffle_result,
257
+        }
258
+
115 259
     async def ingest_incident(self, event: dict[str, Any]) -> dict[str, Any]:
116 260
         policy = self.repo.get_policy()
117 261
         incident_key = self._incident_key(event)
@@ -233,27 +377,48 @@ class MvpService:
233 377
         policy = self.repo.get_policy()
234 378
         workflow_id = str(policy.get("shuffle", {}).get("ioc_workflow_id", "")).strip()
235 379
 
236
-        matched = True
237
-        confidence = 0.7
238 380
         shuffle_result: dict[str, Any] | None = None
239 381
 
240 382
         if workflow_id:
241 383
             shuffle_result = await self.shuffle_adapter.trigger_workflow(workflow_id, payload)
384
+        verdict = self._extract_shuffle_verdict(shuffle_result)
385
+        matched = bool(verdict["matched"])
386
+        confidence = self._to_float(verdict["confidence"], 0.0)
387
+
388
+        logger.info(
389
+            "ioc evaluation workflow_id=%s matched=%s confidence=%.2f",
390
+            workflow_id or "<none>",
391
+            matched,
392
+            confidence,
393
+        )
242 394
 
243 395
         if matched:
396
+            src_event = payload.get("source_event", {})
397
+            event_id = src_event.get("event_id") or f"ioc-{int(time.time())}"
398
+            if not isinstance(event_id, str):
399
+                event_id = str(event_id)
400
+
401
+            description = f"IOC evaluation result confidence={confidence:.2f}"
402
+            evidence = str(verdict.get("evidence", "")).strip()
403
+            if evidence:
404
+                description = f"{description} evidence={evidence[:180]}"
405
+
244 406
             event = {
245 407
                 "source": "shuffle",
246 408
                 "event_type": "ioc_dns" if payload.get("ioc_type") == "domain" else "ioc_ips",
247
-                "event_id": payload.get("source_event", {}).get("event_id") or f"ioc-{int(time.time())}",
409
+                "event_id": event_id,
248 410
                 "timestamp": datetime.now(timezone.utc).isoformat(),
249
-                "severity": "medium",
411
+                "severity": verdict["severity"],
250 412
                 "title": f"IOC match: {payload.get('ioc_value', 'unknown')}",
251
-                "description": "IOC evaluation result",
252
-                "asset": payload.get("source_event", {}).get("asset", {}),
253
-                "network": payload.get("source_event", {}).get("network", {}),
413
+                "description": description,
414
+                "asset": src_event.get("asset", {}),
415
+                "network": src_event.get("network", {}),
254 416
                 "tags": ["ioc", str(payload.get("ioc_type", "unknown"))],
255 417
                 "risk_context": {},
256
-                "raw": payload,
418
+                "raw": {
419
+                    "payload": payload,
420
+                    "shuffle": verdict.get("raw"),
421
+                },
257 422
                 "payload": {},
258 423
             }
259 424
             ingest_result = await self.ingest_incident(event)
@@ -263,6 +428,10 @@ class MvpService:
263 428
         return {
264 429
             "matched": matched,
265 430
             "confidence": confidence,
431
+            "severity": verdict["severity"],
432
+            "evidence": verdict["evidence"],
433
+            "iocs": verdict["iocs"],
434
+            "decision_source": verdict["reason"],
266 435
             "shuffle": shuffle_result,
267 436
             "result": ingest_result,
268 437
         }
@@ -310,6 +479,83 @@ class MvpService:
310 479
             "escalation_stub_sent": ingest_result.get("escalation_stub_sent", False),
311 480
         }
312 481
 
482
+    async def sync_wazuh_alerts(
483
+        self,
484
+        query: str = "soc_mvp_test=true OR event_type:*",
485
+        limit: int = 50,
486
+        minutes: int = 120,
487
+    ) -> dict[str, Any]:
488
+        raw = await self.wazuh_adapter.search_alerts(query=query, limit=limit, minutes=minutes)
489
+        hits = (raw.get("hits", {}) or {}).get("hits", []) if isinstance(raw, dict) else []
490
+
491
+        processed = 0
492
+        ingested = 0
493
+        skipped_existing = 0
494
+        failed = 0
495
+        errors: list[str] = []
496
+        created_incidents: list[str] = []
497
+        ioc_evaluated = 0
498
+        ioc_matched = 0
499
+        ioc_rejected = 0
500
+
501
+        for hit in hits:
502
+            processed += 1
503
+            event = self._normalize_wazuh_hit(hit)
504
+            event_id = str(event.get("event_id", "")).strip()
505
+            if event_id and self.repo.has_event("wazuh", event_id):
506
+                skipped_existing += 1
507
+                continue
508
+            try:
509
+                if event.get("event_type") in {"ioc_dns", "ioc_ips"}:
510
+                    ioc_evaluated += 1
511
+                    payload = {
512
+                        "ioc_type": "domain" if event.get("event_type") == "ioc_dns" else "ip",
513
+                        "ioc_value": (event.get("network", {}) or {}).get("domain")
514
+                        or (event.get("network", {}) or {}).get("dst_ip")
515
+                        or (event.get("network", {}) or {}).get("src_ip")
516
+                        or "unknown",
517
+                        "source_event": {
518
+                            "event_id": event.get("event_id"),
519
+                            "asset": event.get("asset", {}),
520
+                            "network": event.get("network", {}),
521
+                            "raw": event.get("raw", {}),
522
+                        },
523
+                    }
524
+                    ioc_result = await self.evaluate_ioc(payload)
525
+                    if ioc_result.get("matched"):
526
+                        ioc_matched += 1
527
+                        ingested += 1
528
+                        incident_key = str((ioc_result.get("result", {}) or {}).get("incident_key", ""))
529
+                        if incident_key:
530
+                            created_incidents.append(incident_key)
531
+                    else:
532
+                        ioc_rejected += 1
533
+                else:
534
+                    result = await self.ingest_incident(event)
535
+                    ingested += 1
536
+                    incident_key = str(result.get("incident_key", ""))
537
+                    if incident_key:
538
+                        created_incidents.append(incident_key)
539
+            except Exception as exc:
540
+                failed += 1
541
+                errors.append(f"{event_id or 'unknown_event'}: {exc}")
542
+
543
+        return {
544
+            "query": query,
545
+            "window_minutes": minutes,
546
+            "limit": limit,
547
+            "processed": processed,
548
+            "ingested": ingested,
549
+            "skipped_existing": skipped_existing,
550
+            "failed": failed,
551
+            "ioc_evaluated": ioc_evaluated,
552
+            "ioc_matched": ioc_matched,
553
+            "ioc_rejected": ioc_rejected,
554
+            "incident_keys": created_incidents,
555
+            "errors": errors[:10],
556
+            "total_hits": (raw.get("hits", {}).get("total", {}) if isinstance(raw, dict) else {}),
557
+        }
558
+
313 559
     async def dependency_health(self) -> dict[str, Any]:
314 560
         out: dict[str, Any] = {}
315 561
 

+ 7 - 0
wazuh-docker/single-node/config/wazuh_cluster/local_rules.xml

@@ -0,0 +1,7 @@
1
+<group name="soc_mvp_test,">
2
+  <rule id="100200" level="10">
3
+    <match>soc_mvp_test=true</match>
4
+    <description>SOC MVP synthetic test event detected</description>
5
+    <group>soc_mvp_test,syslog,</group>
6
+  </rule>
7
+</group>

+ 6 - 0
wazuh-docker/single-node/config/wazuh_cluster/wazuh_manager.conf

@@ -30,6 +30,12 @@
30 30
     <protocol>tcp</protocol>
31 31
     <queue_size>131072</queue_size>
32 32
   </remote>
33
+  <remote>
34
+    <connection>syslog</connection>
35
+    <port>514</port>
36
+    <protocol>udp</protocol>
37
+    <allowed-ips>0.0.0.0/0</allowed-ips>
38
+  </remote>
33 39
 
34 40
   <!-- Policy monitoring -->
35 41
   <rootcheck>

+ 2 - 1
wazuh-docker/single-node/docker-compose.yml

@@ -41,7 +41,8 @@ services:
41 41
       - ./config/wazuh_indexer_ssl_certs/root-ca-manager.pem:/etc/ssl/root-ca.pem
42 42
       - ./config/wazuh_indexer_ssl_certs/wazuh.manager.pem:/etc/ssl/filebeat.pem
43 43
       - ./config/wazuh_indexer_ssl_certs/wazuh.manager-key.pem:/etc/ssl/filebeat.key
44
-      - ./config/wazuh_cluster/wazuh_manager.conf:/wazuh-config-mount/etc/ossec.conf
44
+      - ./config/wazuh_cluster/wazuh_manager.conf:/var/ossec/etc/ossec.conf
45
+      - ./config/wazuh_cluster/local_rules.xml:/var/ossec/etc/rules/local_rules.xml
45 46
 
46 47
   wazuh.indexer:
47 48
     image: wazuh/wazuh-indexer:4.14.3

tum/soc - Gogs: Simplico Git Service

Geen omschrijving