> 475
+        ioc_value=payload.ioc_value,
476
+        providers=providers,
477
+        request_payload=payload.model_dump(mode="json"),
478
+        response_payload=result,
479
+    )
480
+    return ApiResponse(data={"ioc": result})
481
+
482
+
483
+@app.post("/ioc/evaluate", response_model=ApiResponse)
484
+async def ioc_evaluate(payload: IocEvaluateRequest) -> ApiResponse:
485
+    providers = [p.lower().strip() for p in payload.providers]
486
+    supported = {"virustotal", "abuseipdb"}
487
+    requested = [p for p in providers if p in supported]
488
+    if not requested:
489
+        raise HTTPException(status_code=400, detail="No supported provider requested. Use ['virustotal'] or ['abuseipdb'].")
490
+
491
+    per_provider: dict[str, dict[str, object]] = {}
492
+    errors: dict[str, str] = {}
493
+
494
+    if "virustotal" in requested:
495
+        try:
496
+            vt = await virustotal_adapter.enrich_ioc(payload.ioc_type, payload.ioc_value)
497
+            vt_result, _, _, _ = _build_vt_ioc_result(
498
+                vt=vt,
499
+                ioc_type=payload.ioc_type,
500
+                ioc_value=payload.ioc_value,
501
+                malicious_threshold=payload.malicious_threshold,
502
+                suspicious_threshold=payload.suspicious_threshold,
503
+            )
504
+            per_provider["virustotal"] = vt_result
505
+        except Exception as exc:
506
+            errors["virustotal"] = str(exc)
507
+
508
+    if "abuseipdb" in requested:
509
+        if payload.ioc_type != "ip":
510
+            errors["abuseipdb"] = "AbuseIPDB supports ioc_type='ip' only"
511
+        else:
512
+            try:
513
+                abuse = await abuseipdb_adapter.check_ip(payload.ioc_value)
514
+                abuse_result, _, _, _ = _build_abuseipdb_ioc_result(
515
+                    abuse=abuse,
516
+                    ioc_value=payload.ioc_value,
517
+                    confidence_threshold=50,
518
+                )
519
+                per_provider["abuseipdb"] = abuse_result
520
+            except Exception as exc:
521
+                errors["abuseipdb"] = str(exc)
522
+
523
+    if not per_provider:
524
+        repo.add_ioc_trace(
525
+            action="evaluate",
526
+            ioc_type=payload.ioc_type,
527
+            ioc_value=payload.ioc_value,
528
+            providers=requested,
529
+            request_payload=payload.model_dump(mode="json"),
530
+            response_payload={},
531
+            error=str(errors),
532
+        )
533
+        raise HTTPException(status_code=502, detail=f"Provider evaluation failed: {errors}")
534
+
535
+    # aggregate decision (max confidence/severity, matched if any provider matched)
536
+    order = {"low": 1, "medium": 2, "high": 3, "critical": 4}
537
+    matched = any(bool(r.get("matched")) for r in per_provider.values())
538
+    confidence = max(float(r.get("confidence", 0.0) or 0.0) for r in per_provider.values())
539
+    severity = max((str(r.get("severity", "low")) for r in per_provider.values()), key=lambda x: order.get(x, 1))
540
+    reason_parts = [f"{name}:{res.get('reason','')}" for name, res in per_provider.items()]
541
+    if errors:
542
+        reason_parts.append(f"errors={errors}")
543
+    ioc_result = {
544
+        "ioc_type": payload.ioc_type,
545
+        "ioc_value": payload.ioc_value,
546
+        "matched": matched,
547
+        "severity": severity,
548
+        "confidence": round(confidence, 4),
549
+        "reason": " | ".join(reason_parts),
550
+        "providers": per_provider,
551
+    }
552
+
553
+    repo.add_ioc_trace(
554
+        action="evaluate",
555
+        ioc_type=payload.ioc_type,
556
+        ioc_value=payload.ioc_value,
557
+        providers=providers,
558
+        request_payload=payload.model_dump(mode="json"),
559
+        response_payload=ioc_result,
560
+        matched=matched,
561
+        severity=severity,
562
+        confidence=float(ioc_result["confidence"]),
563
+    )
564
+
565
+    return ApiResponse(data={"ioc": ioc_result})
566
+
567
+
568
+@app.post("/ioc/upload-file", response_model=ApiResponse)
569
+async def ioc_upload_file(file: UploadFile = File(...)) -> ApiResponse:
570
+    content = await file.read()
571
+    if not content:
572
+        raise HTTPException(status_code=400, detail="Uploaded file is empty")
573
+    try:
574
+        vt_upload = await virustotal_adapter.upload_file(file.filename or "upload.bin", content)
575
+    except Exception as exc:
576
+        repo.add_ioc_trace(
577
+            action="upload_file",
578
+            ioc_type="hash",
579
+            ioc_value=file.filename or "<unknown>",
580
+            providers=["virustotal"],
581
+            request_payload={"filename": file.filename, "size": len(content)},
582
+            response_payload={},
583
+            error=str(exc),
584
+        )
585
+        raise HTTPException(status_code=502, detail=f"VirusTotal upload failed: {exc}") from exc
586
+
587
+    repo.add_ioc_trace(
588
+        action="upload_file",
589
+        ioc_type="hash",
590
+        ioc_value=file.filename or "<unknown>",
591
+        providers=["virustotal"],
592
+        request_payload={"filename": file.filename, "size": len(content)},
593
+        response_payload=vt_upload if isinstance(vt_upload, dict) else {"raw": str(vt_upload)},
594
+    )
595
+    return ApiResponse(data={"virustotal": vt_upload})
596
+
597
+
598
+@app.get("/ioc/analysis/{analysis_id}", response_model=ApiResponse)
599
+async def ioc_get_analysis(analysis_id: str) -> ApiResponse:
600
+    try:
601
+        vt_analysis = await virustotal_adapter.get_analysis(analysis_id)
602
+    except Exception as exc:
603
+        repo.add_ioc_trace(
604
+            action="analysis",
605
+            ioc_type="hash",
606
+            ioc_value=analysis_id,
607
+            providers=["virustotal"],
608
+            request_payload={"analysis_id": analysis_id},
609
+            response_payload={},
610
+            error=str(exc),
611
+        )
612
+        raise HTTPException(status_code=502, detail=f"VirusTotal analysis fetch failed: {exc}") from exc
613
+
614
+    repo.add_ioc_trace(
615
+        action="analysis",
616
+        ioc_type="hash",
617
+        ioc_value=analysis_id,
618
+        providers=["virustotal"],
619
+        request_payload={"analysis_id": analysis_id},
620
+        response_payload=vt_analysis if isinstance(vt_analysis, dict) else {"raw": str(vt_analysis)},
621
+    )
622
+    return ApiResponse(data={"virustotal": vt_analysis})
623
+
624
+
625
+@app.post("/ioc/evaluate-file", response_model=ApiResponse)
626
+async def ioc_evaluate_file(
627
+    file: UploadFile = File(...),
628
+    malicious_threshold: int = 1,
629
+    suspicious_threshold: int = 3,
630
+    poll_timeout_seconds: int = 30,
631
+    poll_interval_seconds: int = 2,
632
+) -> ApiResponse:
633
+    content = await file.read()
634
+    if not content:
635
+        raise HTTPException(status_code=400, detail="Uploaded file is empty")
636
+
637
+    try:
638
+        vt_upload = await virustotal_adapter.upload_file(file.filename or "upload.bin", content)
639
+    except Exception as exc:
640
+        repo.add_ioc_trace(
641
+            action="evaluate_file",
642
+            ioc_type="hash",
643
+            ioc_value=file.filename or "<unknown>",
644
+            providers=["virustotal"],
645
+            request_payload={"filename": file.filename, "size": len(content)},
646
+            response_payload={},
647
+            error=str(exc),
648
+        )
649
+        raise HTTPException(status_code=502, detail=f"VirusTotal upload failed: {exc}") from exc
650
+
651
+    analysis_id = (
652
+        (((vt_upload.get("data") or {}).get("id")) if isinstance(vt_upload, dict) else None)
653
+        or ""
654
+    )
655
+    if not analysis_id:
656
+        raise HTTPException(status_code=502, detail="VirusTotal upload response missing analysis ID")
657
+
658
+    timeout = max(1, poll_timeout_seconds)
659
+    interval = max(1, poll_interval_seconds)
660
+    elapsed = 0
661
+    analysis: dict[str, object] = {}
662
+    while elapsed <= timeout:
663
+        analysis = await virustotal_adapter.get_analysis(analysis_id)
664
+        status = (
665
+            (((analysis.get("data") or {}).get("attributes") or {}).get("status"))
666
+            if isinstance(analysis, dict)
667
+            else None
668
+        )
669
+        if status == "completed":
670
+            break
671
+        await asyncio.sleep(interval)
672
+        elapsed += interval
673
+
674
+    sha256 = (
675
+        (((analysis.get("meta") or {}).get("file_info") or {}).get("sha256"))
676
+        if isinstance(analysis, dict)
677
+        else None
678
+    )
679
+    if not sha256:
680
+        raise HTTPException(status_code=502, detail="VirusTotal analysis did not return file hash yet")
681
+
682
+    try:
683
+        vt_file = await virustotal_adapter.enrich_ioc("hash", str(sha256))
684
+    except Exception as exc:
685
+        repo.add_ioc_trace(
686
+            action="evaluate_file",
687
+            ioc_type="hash",
688
+            ioc_value=str(sha256),
689
+            providers=["virustotal"],
690
+            request_payload={"filename": file.filename, "analysis_id": analysis_id},
691
+            response_payload={"upload": vt_upload, "analysis": analysis},
692
+            error=str(exc),
693
+        )
694
+        raise HTTPException(status_code=502, detail=f"VirusTotal report fetch failed: {exc}") from exc
695
+
696
+    ioc_result, matched, severity, confidence = _build_vt_ioc_result(
697
+        vt=vt_file,
698
+        ioc_type="hash",
699
+        ioc_value=str(sha256),
700
+        malicious_threshold=malicious_threshold,
701
+        suspicious_threshold=suspicious_threshold,
702
+    )
703
+    ioc_result["analysis_id"] = analysis_id
704
+    ioc_result["filename"] = file.filename
705
+
706
+    repo.add_ioc_trace(
707
+        action="evaluate_file",
708
+        ioc_type="hash",
709
+        ioc_value=str(sha256),
710
+        providers=["virustotal"],
711
+        request_payload={"filename": file.filename, "analysis_id": analysis_id},
712
+        response_payload={
713
+            "upload": vt_upload,
714
+            "analysis": analysis,
715
+            "ioc": ioc_result,
716
+        },
717
+        matched=matched,
718
+        severity=severity,
719
+        confidence=confidence,
720
+    )
721
+    return ApiResponse(data={"ioc": ioc_result, "analysis": analysis, "upload": vt_upload})
722
+
723
+
724
+@app.get("/ioc/history", response_model=ApiResponse)
725
+async def ioc_history(limit: int = 50, offset: int = 0) -> ApiResponse:
726
+    return ApiResponse(data={"items": repo.list_ioc_trace(limit=limit, offset=offset)})
727
+
728
+
300 729
 @app.get("/sync/wazuh-version", response_model=ApiResponse)
301 730
 async def sync_wazuh_version() -> ApiResponse:
302 731
     try:

+ 22 - 0
soc-integrator/app/models.py

@@ -25,6 +25,28 @@ class ActionCreateIncidentRequest(BaseModel):
25 25
     payload: dict[str, Any] = Field(default_factory=dict)
26 26
 
27 27
 
28
+class IrisTicketCreateRequest(BaseModel):
29
+    title: str
30
+    description: str = "Created by soc-integrator"
31
+    case_customer: int | None = None
32
+    case_soc_id: str | None = None
33
+    payload: dict[str, Any] = Field(default_factory=dict)
34
+
35
+
36
+class IocEnrichRequest(BaseModel):
37
+    ioc_type: Literal["domain", "ip", "hash", "url"]
38
+    ioc_value: str
39
+    providers: list[str] = Field(default_factory=lambda: ["virustotal"])
40
+
41
+
42
+class IocEvaluateRequest(BaseModel):
43
+    ioc_type: Literal["domain", "ip", "hash", "url"]
44
+    ioc_value: str
45
+    providers: list[str] = Field(default_factory=lambda: ["virustotal"])
46
+    malicious_threshold: int = 1
47
+    suspicious_threshold: int = 3
48
+
49
+
28 50
 class TriggerShuffleRequest(BaseModel):
29 51
     workflow_id: str
30 52
     execution_argument: dict[str, Any] = Field(default_factory=dict)

+ 49 - 0
soc-integrator/app/repositories/mvp_repo.py

@@ -151,3 +151,52 @@ class MvpRepository:
151 151
                 """,
152 152
                 (incident_key, status_code, success, response_excerpt),
153 153
             )
154
+
155
+    def add_ioc_trace(
156
+        self,
157
+        action: str,
158
+        ioc_type: str,
159
+        ioc_value: str,
160
+        providers: list[str],
161
+        request_payload: dict[str, Any],
162
+        response_payload: dict[str, Any],
163
+        matched: bool | None = None,
164
+        severity: str | None = None,
165
+        confidence: float | None = None,
166
+        error: str | None = None,
167
+    ) -> None:
168
+        with get_conn() as conn, conn.cursor() as cur:
169
+            cur.execute(
170
+                """
171
+                INSERT INTO ioc_trace(
172
+                  action, ioc_type, ioc_value, providers,
173
+                  request_payload, response_payload, matched, severity, confidence, error
174
+                )
175
+                VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
176
+                """,
177
+                (
178
+                    action,
179
+                    ioc_type,
180
+                    ioc_value,
181
+                    Json(providers),
182
+                    Json(request_payload),
183
+                    Json(response_payload),
184
+                    matched,
185
+                    severity,
186
+                    confidence,
187
+                    error,
188
+                ),
189
+            )
190
+
191
+    def list_ioc_trace(self, limit: int = 50, offset: int = 0) -> list[dict[str, Any]]:
192
+        with get_conn() as conn, conn.cursor() as cur:
193
+            cur.execute(
194
+                """
195
+                SELECT id, action, ioc_type, ioc_value, providers, matched, severity, confidence, error, created_at
196
+                FROM ioc_trace
197
+                ORDER BY created_at DESC
198
+                LIMIT %s OFFSET %s
199
+                """,
200
+                (max(1, limit), max(0, offset)),
201
+            )
202
+            return [dict(row) for row in cur.fetchall()]

+ 19 - 0
soc-integrator/examples/README.md

@@ -25,3 +25,22 @@ This script demonstrates:
25 25
 
26 26
 1. Direct call to `IRIS /api/v2/cases`
27 27
 2. Call through `soc-integrator /action/create-iris-case`
28
+
29
+## Send sample event to Shuffle webhook
30
+
31
+Use this helper with the sample workflow:
32
+
33
+- `/Users/simplicoltd./projects/soc/shuffle-workflows/sample-webhook-soc-integrator-iris-workflow.json`
34
+
35
+Run:
36
+
37
+```bash
38
+SHUFFLE_WEBHOOK_URL='http://localhost:3001/api/v1/hooks/webhook_xxx' \
39
+bash soc-integrator/examples/send_to_shuffle_webhook.sh
40
+```
41
+
42
+Environment variables:
43
+
44
+- `SHUFFLE_WEBHOOK_URL` (required)
45
+- `INTEGRATOR_URL` (default: `http://localhost:8088`)
46
+- `INTERNAL_KEY` (optional)

+ 29 - 0
soc-integrator/examples/send_to_shuffle_webhook.sh

@@ -0,0 +1,29 @@
1
+#!/usr/bin/env bash
2
+set -euo pipefail
3
+
4
+SHUFFLE_WEBHOOK_URL="${SHUFFLE_WEBHOOK_URL:-}"
5
+INTEGRATOR_URL="${INTEGRATOR_URL:-http://localhost:8088}"
6
+INTERNAL_KEY="${INTERNAL_KEY:-}"
7
+
8
+if [[ -z "${SHUFFLE_WEBHOOK_URL}" ]]; then
9
+  echo "error: SHUFFLE_WEBHOOK_URL is required"
10
+  echo "example:"
11
+  echo "  SHUFFLE_WEBHOOK_URL='http://localhost:3001/api/v1/hooks/webhook_...' \\"
12
+  echo "  bash soc-integrator/examples/send_to_shuffle_webhook.sh"
13
+  exit 1
14
+fi
15
+
16
+curl -sS -X POST "${SHUFFLE_WEBHOOK_URL}" \
17
+  -H "Content-Type: application/json" \
18
+  -d "{
19
+    \"event_id\": \"soc-integrator-test-$(date +%s)\",
20
+    \"source\": \"soc-integrator\",
21
+    \"severity\": \"high\",
22
+    \"title\": \"Suspicious VPN login outside Thailand\",
23
+    \"description\": \"Detected by soc-integrator test script\",
24
+    \"integrator_url\": \"${INTEGRATOR_URL}\",
25
+    \"internal_key\": \"${INTERNAL_KEY}\"
26
+  }"
27
+
28
+echo
29
+echo "sent webhook payload to Shuffle"

+ 1 - 0
soc-integrator/requirements.txt

@@ -3,3 +3,4 @@ uvicorn==0.35.0
3 3
 httpx==0.28.1
4 4
 pydantic-settings==2.10.1
5 5
 psycopg[binary]==3.2.1
6
+python-multipart==0.0.20

+ 0 - 6
wazuh-docker/.env

@@ -1,6 +0,0 @@
1
-WAZUH_VERSION=4.14.3
2
-WAZUH_IMAGE_VERSION=4.14.3
3
-WAZUH_TAG_REVISION=1
4
-FILEBEAT_TEMPLATE_BRANCH=4.14.3
5
-WAZUH_FILEBEAT_MODULE=wazuh-filebeat-0.5.tar.gz
6
-WAZUH_UI_REVISION=1

tum/sflite - Gogs: Simplico Git Service

Nenhuma Descrição

.java-version 3B

12
  1. 17