Custom default settings (#165)
* Update error message for existing customer provisioning default settings * office365 exchange alert creation * file rename * updated dependencies * default settings response * office365 threat intel exchange alert creation * Fix async bug in delete_customer_provisioning_default_settings function * added CustomerDefaultSettingForm * bug fix * updated customer-provision-wizard * precommit fixes --------- Co-authored-by: Davide Di Modica <webmaster.ddm@gmail.com>
taylor_socfortress committed
Feb 23, 2024 at 16:04 UTC
fdef46e64a9fb58d0a70e2a9a5cb4d1a5cbfcee3
22 files changed
+1329
-195
.gitignore
+2
@@ -53,3 +53,5 @@ unplugin.components.d.ts
53
package-lock.json
54
*.checkpoint
55
firewall.spec
56
+
57
+frontend/cypress/screenshots/
.vscode/settings.json
+2
@@ -8,6 +8,7 @@
8
"datetimesec",
9
"echarts",
10
"firedtimes",
11
+ "forgotpassword",
12
"Healthcheck",
13
"healthchecks",
14
"majesticons",
@@ -16,6 +17,7 @@
17
"picocolors",
18
"redoc",
19
"rushstack",
20
+ "signin",
21
"Socfortress",
22
"sparkline",
23
"taze",
backend/app/customer_provisioning/routes/default_settings.py
+20
-8
@@ -46,7 +46,7 @@ async def get_all_customer_provisioning_default_settings(
46
47
@customer_provisioning_default_settings_router.post(
48
"/default_settings",
49
- response_model=CustomerProvisioningDefaultSettings,
49
+ response_model=CustomerProvisioningDefaultSettingsResponse,
50
description="Create a new default settings for customer provisioning",
51
dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
52
)
@@ -60,17 +60,21 @@ async def create_customer_provisioning_default_settings(
60
existing_settings = result.scalars().first()
61
62
if existing_settings:
63
- raise HTTPException(status_code=400, detail="Only one settings entry is allowed")
63
+ raise HTTPException(status_code=400, detail="Only one customer provisioning default settings can exist")
64
65
db.add(customer_provisioning_default_settings)
66
await db.commit()
67
await db.refresh(customer_provisioning_default_settings)
68
- return customer_provisioning_default_settings
68
+ return CustomerProvisioningDefaultSettingsResponse(
69
+ message="Customer Provisioning Default Settings created successfully",
70
+ success=True,
71
+ customer_provisioning_default_settings=customer_provisioning_default_settings,
72
+ )
73
74
75
@customer_provisioning_default_settings_router.put(
76
"/default_settings",
73
- response_model=CustomerProvisioningDefaultSettings,
77
+ response_model=CustomerProvisioningDefaultSettingsResponse,
78
description="Update default settings for customer provisioning",
79
dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
80
)
@@ -94,12 +98,16 @@ async def update_customer_provisioning_default_settings(
98
99
await db.commit()
100
await db.refresh(existing_settings)
97
- return existing_settings
101
+ return CustomerProvisioningDefaultSettingsResponse(
102
+ message="Customer Provisioning Default Settings updated successfully",
103
+ success=True,
104
+ customer_provisioning_default_settings=existing_settings,
105
+ )
106
107
108
@customer_provisioning_default_settings_router.delete(
109
"/default_settings",
102
- response_model=CustomerProvisioningDefaultSettings,
110
+ response_model=CustomerProvisioningDefaultSettingsResponse,
111
description="Delete default settings for customer provisioning",
112
dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
113
)
@@ -113,6 +121,10 @@ async def delete_customer_provisioning_default_settings(
121
if not existing_settings:
122
raise HTTPException(status_code=404, detail="Settings not found")
123
116
- db.delete(existing_settings)
124
+ await db.delete(existing_settings)
125
await db.commit()
118
- return existing_settings
126
+ return CustomerProvisioningDefaultSettingsResponse(
127
+ message="Customer Provisioning Default Settings deleted successfully",
128
+ success=True,
129
+ customer_provisioning_default_settings=existing_settings,
130
+ )
backend/app/integrations/monitoring_alert/routes/monitoring_alert.py
+61
-1
@@ -26,9 +26,12 @@ from app.integrations.monitoring_alert.schema.monitoring_alert import (
26
from app.integrations.monitoring_alert.schema.monitoring_alert import (
27
MonitoringWazuhAlertsRequestModel,
28
)
29
-from app.integrations.monitoring_alert.services.office365 import (
29
+from app.integrations.monitoring_alert.services.office365_exchange import (
30
analyze_office365_exchange_online_alerts,
31
)
32
+from app.integrations.monitoring_alert.services.office365_threatintel import (
33
+ analyze_office365_threatintel_alerts,
34
+)
35
from app.integrations.monitoring_alert.services.suricata import analyze_suricata_alerts
36
from app.integrations.monitoring_alert.services.wazuh import analyze_wazuh_alerts
37
from app.integrations.sap_siem.services.sap_siem_multiple_logins import (
@@ -122,6 +125,15 @@ async def create_monitoring_alert(
125
)
126
customer_meta = customer_meta.scalars().first()
127
128
+ if not customer_meta:
129
+ logger.info(f"Getting customer meta for customer_meta_office365_organization_id: {monitoring_alert.event.fields.CUSTOMER_CODE}")
130
+ customer_meta = await session.execute(
131
+ select(CustomersMeta).where(
132
+ CustomersMeta.customer_meta_office365_organization_id == monitoring_alert.event.fields.CUSTOMER_CODE,
133
+ ),
134
+ )
135
+ customer_meta = customer_meta.scalars().first()
136
+
137
if not customer_meta:
138
raise HTTPException(status_code=404, detail="Customer not found")
139
@@ -293,6 +305,54 @@ async def run_office365_exchange_online_analysis(
305
)
306
307
308
+@monitoring_alerts_router.post(
309
+ "/run_analysis/office365/threat_intel",
310
+ response_model=AlertAnalysisResponse,
311
+)
312
+async def run_office365_threat_intel_analysis(
313
+ request: MonitoringWazuhAlertsRequestModel,
314
+ session: AsyncSession = Depends(get_db),
315
+) -> AlertAnalysisResponse:
316
+ """
317
+ This route is used to run analysis on the monitoring alerts.
318
+
319
+ 1. Get all the monitoring alerts from the database where the customer_code matches the customer_code provided
320
+ and the alert_source is OFFICE365_THREAT_INTEL.
321
+
322
+ 2. Call the analyze_office365_threatintel_alerts function to analyze the alerts.
323
+
324
+ Args:
325
+ request (MonitoringWazuhAlertsRequestModel): The customer code.
326
+ session (AsyncSession, optional): The database session. Defaults to Depends(get_db).
327
+
328
+ Returns:
329
+ WazuhAnalysisResponse: The response containing the analysis results.
330
+ """
331
+ logger.info(f"Running analysis for customer_code: {request.customer_code}")
332
+
333
+ customer_meta = await get_customer_meta(request.customer_code, session)
334
+
335
+ monitoring_alerts = await session.execute(
336
+ select(MonitoringAlerts).where(
337
+ (MonitoringAlerts.customer_code == request.customer_code) & (MonitoringAlerts.alert_source == "OFFICE365_THREAT_INTEL"),
338
+ ),
339
+ )
340
+ monitoring_alerts = monitoring_alerts.scalars().all()
341
+
342
+ logger.info(f"Found {len(monitoring_alerts)} monitoring alerts")
343
+
344
+ if not monitoring_alerts:
345
+ raise HTTPException(status_code=404, detail="No monitoring alerts found")
346
+
347
+ # Call the analyze_office365_threatintel_alerts function to analyze the alerts
348
+ await analyze_office365_threatintel_alerts(monitoring_alerts, customer_meta, session)
349
+
350
+ return AlertAnalysisResponse(
351
+ success=True,
352
+ message="Analysis completed successfully",
353
+ )
354
+
355
+
356
@monitoring_alerts_router.post(
357
"/run_analysis/sap_siem/suspicious_logins",
358
response_model=AlertAnalysisResponse,
backend/app/integrations/monitoring_alert/schema/monitoring_alert.py
+188
-59
@@ -547,15 +547,16 @@ class SuricataIrisAlertPayload(BaseModel):
547
return self.dict(exclude_none=True)
548
549
550
-########### ! Office365 ALERTS SCHEMA ! ###########
551
-class Office365SourceModel(BaseModel):
552
- alert_signature: str = Field(..., description="Signature of the alert")
553
- alert_severity: int = Field(..., description="Severity level of the alert")
554
- alert_signature_id: int = Field(..., description="Signature ID of the alert")
555
- src_ip: str = Field(..., description="Source IP address")
556
- dest_ip: str = Field(..., description="Destination IP address")
557
- app_proto: str = Field(..., description="Application protocol")
558
- agent_labels_customer: str = Field(..., description="Customer of the agent")
550
+########### ! Office365 Exchange ALERTS SCHEMA ! ###########
551
+class Office365ExchangeSourceModel(BaseModel):
552
+ client_ip: Optional[str] = Field("Not found", description="Client IP address")
553
+ operation: Optional[str] = Field("Not found", description="Operation")
554
+ creation_time: Optional[str] = Field("Not found", description="Creation time")
555
+ office365_id: str = Field(..., description="Office365 ID")
556
+ organization_name: str = Field(..., description="Organization name")
557
+ user_id: str = Field(..., description="User ID")
558
+ workload: str = Field(..., description="Workload")
559
+ organization_id: str = Field(..., description="Organization ID")
560
timestamp: str = Field(..., description="The timestamp of the alert.")
561
timestamp_utc: Optional[str] = Field(
562
...,
@@ -569,11 +570,10 @@ class Office365SourceModel(BaseModel):
570
None,
571
description="Date of the alert in Unix timestamp",
572
)
572
- alert_metadata_tag: Optional[str] = Field(
573
- None,
574
- description="Metadata tag for the alert",
573
+ rule_description: str = Field(
574
+ ...,
575
+ description="Description of the rule",
576
)
576
- alert_gid: Optional[int] = Field(None, description="Alert group ID")
577
578
class Config:
579
allow_population_by_field_name = True
@@ -583,11 +583,11 @@ class Office365SourceModel(BaseModel):
583
return self.dict(exclude_none=True)
584
585
586
-class Office365AlertModel(BaseModel):
586
+class Office365ExchangeAlertModel(BaseModel):
587
_index: str
588
_id: str
589
_version: int
590
- _source: Office365SourceModel
590
+ _source: Office365ExchangeSourceModel
591
asset_type_id: Optional[int] = Field(
592
None,
593
description="The asset type id of the alert which is needed for when we add the asset to IRIS.",
@@ -605,25 +605,20 @@ class Office365AlertModel(BaseModel):
605
extra = Extra.allow
606
607
608
-########### ! Create Suricata Alerts In IRIS Schemas ! ###########
609
-class Office365IrisAsset(BaseModel):
608
+########### ! Create Office365 Exchange Alerts In IRIS Schemas ! ###########
609
+class Office365ExchangeIrisAsset(BaseModel):
610
asset_name: Optional[str] = Field(
611
- "Asset Does Not Apply to Suricata Alerts",
611
+ "Asset Does Not Apply to Office365 Exchange Alerts",
612
description="Name of the asset",
613
- example="Server01",
614
- )
615
- asset_ip: Optional[str] = Field(
616
- "Asset Does Not Apply to Suricata Alerts",
617
- description="IP address of the asset",
618
- example="192.168.1.1",
613
+ example="test@socfortress.co",
614
)
615
asset_description: Optional[str] = Field(
621
- "Asset Does Not Apply to Suricata Alerts",
616
+ "Asset Does Not Apply to Office365 Exchange Alerts",
617
description="Description of the asset",
618
example="Windows Server",
619
)
620
asset_type_id: Optional[int] = Field(
626
- 9,
621
+ 1,
622
description="Type ID of the asset",
623
example=1,
624
)
@@ -632,55 +627,180 @@ class Office365IrisAsset(BaseModel):
627
return self.dict(exclude_none=True)
628
629
635
-class Office365IrisIoc(BaseModel):
636
- ioc_value: str = Field(
630
+class Office365ExchangeIrisAlertContext(BaseModel):
631
+ _source: Office365ExchangeSourceModel = Field(..., description="Source of the alert")
632
+ client_ip: Optional[str] = Field("Not found", description="Client IP address")
633
+ operation: Optional[str] = Field("Not found", description="Operation")
634
+ creation_time: Optional[str] = Field("Not found", description="Creation time")
635
+ office365_id: str = Field(..., description="Office365 ID")
636
+ organization_name: str = Field(..., description="Organization name")
637
+ user_id: str = Field(..., description="User ID")
638
+ workload: str = Field(..., description="Workload")
639
+ organization_id: str = Field(..., description="Organization ID")
640
+ customer_iris_id: Optional[int] = Field(
641
+ None,
642
+ description="IRIS ID of the customer",
643
+ )
644
+ customer_name: Optional[str] = Field(
645
+ None,
646
+ description="Name of the customer",
647
+ )
648
+ customer_cases_index: Optional[str] = Field(
649
+ None,
650
+ description="IRIS case index name in the Wazuh-Indexer",
651
+ )
652
+ time_field: Optional[str] = Field(
653
+ "timestamp_utc",
654
+ description="The timefield of the alert to be used when creating the IRIS alert.",
655
+ )
656
+ rule_description: str = Field(
657
...,
638
- description="Value of the IoC",
639
- example="www.google.com",
658
+ description="Description of the rule",
659
)
641
- ioc_description: str = Field(
660
+ rule_id: str = Field(
661
...,
643
- description="Description of the IoC",
644
- example="Google",
662
+ description="ID of the rule that triggered the alert",
663
+ example="2001",
664
)
646
- ioc_tlp_id: int = Field(1, description="TLP ID of the IoC", example=1)
647
- ioc_type_id: int = Field(20, description="Type ID of the IoC", example=20)
665
666
+ def to_dict(self):
667
+ return self.dict(exclude_none=True)
668
650
-class Office365IrisAlertContext(BaseModel):
651
- _source: Office365SourceModel
652
- alert_id: str = Field(..., description="ID of the alert", example="123")
653
- alert_name: str = Field(
669
+
670
+class Office365ExchangeIrisAlertPayload(BaseModel):
671
+ alert_title: str = Field(
672
...,
655
- description="Name of the alert",
673
+ description="Title of the alert",
674
example="Intrusion Detected",
675
)
658
- alert_level: int = Field(..., description="Severity level of the alert", example=3)
659
- rule_id: int = Field(
676
+ alert_description: str = Field(
677
...,
661
- description="ID of the Suricata rule that triggered the alert",
662
- example="2001",
678
+ description="Description of the alert",
679
+ example="Intrusion Detected by Firewall",
680
)
664
- src_ip: str = Field(
681
+ alert_source: str = Field(..., description="Source of the alert", example="Suricata")
682
+ assets: List[Office365ExchangeIrisAsset] = Field(..., description="List of affected assets")
683
+ alert_status_id: int = Field(..., description="Status ID of the alert", example=3)
684
+ alert_severity_id: int = Field(
685
...,
666
- description="Source IP address of the alert",
667
- example="1.1.1.1",
686
+ description="Severity ID of the alert",
687
+ example=5,
688
)
669
- dest_ip: str = Field(
689
+ alert_customer_id: int = Field(
690
...,
671
- description="Destination IP address of the alert",
672
- example="8.8.8.8",
691
+ description="Customer ID related to the alert",
692
+ example=1,
693
)
674
- app_proto: str = Field(
694
+ alert_source_content: Dict[str, Any] = Field(
695
...,
676
- description="Application protocol of the alert",
677
- example="TCP",
696
+ description="Original content from the alert source",
697
)
679
- agent_labels_customer: str = Field(
698
+ alert_context: Office365ExchangeIrisAlertContext = Field(
699
...,
681
- description="Customer of the endpoint",
682
- example="SOCFortress",
700
+ description="Contextual information about the alert",
701
+ )
702
+ alert_iocs: Optional[List[IrisIoc]] = Field(
703
+ None,
704
+ description="List of IoCs related to the alert",
705
+ )
706
+ alert_source_event_time: str = Field(
707
+ ...,
708
+ description="Timestamp of the alert",
709
+ example="2021-01-01T00:00:00.000Z",
710
)
711
+
712
+ def to_dict(self):
713
+ return self.dict(exclude_none=True)
714
+
715
+
716
+########### ! Office365 Threat Intel ALERTS SCHEMA ! ###########
717
+class Office365ThreatIntelSourceModel(BaseModel):
718
+ sender_ip: Optional[str] = Field("Not found", description="Sender IP address")
719
+ operation: Optional[str] = Field("Not found", description="Operation")
720
+ creation_time: Optional[str] = Field("Not found", description="Creation time")
721
+ office365_id: str = Field(..., description="Office365 ID")
722
+ recipients: str = Field(..., description="Recipients")
723
+ workload: str = Field(..., description="Workload")
724
+ organization_id: str = Field(..., description="Organization ID")
725
+ timestamp: str = Field(..., description="The timestamp of the alert.")
726
+ timestamp_utc: Optional[str] = Field(
727
+ ...,
728
+ description="The UTC timestamp of the alert.",
729
+ )
730
+ time_field: Optional[str] = Field(
731
+ "timestamp",
732
+ description="The timefield of the alert to be used when creating the IRIS alert.",
733
+ )
734
+ date: Optional[float] = Field(
735
+ None,
736
+ description="Date of the alert in Unix timestamp",
737
+ )
738
+ rule_description: str = Field(
739
+ ...,
740
+ description="Description of the rule",
741
+ )
742
+
743
+ class Config:
744
+ allow_population_by_field_name = True
745
+ extra = Extra.allow
746
+
747
+ def to_dict(self):
748
+ return self.dict(exclude_none=True)
749
+
750
+
751
+class Office365ThreatIntelAlertModel(BaseModel):
752
+ _index: str
753
+ _id: str
754
+ _version: int
755
+ _source: Office365ThreatIntelSourceModel
756
+ asset_type_id: Optional[int] = Field(
757
+ None,
758
+ description="The asset type id of the alert which is needed for when we add the asset to IRIS.",
759
+ )
760
+ ioc_value: Optional[str] = Field(
761
+ None,
762
+ description="The IoC value of the alert which is needed for when we add the IoC to IRIS.",
763
+ )
764
+ ioc_type: Optional[str] = Field(
765
+ None,
766
+ description="The IoC type of the alert which is needed for when we add the IoC to IRIS.",
767
+ )
768
+
769
+ class Config:
770
+ extra = Extra.allow
771
+
772
+
773
+########### ! Create Office365 Threat Intel Alerts In IRIS Schemas ! ###########
774
+class Office365ThreatIntelIrisAsset(BaseModel):
775
+ asset_name: Optional[str] = Field(
776
+ "Asset Does Not Apply to Office365 Exchange Alerts",
777
+ description="Name of the asset",
778
+ example="test@socfortress.co",
779
+ )
780
+ asset_description: Optional[str] = Field(
781
+ "Asset Does Not Apply to Office365 Exchange Alerts",
782
+ description="Description of the asset",
783
+ example="Windows Server",
784
+ )
785
+ asset_type_id: Optional[int] = Field(
786
+ 1,
787
+ description="Type ID of the asset",
788
+ example=1,
789
+ )
790
+
791
+ def to_dict(self):
792
+ return self.dict(exclude_none=True)
793
+
794
+
795
+class Office365ThreatIntelIrisAlertContext(BaseModel):
796
+ _source: Office365ThreatIntelSourceModel = Field(..., description="Source of the alert")
797
+ sender_ip: Optional[str] = Field("Not found", description="Sender IP address")
798
+ operation: Optional[str] = Field("Not found", description="Operation")
799
+ creation_time: Optional[str] = Field("Not found", description="Creation time")
800
+ office365_id: str = Field(..., description="Office365 ID")
801
+ recipients: str = Field(..., description="Recipients")
802
+ workload: str = Field(..., description="Workload")
803
+ organization_id: str = Field(..., description="Organization ID")
804
customer_iris_id: Optional[int] = Field(
805
None,
806
description="IRIS ID of the customer",
@@ -697,12 +817,21 @@ class Office365IrisAlertContext(BaseModel):
817
"timestamp_utc",
818
description="The timefield of the alert to be used when creating the IRIS alert.",
819
)
820
+ rule_description: str = Field(
821
+ ...,
822
+ description="Description of the rule",
823
+ )
824
+ rule_id: str = Field(
825
+ ...,
826
+ description="ID of the rule that triggered the alert",
827
+ example="2001",
828
+ )
829
830
def to_dict(self):
831
return self.dict(exclude_none=True)
832
833
705
-class Office365IrisAlertPayload(BaseModel):
834
+class Office365ThreatIntelIrisAlertPayload(BaseModel):
835
alert_title: str = Field(
836
...,
837
description="Title of the alert",
@@ -714,7 +843,7 @@ class Office365IrisAlertPayload(BaseModel):
843
example="Intrusion Detected by Firewall",
844
)
845
alert_source: str = Field(..., description="Source of the alert", example="Suricata")
717
- assets: List[SuricataIrisAsset] = Field(..., description="List of affected assets")
846
+ assets: List[Office365ThreatIntelIrisAsset] = Field(..., description="List of affected assets")
847
alert_status_id: int = Field(..., description="Status ID of the alert", example=3)
848
alert_severity_id: int = Field(
849
...,
@@ -730,7 +859,7 @@ class Office365IrisAlertPayload(BaseModel):
859
...,
860
description="Original content from the alert source",
861
)
733
- alert_context: SuricataIrisAlertContext = Field(
862
+ alert_context: Office365ThreatIntelIrisAlertContext = Field(
863
...,
864
description="Contextual information about the alert",
865
)
backend/app/integrations/monitoring_alert/services/office365_exchange.py
renamed
+73
-67
@@ -30,15 +30,17 @@ from app.integrations.monitoring_alert.schema.monitoring_alert import (
30
FilterAlertsRequest,
31
)
32
from app.integrations.monitoring_alert.schema.monitoring_alert import (
33
- Office365AlertModel,
33
+ Office365ExchangeAlertModel,
34
)
35
from app.integrations.monitoring_alert.schema.monitoring_alert import (
36
- Office365IrisAlertContext,
36
+ Office365ExchangeIrisAlertContext,
37
)
38
from app.integrations.monitoring_alert.schema.monitoring_alert import (
39
- Office365IrisAlertPayload,
39
+ Office365ExchangeIrisAlertPayload,
40
+)
41
+from app.integrations.monitoring_alert.schema.monitoring_alert import (
42
+ Office365ExchangeIrisAsset,
43
)
41
-from app.integrations.monitoring_alert.schema.monitoring_alert import Office365IrisAsset
44
from app.integrations.monitoring_alert.utils.db_operations import remove_alert_id
45
from app.integrations.utils.alerts import validate_ioc_type
46
from app.utils import get_customer_alert_settings
@@ -56,7 +58,7 @@ def valid_ioc_fields() -> Set[str]:
58
59
60
async def construct_alert_source_link(
59
- alert_details: Office365IrisAlertContext,
61
+ alert_details: Office365ExchangeIrisAlertContext,
62
session: AsyncSession,
63
) -> str:
64
"""
@@ -71,18 +73,18 @@ async def construct_alert_source_link(
73
The alert source link.
74
"""
75
logger.info(f"Constructing alert source link for alert: {alert_details}")
74
- query_string = f"%22query%22:%22alert_signature_id:%5C%22{alert_details.alert_id}%5C%22%20AND%20"
76
+ query_string = f"%22query%22:%22data_office365_ClientIP:%5C%22{alert_details.client_ip}%5C%22%20AND%20"
77
grafana_url = (
78
await get_customer_alert_settings(
77
- customer_code=alert_details.agent_labels_customer,
79
+ customer_code=alert_details.organization_id,
80
session=session,
81
)
82
).grafana_url
83
84
return (
83
- f"{grafana_url}/explore?left=%5B%22now-6h%22,%22now%22,%22Office365%22,%7B%22refId%22:%22A%22,"
85
+ f"{grafana_url}/explore?left=%5B%22now-6h%22,%22now%22,%22O365%22,%7B%22refId%22:%22A%22,"
86
f"{query_string}"
85
- f"src_ip:%5C%22{alert_details.src_ip}%5C%22%22,"
87
+ f"data_office365_UserId:%5C%22{alert_details.user_id}%5C%22%22,"
88
"%22alias%22:%22%22,%22metrics%22:%5B%7B%22id%22:%221%22,%22type%22:%22logs%22,%22settings%22:%7B%22limit%22:%22500%22%7D%7D%5D,"
89
"%22bucketAggs%22:%5B%5D,%22timeField%22:%22timestamp%22%7D%5D"
90
)
@@ -112,9 +114,9 @@ async def build_ioc_payload(alert_details: CreateAlertRequest) -> Optional[IrisI
114
115
116
async def build_asset_payload(
115
- alert_details: Office365IrisAlertContext,
117
+ alert_details: Office365ExchangeIrisAlertContext,
118
session: AsyncSession,
117
-) -> Office365IrisAsset:
119
+) -> Office365ExchangeIrisAsset:
120
"""
121
Build the payload for an IrisAsset object based on the agent data and alert details.
122
@@ -128,19 +130,19 @@ async def build_asset_payload(
130
# Get the agent_id based on the hostname from the Agents table
131
logger.info(f"Building asset payload for alert: {alert_details}")
132
if alert_details is not None:
131
- return Office365IrisAsset(
132
- asset_name=alert_details.src_ip,
133
- asset_ip=alert_details.src_ip,
133
+ return Office365ExchangeIrisAsset(
134
+ asset_name=alert_details.user_id,
135
+ asset_ip=alert_details.client_ip,
136
asset_description=await construct_alert_source_link(
137
alert_details,
138
session=session,
139
),
138
- asset_type_id=2,
140
+ asset_type_id=1,
141
)
140
- return Office365IrisAsset()
142
+ return Office365ExchangeIrisAsset()
143
144
143
-async def fetch_wazuh_indexer_details(alert_id: str, index: str) -> Office365AlertModel:
145
+async def fetch_wazuh_indexer_details(alert_id: str, index: str) -> Office365ExchangeAlertModel:
146
"""
147
Fetch the Office365 alert details from the Wazuh-Indexer.
148
@@ -158,10 +160,10 @@ async def fetch_wazuh_indexer_details(alert_id: str, index: str) -> Office365Ale
160
es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
161
response = es_client.get(index=index, id=alert_id)
162
161
- return Office365AlertModel(**response)
163
+ return Office365ExchangeAlertModel(**response)
164
165
164
-async def fetch_alert_details(alert: MonitoringAlerts) -> Office365AlertModel:
166
+async def fetch_alert_details(alert: MonitoringAlerts) -> Office365ExchangeAlertModel:
167
logger.info(f"Analyzing Office365 Exchange Online alert: {alert}")
168
alert_details = await fetch_wazuh_indexer_details(alert.alert_id, alert.alert_index)
169
logger.info(f"Alert details: {alert_details}")
@@ -169,7 +171,7 @@ async def fetch_alert_details(alert: MonitoringAlerts) -> Office365AlertModel:
171
172
173
async def check_event_exclusion(
172
- alert_details: Office365AlertModel,
174
+ alert_details: Office365ExchangeAlertModel,
175
alert_detail_service: AlertDetailsService,
176
session: AsyncSession,
177
):
@@ -189,7 +191,7 @@ async def check_event_exclusion(
191
logger.info("Alert is not excluded due to multi exclusion.")
192
193
192
-async def check_if_open_alert_exists_in_iris(alert_details: Office365AlertModel, session: AsyncSession) -> list:
194
+async def check_if_open_alert_exists_in_iris(alert_details: Office365ExchangeAlertModel, session: AsyncSession) -> list:
195
"""
196
Check if the alert exists in IRIS.
197
@@ -203,13 +205,13 @@ async def check_if_open_alert_exists_in_iris(alert_details: Office365AlertModel,
205
client, alert_client = await initialize_client_and_alert("DFIR-IRIS")
206
customer_iris_id = (
207
await get_customer_alert_settings(
206
- # customer_code=alert_details._source["agent_labels_customer"],
207
- customer_code="00002",
208
+ customer_code=alert_details._source["data_office365_OrganizationId"],
209
+ # customer_code="9668d0df-6e2e-40fd-947d-d568e520e084",
210
session=session,
211
)
212
).iris_customer_id
213
request = FilterAlertsRequest(
212
- alert_tags=alert_details._source["alert_signature_id"],
214
+ alert_tags=alert_details._source["rule_id"],
215
alert_customer_id=customer_iris_id,
216
)
217
params = construct_params(request)
@@ -246,9 +248,9 @@ def construct_params(request: FilterAlertsRequest) -> dict:
248
249
250
async def build_alert_context_payload(
249
- alert_details: Office365IrisAlertContext,
251
+ alert_details: Office365ExchangeIrisAlertContext,
252
session: AsyncSession,
251
-) -> Office365IrisAlertContext:
253
+) -> Office365ExchangeIrisAlertContext:
254
"""
255
Builds the payload for the alert context.
256
@@ -260,41 +262,44 @@ async def build_alert_context_payload(
262
Returns:
263
Office365IrisAlertContext: The built alert context payload.
264
"""
263
- return Office365IrisAlertContext(
265
+ return Office365ExchangeIrisAlertContext(
266
customer_iris_id=(
267
await get_customer_alert_settings(
266
- customer_code=alert_details.agent_labels_customer,
268
+ customer_code=alert_details.organization_id,
269
session=session,
270
)
271
).iris_customer_id,
272
customer_name=(
273
await get_customer_alert_settings(
272
- customer_code=alert_details.agent_labels_customer,
274
+ customer_code=alert_details.organization_id,
275
session=session,
276
)
277
).customer_name,
278
customer_cases_index=(
279
await get_customer_alert_settings(
278
- customer_code=alert_details.agent_labels_customer,
280
+ customer_code=alert_details.organization_id,
281
session=session,
282
)
283
).iris_index,
282
- alert_id=alert_details.alert_id,
283
- alert_name=alert_details.alert_name,
284
- alert_level=alert_details.alert_level,
284
+ client_ip=alert_details.client_ip,
285
+ operation=alert_details.operation,
286
+ creation_time=alert_details.creation_time,
287
+ office365_id=alert_details.office365_id,
288
+ organization_name=alert_details.organization_name,
289
+ user_id=alert_details.user_id,
290
+ workload=alert_details.workload,
291
+ organization_id=alert_details.organization_id,
292
+ agent_labels_customer=alert_details.organization_id,
293
+ rule_description=alert_details.rule_description,
294
rule_id=alert_details.rule_id,
286
- src_ip=alert_details.src_ip,
287
- dest_ip=alert_details.dest_ip,
288
- app_proto=alert_details.app_proto,
289
- agent_labels_customer=alert_details.agent_labels_customer,
295
)
296
297
298
async def build_alert_payload(
294
- alert_details: Office365IrisAlertContext,
299
+ alert_details: Office365ExchangeIrisAlertContext,
300
ioc_payload: Optional[IrisIoc],
301
session: AsyncSession,
297
-) -> Office365IrisAlertPayload:
302
+) -> Office365ExchangeIrisAlertPayload:
303
"""
304
Builds the payload for an alert based on the provided alert details, agent data, IoC payload, and session.
305
@@ -322,16 +327,16 @@ async def build_alert_payload(
327
328
if ioc_payload:
329
logger.info(f"Alert has IoC: {ioc_payload}")
325
- return Office365IrisAlertPayload(
326
- alert_title=alert_details.alert_name,
327
- alert_description=alert_details.alert_name,
328
- alert_source="COPILOT Office365 ANALYSIS",
330
+ return Office365ExchangeIrisAlertPayload(
331
+ alert_title=alert_details.rule_description,
332
+ alert_description=alert_details.rule_description,
333
+ alert_source="COPILOT OFFICE365 EXCHANGE ANALYSIS",
334
assets=[asset_payload],
335
alert_status_id=3,
336
alert_severity_id=5,
337
alert_customer_id=(
338
await get_customer_alert_settings(
334
- customer_code=alert_details.agent_labels_customer,
339
+ customer_code=alert_details.organization_id,
340
session=session,
341
)
342
).iris_customer_id,
@@ -342,16 +347,16 @@ async def build_alert_payload(
347
)
348
else:
349
logger.info("Alert does not have IoC")
345
- return Office365IrisAlertPayload(
346
- alert_title=alert_details.alert_name,
347
- alert_description=alert_details.alert_name,
348
- alert_source="COPILOT Office365 ANALYSIS",
350
+ return Office365ExchangeIrisAlertPayload(
351
+ alert_title=alert_details.rule_description,
352
+ alert_description=alert_details.rule_description,
353
+ alert_source="COPILOT OFFICE365 EXCHANGE ANALYSIS",
354
assets=[asset_payload],
355
alert_status_id=3,
356
alert_severity_id=5,
357
alert_customer_id=(
358
await get_customer_alert_settings(
354
- customer_code=alert_details.agent_labels_customer,
359
+ customer_code=alert_details.organization_id,
360
session=session,
361
)
362
).iris_customer_id,
@@ -362,8 +367,8 @@ async def build_alert_payload(
367
368
369
async def create_alert_details(
365
- alert_details: Office365AlertModel,
366
-) -> Office365IrisAlertContext:
370
+ alert_details: Office365ExchangeAlertModel,
371
+) -> Office365ExchangeIrisAlertContext:
372
"""
373
Create an alert details object from the Office365 alert details.
374
@@ -374,26 +379,26 @@ async def create_alert_details(
379
Office365IrisAlertContext: The alert details object.
380
"""
381
logger.info(f"Creating alert details for alert: {alert_details}")
377
- return Office365IrisAlertContext(
382
+ return Office365ExchangeIrisAlertContext(
383
index=alert_details._index,
384
id=alert_details._id,
380
- alert_id=alert_details._source["alert_signature_id"],
381
- alert_name=alert_details._source["alert_signature"],
382
- alert_level=alert_details._source["alert_severity"],
383
- rule_id=alert_details._source["alert_signature_id"],
384
- src_ip=alert_details._source["src_ip"],
385
- dest_ip=alert_details._source["dest_ip"],
386
- app_proto=alert_details._source.get(
387
- "app_proto",
388
- "No application protocol found",
389
- ),
390
- agent_labels_customer=alert_details._source["agent_labels_customer"],
385
+ client_ip=alert_details._source["data_office365_ClientIP"],
386
+ operation=alert_details._source["data_office365_Operation"],
387
+ creation_time=alert_details._source["data_office365_CreationTime"],
388
+ office365_id=alert_details._source["data_office365_Id"],
389
+ organization_name=alert_details._source["data_office365_OrganizationName"],
390
+ user_id=alert_details._source["data_office365_UserId"],
391
+ workload=alert_details._source["data_office365_Workload"],
392
+ organization_id=alert_details._source["data_office365_OrganizationId"],
393
+ agent_labels_customer=alert_details._source["data_office365_OrganizationId"],
394
time_field=alert_details._source.get("timestamp_utc", alert_details._source.get("timestamp")),
395
+ rule_description=alert_details._source["rule_description"],
396
+ rule_id=alert_details._source["rule_id"],
397
)
398
399
400
async def create_and_update_alert_in_iris(
396
- alert_details: Office365AlertModel,
401
+ alert_details: Office365ExchangeAlertModel,
402
session: AsyncSession,
403
) -> int:
404
"""
@@ -430,14 +435,14 @@ async def create_and_update_alert_in_iris(
435
client,
436
alert_client.update_alert,
437
alert_id,
433
- {"alert_tags": f"{alert_details.alert_id}"},
438
+ {"alert_tags": f"{alert_details.rule_id}"},
439
)
440
# Update the alert with the asset payload
441
await fetch_and_validate_data(
442
client,
443
alert_client.update_alert,
444
alert_id,
440
- {"assets": [dict(Office365IrisAsset(**iris_alert_payload.assets[0].to_dict()))]},
445
+ {"assets": [dict(Office365ExchangeIrisAsset(**iris_alert_payload.assets[0].to_dict()))]},
446
)
447
if ioc_payload:
448
await fetch_and_validate_data(
@@ -516,6 +521,7 @@ async def analyze_office365_exchange_online_alerts(
521
alert_details,
522
session,
523
)
524
+
525
logger.info(f"Alert {iris_alert_id} created in IRIS.")
526
await remove_alert_id(alert.alert_id, session)
527
es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
@@ -546,7 +552,7 @@ async def analyze_office365_exchange_online_alerts(
552
alert_details=alert_details,
553
session=session,
554
)
549
- current_assets.append(dict(Office365IrisAsset(**asset_payload.to_dict())))
555
+ current_assets.append(dict(Office365ExchangeIrisAsset(**asset_payload.to_dict())))
556
current_assets = await remove_duplicate_assets(current_assets)
557
await update_alert_with_assets(
558
client,
backend/app/integrations/monitoring_alert/services/office365_threatintel.py
new
+576
@@ -0,0 +1,576 @@
1
+import json
2
+from typing import Optional
3
+from typing import Set
4
+
5
+from fastapi import HTTPException
6
+from loguru import logger
7
+from sqlalchemy.ext.asyncio import AsyncSession
8
+
9
+from app.connectors.dfir_iris.utils.universal import fetch_and_validate_data
10
+from app.connectors.dfir_iris.utils.universal import initialize_client_and_alert
11
+from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client
12
+from app.db.universal_models import CustomersMeta
13
+from app.integrations.alert_creation.general.schema.alert import CreateAlertRequest
14
+from app.integrations.alert_creation.general.schema.alert import IrisIoc
15
+from app.integrations.alert_creation.general.schema.alert import ValidIocFields
16
+from app.integrations.alert_creation.general.services.alert_multi_exclude import (
17
+ AlertDetailsService,
18
+)
19
+from app.integrations.alert_escalation.schema.general_alert import (
20
+ CreateAlertRequest as AddAlertRequest,
21
+)
22
+from app.integrations.alert_escalation.services.general_alert import (
23
+ add_alert_to_document,
24
+)
25
+from app.integrations.monitoring_alert.models.monitoring_alert import MonitoringAlerts
26
+from app.integrations.monitoring_alert.schema.monitoring_alert import (
27
+ AlertAnalysisResponse,
28
+)
29
+from app.integrations.monitoring_alert.schema.monitoring_alert import (
30
+ FilterAlertsRequest,
31
+)
32
+from app.integrations.monitoring_alert.schema.monitoring_alert import (
33
+ Office365ThreatIntelAlertModel,
34
+)
35
+from app.integrations.monitoring_alert.schema.monitoring_alert import (
36
+ Office365ThreatIntelIrisAlertContext,
37
+)
38
+from app.integrations.monitoring_alert.schema.monitoring_alert import (
39
+ Office365ThreatIntelIrisAlertPayload,
40
+)
41
+from app.integrations.monitoring_alert.schema.monitoring_alert import (
42
+ Office365ThreatIntelIrisAsset,
43
+)
44
+from app.integrations.monitoring_alert.utils.db_operations import remove_alert_id
45
+from app.integrations.utils.alerts import validate_ioc_type
46
+from app.utils import get_customer_alert_settings
47
+
48
+
49
+def valid_ioc_fields() -> Set[str]:
50
+ """
51
+ Getter for the set of valid IoC fields.
52
+ Returns
53
+ -------
54
+ Set[str]
55
+ The set of valid IoC fields.
56
+ """
57
+ return {field.value for field in ValidIocFields}
58
+
59
+
60
+async def construct_alert_source_link(
61
+ alert_details: Office365ThreatIntelIrisAlertContext,
62
+ session: AsyncSession,
63
+) -> str:
64
+ """
65
+ Construct the alert source link for the alert details.
66
+ Parameters
67
+ ----------
68
+ alert_details: CreateAlertRequest
69
+ The alert details.
70
+ Returns
71
+ -------
72
+ str
73
+ The alert source link.
74
+ """
75
+ logger.info(f"Constructing alert source link for alert: {alert_details}")
76
+ query_string = f"%22query%22:%22data_office365_SenderIp:%5C%22{alert_details.sender_ip}%5C%22%20AND%20"
77
+ grafana_url = (
78
+ await get_customer_alert_settings(
79
+ customer_code=alert_details.organization_id,
80
+ session=session,
81
+ )
82
+ ).grafana_url
83
+
84
+ return (
85
+ f"{grafana_url}/explore?left=%5B%22now-6h%22,%22now%22,%22O365%22,%7B%22refId%22:%22A%22,"
86
+ f"{query_string}"
87
+ f"data_office365_Recipients:%5C%22{alert_details.recipients}%5C%22%22,"
88
+ "%22alias%22:%22%22,%22metrics%22:%5B%7B%22id%22:%221%22,%22type%22:%22logs%22,%22settings%22:%7B%22limit%22:%22500%22%7D%7D%5D,"
89
+ "%22bucketAggs%22:%5B%5D,%22timeField%22:%22timestamp%22%7D%5D"
90
+ )
91
+
92
+
93
+async def build_ioc_payload(alert_details: CreateAlertRequest) -> Optional[IrisIoc]:
94
+ """
95
+ Builds an IoC payload based on the provided alert details.
96
+
97
+ Args:
98
+ alert_details (CreateAlertRequest): The details of the alert.
99
+
100
+ Returns:
101
+ Optional[IrisIoc]: The constructed IoC payload, or None if no valid IoC fields are found.
102
+ """
103
+ for field in valid_ioc_fields():
104
+ if hasattr(alert_details, field):
105
+ ioc_value = getattr(alert_details, field)
106
+ ioc_type = await validate_ioc_type(ioc_value=ioc_value)
107
+ return IrisIoc(
108
+ ioc_value=ioc_value,
109
+ ioc_description="IoC found in alert",
110
+ ioc_tlp_id=1,
111
+ ioc_type_id=ioc_type,
112
+ )
113
+ return None
114
+
115
+
116
+async def build_asset_payload(
117
+ alert_details: Office365ThreatIntelIrisAlertContext,
118
+ session: AsyncSession,
119
+) -> Office365ThreatIntelIrisAsset:
120
+ """
121
+ Build the payload for an IrisAsset object based on the agent data and alert details.
122
+
123
+ Args:
124
+ agent_data (AgentsResponse): The response containing agent data.
125
+ alert_details: The details of the alert.
126
+
127
+ Returns:
128
+ IrisAsset: The constructed IrisAsset object.
129
+ """
130
+ # Get the agent_id based on the hostname from the Agents table
131
+ logger.info(f"Building asset payload for alert: {alert_details}")
132
+ if alert_details is not None:
133
+ return Office365ThreatIntelIrisAsset(
134
+ asset_name=alert_details.recipients,
135
+ asset_description=await construct_alert_source_link(
136
+ alert_details,
137
+ session=session,
138
+ ),
139
+ asset_type_id=1,
140
+ )
141
+ return Office365ThreatIntelIrisAsset()
142
+
143
+
144
+async def fetch_wazuh_indexer_details(alert_id: str, index: str) -> Office365ThreatIntelAlertModel:
145
+ """
146
+ Fetch the Office365 alert details from the Wazuh-Indexer.
147
+
148
+ Args:
149
+ alert_id (str): The alert ID.
150
+ index (str): The index.
151
+
152
+ Returns:
153
+ CollectAlertsResponse: The response from the Wazuh-Indexer.
154
+ """
155
+ logger.info(
156
+ f"Fetching Office365 alert details for alert_id: {alert_id} and index: {index}",
157
+ )
158
+
159
+ es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
160
+ response = es_client.get(index=index, id=alert_id)
161
+
162
+ return Office365ThreatIntelAlertModel(**response)
163
+
164
+
165
+async def fetch_alert_details(alert: MonitoringAlerts) -> Office365ThreatIntelAlertModel:
166
+ logger.info(f"Analyzing Office365 Exchange Online alert: {alert}")
167
+ alert_details = await fetch_wazuh_indexer_details(alert.alert_id, alert.alert_index)
168
+ logger.info(f"Alert details: {alert_details}")
169
+ return alert_details
170
+
171
+
172
+async def check_event_exclusion(
173
+ alert_details: Office365ThreatIntelAlertModel,
174
+ alert_detail_service: AlertDetailsService,
175
+ session: AsyncSession,
176
+):
177
+ logger.info("Checking if alert is excluded due to multi exclusion.")
178
+ logger.info(f"Alert details: {alert_details}")
179
+ event_exclude_result = await alert_detail_service.collect_alert_timeline_process_id(
180
+ agent_name=alert_details._source["agent_name"],
181
+ process_id=alert_details._source.get("process_id", "n/a"),
182
+ index=alert_details._index,
183
+ session=session,
184
+ )
185
+ if event_exclude_result is True:
186
+ raise HTTPException(
187
+ status_code=400,
188
+ detail="Alert excluded due to multi exclusion as set in the config.ini file.",
189
+ )
190
+ logger.info("Alert is not excluded due to multi exclusion.")
191
+
192
+
193
+async def check_if_open_alert_exists_in_iris(alert_details: Office365ThreatIntelAlertModel, session: AsyncSession) -> list:
194
+ """
195
+ Check if the alert exists in IRIS.
196
+
197
+ Args:
198
+ alert_details (Office365AlertModel): The alert details.
199
+ session (AsyncSession): The database session.
200
+
201
+ Returns:
202
+ bool: True if the alert exists in IRIS, False otherwise.
203
+ """
204
+ client, alert_client = await initialize_client_and_alert("DFIR-IRIS")
205
+ customer_iris_id = (
206
+ await get_customer_alert_settings(
207
+ customer_code=alert_details._source["data_office365_OrganizationId"],
208
+ # customer_code="9668d0df-6e2e-40fd-947d-d568e520e084",
209
+ session=session,
210
+ )
211
+ ).iris_customer_id
212
+ request = FilterAlertsRequest(
213
+ alert_tags=alert_details._source["rule_id"],
214
+ alert_customer_id=customer_iris_id,
215
+ )
216
+ params = construct_params(request)
217
+ alert_exists = await fetch_and_validate_data(
218
+ client,
219
+ lambda: alert_client.filter_alerts(**params),
220
+ )
221
+ logger.info(f"Alert exists: {alert_exists['data']['alerts']}")
222
+ return alert_exists["data"]["alerts"][0]["alert_id"] if alert_exists["data"]["alerts"] else []
223
+
224
+
225
+def construct_params(request: FilterAlertsRequest) -> dict:
226
+ """
227
+ Constructs the parameters for the alert filtering request.
228
+
229
+ Args:
230
+ request (FilterAlertsRequest): The request object containing filtering criteria.
231
+
232
+ Returns:
233
+ dict: A dictionary of parameters for the alert filtering request.
234
+ """
235
+ params = {
236
+ "page": request.page,
237
+ "per_page": request.per_page,
238
+ "sort": request.sort,
239
+ "alert_tags": request.alert_tags,
240
+ "alert_status_id": request.alert_status_id,
241
+ "alert_customer_id": request.alert_customer_id,
242
+ # Add more parameters here as needed
243
+ }
244
+
245
+ # Remove parameters that have a value of None
246
+ return {k: v for k, v in params.items() if v is not None}
247
+
248
+
249
+async def build_alert_context_payload(
250
+ alert_details: Office365ThreatIntelIrisAlertContext,
251
+ session: AsyncSession,
252
+) -> Office365ThreatIntelIrisAlertContext:
253
+ """
254
+ Builds the payload for the alert context.
255
+
256
+ Args:
257
+ alert_details (CreateAlertRequest): The details of the alert.
258
+ agent_data (AgentsResponse): The agent data.
259
+ session (AsyncSession): The async session.
260
+
261
+ Returns:
262
+ Office365IrisAlertContext: The built alert context payload.
263
+ """
264
+ return Office365ThreatIntelIrisAlertContext(
265
+ customer_iris_id=(
266
+ await get_customer_alert_settings(
267
+ customer_code=alert_details.organization_id,
268
+ session=session,
269
+ )
270
+ ).iris_customer_id,
271
+ customer_name=(
272
+ await get_customer_alert_settings(
273
+ customer_code=alert_details.organization_id,
274
+ session=session,
275
+ )
276
+ ).customer_name,
277
+ customer_cases_index=(
278
+ await get_customer_alert_settings(
279
+ customer_code=alert_details.organization_id,
280
+ session=session,
281
+ )
282
+ ).iris_index,
283
+ sender_ip=alert_details.sender_ip,
284
+ operation=alert_details.operation,
285
+ creation_time=alert_details.creation_time,
286
+ office365_id=alert_details.office365_id,
287
+ recipients=alert_details.recipients,
288
+ workload=alert_details.workload,
289
+ organization_id=alert_details.organization_id,
290
+ agent_labels_customer=alert_details.organization_id,
291
+ rule_description=alert_details.rule_description,
292
+ rule_id=alert_details.rule_id,
293
+ )
294
+
295
+
296
+async def build_alert_payload(
297
+ alert_details: Office365ThreatIntelIrisAlertContext,
298
+ ioc_payload: Optional[IrisIoc],
299
+ session: AsyncSession,
300
+) -> Office365ThreatIntelIrisAlertPayload:
301
+ """
302
+ Builds the payload for an alert based on the provided alert details, agent data, IoC payload, and session.
303
+
304
+ Args:
305
+ alert_details (Office365AlertModel): The details of the alert.
306
+ agent_data: The agent data associated with the alert.
307
+ ioc_payload (Optional[IrisIoc]): The IoC payload associated with the alert.
308
+ session (AsyncSession): The session used for database operations.
309
+
310
+ Returns:
311
+ Office365IrisAlertPayload: The built alert payload.
312
+ """
313
+ asset_payload = await build_asset_payload(
314
+ alert_details=alert_details,
315
+ session=session,
316
+ )
317
+ logger.info(f"Asset payload: {asset_payload}")
318
+
319
+ context_payload = await build_alert_context_payload(
320
+ alert_details=alert_details,
321
+ session=session,
322
+ )
323
+
324
+ logger.info(f"Alert has context: {context_payload}")
325
+
326
+ if ioc_payload:
327
+ logger.info(f"Alert has IoC: {ioc_payload}")
328
+ return Office365ThreatIntelIrisAlertPayload(
329
+ alert_title=alert_details.rule_description,
330
+ alert_description=alert_details.rule_description,
331
+ alert_source="COPILOT OFFICE365 EXCHANGE ANALYSIS",
332
+ assets=[asset_payload],
333
+ alert_status_id=3,
334
+ alert_severity_id=5,
335
+ alert_customer_id=(
336
+ await get_customer_alert_settings(
337
+ customer_code=alert_details.organization_id,
338
+ session=session,
339
+ )
340
+ ).iris_customer_id,
341
+ alert_source_content=alert_details.to_dict(),
342
+ alert_context=context_payload,
343
+ alert_iocs=[ioc_payload],
344
+ alert_source_event_time=alert_details.time_field,
345
+ )
346
+ else:
347
+ logger.info("Alert does not have IoC")
348
+ return Office365ThreatIntelIrisAlertPayload(
349
+ alert_title=alert_details.rule_description,
350
+ alert_description=alert_details.rule_description,
351
+ alert_source="COPILOT OFFICE365 EXCHANGE ANALYSIS",
352
+ assets=[asset_payload],
353
+ alert_status_id=3,
354
+ alert_severity_id=5,
355
+ alert_customer_id=(
356
+ await get_customer_alert_settings(
357
+ customer_code=alert_details.organization_id,
358
+ session=session,
359
+ )
360
+ ).iris_customer_id,
361
+ alert_source_content=alert_details.to_dict(),
362
+ alert_context=context_payload,
363
+ alert_source_event_time=alert_details.time_field,
364
+ )
365
+
366
+
367
+async def create_alert_details(
368
+ alert_details: Office365ThreatIntelAlertModel,
369
+) -> Office365ThreatIntelIrisAlertContext:
370
+ """
371
+ Create an alert details object from the Office365 alert details.
372
+
373
+ Args:
374
+ alert_details (Office365AlertModel): The Office365 alert details.
375
+
376
+ Returns:
377
+ Office365IrisAlertContext: The alert details object.
378
+ """
379
+ logger.info(f"Creating alert details for alert: {alert_details}")
380
+ return Office365ThreatIntelIrisAlertContext(
381
+ index=alert_details._index,
382
+ id=alert_details._id,
383
+ sender_ip=alert_details._source["data_office365_SenderIp"],
384
+ operation=alert_details._source["data_office365_Operation"],
385
+ creation_time=alert_details._source["data_office365_CreationTime"],
386
+ office365_id=alert_details._source["data_office365_Id"],
387
+ recipients=alert_details._source["data_office365_Recipients"],
388
+ user_id=alert_details._source["data_office365_UserId"],
389
+ workload=alert_details._source["data_office365_Workload"],
390
+ organization_id=alert_details._source["data_office365_OrganizationId"],
391
+ agent_labels_customer=alert_details._source["data_office365_OrganizationId"],
392
+ time_field=alert_details._source.get("timestamp_utc", alert_details._source.get("timestamp")),
393
+ rule_description=alert_details._source["rule_description"],
394
+ rule_id=alert_details._source["rule_id"],
395
+ )
396
+
397
+
398
+async def create_and_update_alert_in_iris(
399
+ alert_details: Office365ThreatIntelAlertModel,
400
+ session: AsyncSession,
401
+) -> int:
402
+ """
403
+ Creates the alert, then updates the alert with the asset and IoC if available.
404
+
405
+ Args:
406
+ alert_details (Office365AlertModel): The details of the alert.
407
+ session (AsyncSession): The async session object.
408
+
409
+ Returns:
410
+ int: The ID of the created alert in IRIS.
411
+ """
412
+ logger.info("Alert does not exist in IRIS. Creating alert.")
413
+ alert_details = await create_alert_details(alert_details)
414
+ ioc_payload = await build_ioc_payload(alert_details)
415
+ logger.info(f"Alert details: {alert_details}")
416
+ iris_alert_payload = await build_alert_payload(
417
+ alert_details=alert_details,
418
+ ioc_payload=ioc_payload,
419
+ session=session,
420
+ )
421
+ logger.info(f"Alert payload: {iris_alert_payload}")
422
+
423
+ client, alert_client = await initialize_client_and_alert("DFIR-IRIS")
424
+ result = await fetch_and_validate_data(
425
+ client,
426
+ alert_client.add_alert,
427
+ iris_alert_payload.to_dict(),
428
+ )
429
+ alert_id = result["data"]["alert_id"]
430
+ logger.info(f"Successfully created alert {alert_id} in IRIS.")
431
+
432
+ await fetch_and_validate_data(
433
+ client,
434
+ alert_client.update_alert,
435
+ alert_id,
436
+ {"alert_tags": f"{alert_details.rule_id}"},
437
+ )
438
+ # Update the alert with the asset payload
439
+ await fetch_and_validate_data(
440
+ client,
441
+ alert_client.update_alert,
442
+ alert_id,
443
+ {"assets": [dict(Office365ThreatIntelIrisAsset(**iris_alert_payload.assets[0].to_dict()))]},
444
+ )
445
+ if ioc_payload:
446
+ await fetch_and_validate_data(
447
+ client,
448
+ alert_client.update_alert,
449
+ alert_id,
450
+ {"iocs": [dict(IrisIoc(**iris_alert_payload.alert_iocs[0].to_dict()))]},
451
+ )
452
+ return alert_id
453
+
454
+
455
+async def get_current_assets(client, alert_client, iris_alert_id):
456
+ result = await fetch_and_validate_data(
457
+ client,
458
+ alert_client.get_alert,
459
+ iris_alert_id,
460
+ )
461
+ return result["data"]["assets"]
462
+
463
+
464
+async def update_alert_with_assets(client, alert_client, iris_alert_id, current_assets):
465
+ await fetch_and_validate_data(
466
+ client,
467
+ alert_client.update_alert,
468
+ iris_alert_id,
469
+ {"assets": current_assets},
470
+ )
471
+
472
+
473
+async def remove_duplicate_assets(current_assets):
474
+ """
475
+ Removes duplicate assets from the given list of current_assets.
476
+
477
+ Args:
478
+ current_assets (list): A list of dictionaries representing current assets.
479
+
480
+ Returns:
481
+ list: A list of dictionaries with duplicate assets removed.
482
+ """
483
+ current_assets = list({d["asset_name"]: d for d in current_assets}.values())
484
+ current_assets_str = [json.dumps(d, sort_keys=True) for d in current_assets]
485
+ current_assets_str = list(set(current_assets_str))
486
+ current_assets = [json.loads(s) for s in current_assets_str]
487
+ return current_assets
488
+
489
+
490
+async def analyze_office365_threatintel_alerts(
491
+ monitoring_alerts: MonitoringAlerts,
492
+ customer_meta: CustomersMeta,
493
+ session: AsyncSession,
494
+) -> AlertAnalysisResponse:
495
+ """
496
+ Analyze the given Office365 Exchange Online Alert and create an alert if necessary. Otherwise update the existing alert with the asset.
497
+
498
+ 1. For each alert, extract the metadata from the Wazuh-Indexer.
499
+ 2. Check if the alert exists in IRIS. If it does, update the alert with the asset. If it does not, create the alert in IRIS.
500
+ The alert will contain the asset and IoC if available.
501
+ 3. Get the current list of assets from the alert to avoid overwriting them.
502
+
503
+ Args:
504
+ monitoring_alerts (MonitoringAlerts): The monitoring alert details.
505
+ session (AsyncSession): The database session.
506
+
507
+ Returns:
508
+ AlertAnalysisResponse: The analysis response.
509
+ """
510
+ logger.info(f"Analyzing Office365 ThreatIntel alerts: {monitoring_alerts}")
511
+ for alert in monitoring_alerts:
512
+ alert_details = await fetch_alert_details(alert)
513
+ iris_alert_id = await check_if_open_alert_exists_in_iris(alert_details, session=session)
514
+ if iris_alert_id == []:
515
+ logger.info(
516
+ f"Alert {alert_details._id} does not exist in IRIS. Creating alert.",
517
+ )
518
+ iris_alert_id = await create_and_update_alert_in_iris(
519
+ alert_details,
520
+ session,
521
+ )
522
+
523
+ logger.info(f"Alert {iris_alert_id} created in IRIS.")
524
+ await remove_alert_id(alert.alert_id, session)
525
+ es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
526
+ await add_alert_to_document(
527
+ es_client=es_client,
528
+ alert=AddAlertRequest(
529
+ alert_id=alert_details._id,
530
+ index_name=alert_details._index,
531
+ ),
532
+ soc_alert_id=iris_alert_id,
533
+ session=session,
534
+ )
535
+
536
+ else:
537
+ logger.info(
538
+ f"Alert {iris_alert_id} exists in IRIS. Updating alert with the asset.",
539
+ )
540
+
541
+ # Fetch the current list of assets from the alert to avoid overwriting them
542
+ client, alert_client = await initialize_client_and_alert("DFIR-IRIS")
543
+ current_assets = await get_current_assets(
544
+ client,
545
+ alert_client,
546
+ iris_alert_id,
547
+ )
548
+ alert_details = await create_alert_details(alert_details)
549
+ asset_payload = await build_asset_payload(
550
+ alert_details=alert_details,
551
+ session=session,
552
+ )
553
+ current_assets.append(dict(Office365ThreatIntelIrisAsset(**asset_payload.to_dict())))
554
+ current_assets = await remove_duplicate_assets(current_assets)
555
+ await update_alert_with_assets(
556
+ client,
557
+ alert_client,
558
+ iris_alert_id,
559
+ current_assets,
560
+ )
561
+ await remove_alert_id(alert.alert_id, session)
562
+ es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
563
+ await add_alert_to_document(
564
+ es_client=es_client,
565
+ alert=AddAlertRequest(
566
+ alert_id=alert.alert_id,
567
+ index_name=alert.alert_index,
568
+ ),
569
+ soc_alert_id=iris_alert_id,
570
+ session=session,
571
+ )
572
+
573
+ return AlertAnalysisResponse(
574
+ success=True,
575
+ message="Office365 alerts analyzed successfully",
576
+ )
backend/app/utils.py
+9
-3
@@ -657,9 +657,15 @@ async def get_customer_alert_settings(
657
)
658
settings = result.scalars().first()
659
660
- if settings:
661
- return settings
662
- return None
660
+ if not settings:
661
+ result = await session.execute(
662
+ select(AlertCreationSettings).filter(
663
+ AlertCreationSettings.office365_organization_id == customer_code,
664
+ ),
665
+ )
666
+ settings = result.scalars().first()
667
+
668
+ return settings
669
670
671
async def get_customer_alert_settings_office365(
frontend/cypress/e2e/example.cy.ts
+1
-1
@@ -3,6 +3,6 @@
3
describe("My First Test", () => {
4
it("visits the app root url", () => {
5
cy.visit("/")
6
- cy.contains("span", "Search")
6
+ cy.contains(".title", "SOCFortress CoPilot")
7
})
8
})
frontend/package-lock.json
+27
-27
@@ -28,7 +28,7 @@
28
"lodash": "^4.17.21",
29
"markdown-it-highlightjs": "^4.0.1",
30
"mitt": "^3.0.1",
31
- "naive-ui": "^2.37.3",
31
+ "naive-ui": "^2.38.0",
32
"password-validator": "^5.3.0",
33
"pinia": "^2.1.7",
34
"pinia-plugin-persistedstate": "^3.2.1",
@@ -38,7 +38,7 @@
38
"vue-advanced-cropper": "^2.8.8",
39
"vue-highlight-words": "^3.0.1",
40
"vue-i18n": "^9.9.1",
41
- "vue-router": "^4.2.5",
41
+ "vue-router": "^4.3.0",
42
"vue-sjv": "^0.0.6",
43
"vue3-apexcharts": "^1.5.2",
44
"vue3-marquee": "^4.2.0"
@@ -56,7 +56,7 @@
56
"@types/lodash": "^4.14.202",
57
"@types/markdown-it": "^13.0.7",
58
"@types/markdown-it-highlightjs": "^3.3.4",
59
- "@types/node": "^20.11.19",
59
+ "@types/node": "^20.11.20",
60
"@types/validator": "^13.11.9",
61
"@vitejs/plugin-vue": "^5.0.4",
62
"@vitejs/plugin-vue-jsx": "^3.1.0",
@@ -65,10 +65,10 @@
65
"@vue/test-utils": "^2.4.4",
66
"@vue/tsconfig": "^0.5.1",
67
"autoprefixer": "^10.4.17",
68
- "cypress": "^13.6.5",
68
+ "cypress": "^13.6.6",
69
"eslint": "^8.56.0",
70
"eslint-plugin-cypress": "^2.15.1",
71
- "eslint-plugin-vue": "^9.21.1",
71
+ "eslint-plugin-vue": "^9.22.0",
72
"fs-extra": "^11.2.0",
73
"ip": "^2.0.1",
74
"jsdom": "^24.0.0",
@@ -85,7 +85,7 @@
85
"ts-node": "^10.9.2",
86
"typescript": "~5.3.3",
87
"unplugin-vue-components": "^0.26.0",
88
- "vite": "^5.1.3",
88
+ "vite": "^5.1.4",
89
"vite-bundle-analyzer": "^0.8.0",
90
"vite-bundle-visualizer": "^1.0.1",
91
"vite-svg-loader": "^5.1.0",
@@ -2416,9 +2416,9 @@
2416
"dev": true
2417
},
2418
"node_modules/@types/node": {
2419
- "version": "20.11.19",
2420
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz",
2421
- "integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==",
2419
+ "version": "20.11.20",
2420
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.20.tgz",
2421
+ "integrity": "sha512-7/rR21OS+fq8IyHTgtLkDK949uzsa6n8BkziAKtPVpugIkO6D+/ooXMvzXxDnZrmtXVfjb1bKQafYpb8s89LOg==",
2422
"dev": true,
2423
"dependencies": {
2424
"undici-types": "~5.26.4"
@@ -4510,9 +4510,9 @@
4510
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
4511
},
4512
"node_modules/cypress": {
4513
- "version": "13.6.5",
4514
- "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.6.5.tgz",
4515
- "integrity": "sha512-2NxSDcO2zHw5kTcosc6dzv2zppEqiXrFFhZw5cx/EWrSNZABTzpr/EyvYzGgrWm46o5173JUfuJfDQcaiZZPVQ==",
4513
+ "version": "13.6.6",
4514
+ "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.6.6.tgz",
4515
+ "integrity": "sha512-S+2S9S94611hXimH9a3EAYt81QM913ZVA03pUmGDfLTFa5gyp85NJ8dJGSlEAEmyRsYkioS1TtnWtbv/Fzt11A==",
4516
"dev": true,
4517
"hasInstallScript": true,
4518
"dependencies": {
@@ -5595,15 +5595,15 @@
5595
}
5596
},
5597
"node_modules/eslint-plugin-vue": {
5598
- "version": "9.21.1",
5599
- "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.21.1.tgz",
5600
- "integrity": "sha512-XVtI7z39yOVBFJyi8Ljbn7kY9yHzznKXL02qQYn+ta63Iy4A9JFBw6o4OSB9hyD2++tVT+su9kQqetUyCCwhjw==",
5598
+ "version": "9.22.0",
5599
+ "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.22.0.tgz",
5600
+ "integrity": "sha512-7wCXv5zuVnBtZE/74z4yZ0CM8AjH6bk4MQGm7hZjUC2DBppKU5ioeOk5LGSg/s9a1ZJnIsdPLJpXnu1Rc+cVHg==",
5601
"dependencies": {
5602
"@eslint-community/eslint-utils": "^4.4.0",
5603
"natural-compare": "^1.4.0",
5604
"nth-check": "^2.1.1",
5605
- "postcss-selector-parser": "^6.0.13",
5606
- "semver": "^7.5.4",
5605
+ "postcss-selector-parser": "^6.0.15",
5606
+ "semver": "^7.6.0",
5607
"vue-eslint-parser": "^9.4.2",
5608
"xml-name-validator": "^4.0.0"
5609
},
@@ -8539,9 +8539,9 @@
8539
}
8540
},
8541
"node_modules/naive-ui": {
8542
- "version": "2.37.3",
8543
- "resolved": "https://registry.npmjs.org/naive-ui/-/naive-ui-2.37.3.tgz",
8544
- "integrity": "sha512-aUkHFXVIluSi8Me+npbcsdv1NYhVMj5t9YaruoCESlqmfqspj+R2QHEVXkTtUI1kQwVrABMCtAGq/wountqjZA==",
8542
+ "version": "2.38.0",
8543
+ "resolved": "https://registry.npmjs.org/naive-ui/-/naive-ui-2.38.0.tgz",
8544
+ "integrity": "sha512-Sa1hPUSTaOBvFy5hBcdQ6ajGVqt59QHN4p6wcsC5stVo6JjQPnbRTUvWtNG3gAsLhCvHQBm9NXeJG5Ne1OFQ4Q==",
8545
"dependencies": {
8546
"@css-render/plugin-bem": "^0.15.12",
8547
"@css-render/vue3-ssr": "^0.15.12",
@@ -12384,9 +12384,9 @@
12384
}
12385
},
12386
"node_modules/vite": {
12387
- "version": "5.1.3",
12388
- "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.3.tgz",
12389
- "integrity": "sha512-UfmUD36DKkqhi/F75RrxvPpry+9+tTkrXfMNZD+SboZqBCMsxKtO52XeGzzuh7ioz+Eo/SYDBbdb0Z7vgcDJew==",
12387
+ "version": "5.1.4",
12388
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.4.tgz",
12389
+ "integrity": "sha512-n+MPqzq+d9nMVTKyewqw6kSt+R3CkvF9QAKY8obiQn8g1fwTscKxyfaYnC632HtBXAQGc1Yjomphwn1dtwGAHg==",
12390
"dev": true,
12391
"dependencies": {
12392
"esbuild": "^0.19.3",
@@ -12853,11 +12853,11 @@
12853
}
12854
},
12855
"node_modules/vue-router": {
12856
- "version": "4.2.5",
12857
- "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.2.5.tgz",
12858
- "integrity": "sha512-DIUpKcyg4+PTQKfFPX88UWhlagBEBEfJ5A8XDXRJLUnZOvcpMF8o/dnL90vpVkGaPbjvXazV/rC1qBKrZlFugw==",
12856
+ "version": "4.3.0",
12857
+ "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.3.0.tgz",
12858
+ "integrity": "sha512-dqUcs8tUeG+ssgWhcPbjHvazML16Oga5w34uCUmsk7i0BcnskoLGwjpa15fqMr2Fa5JgVBrdL2MEgqz6XZ/6IQ==",
12859
"dependencies": {
12860
- "@vue/devtools-api": "^6.5.0"
12860
+ "@vue/devtools-api": "^6.5.1"
12861
},
12862
"funding": {
12863
"url": "https://github.com/sponsors/posva"
frontend/package.json
+4
-4
@@ -53,7 +53,7 @@
53
"lodash": "^4.17.21",
54
"markdown-it-highlightjs": "^4.0.1",
55
"mitt": "^3.0.1",
56
- "naive-ui": "^2.37.3",
56
+ "naive-ui": "^2.38.0",
57
"password-validator": "^5.3.0",
58
"pinia": "^2.1.7",
59
"pinia-plugin-persistedstate": "^3.2.1",
@@ -81,7 +81,7 @@
81
"@types/lodash": "^4.14.202",
82
"@types/markdown-it": "^13.0.7",
83
"@types/markdown-it-highlightjs": "^3.3.4",
84
- "@types/node": "^20.11.19",
84
+ "@types/node": "^20.11.20",
85
"@types/validator": "^13.11.9",
86
"@vitejs/plugin-vue": "^5.0.4",
87
"@vitejs/plugin-vue-jsx": "^3.1.0",
@@ -90,10 +90,10 @@
90
"@vue/test-utils": "^2.4.4",
91
"@vue/tsconfig": "^0.5.1",
92
"autoprefixer": "^10.4.17",
93
- "cypress": "^13.6.5",
93
+ "cypress": "^13.6.6",
94
"eslint": "^8.56.0",
95
"eslint-plugin-cypress": "^2.15.1",
96
- "eslint-plugin-vue": "^9.21.1",
96
+ "eslint-plugin-vue": "^9.22.0",
97
"fs-extra": "^11.2.0",
98
"ip": "^2.0.1",
99
"jsdom": "^24.0.0",
frontend/src/api/customers.ts
+36
-1
@@ -5,7 +5,8 @@ import type {
5
CustomerAgentHealth,
6
CustomerDecomissionedData,
7
CustomerMeta,
8
- CustomerProvision
8
+ CustomerProvision,
9
+ CustomerProvisioningDefaultSettings
10
} from "@/types/customers.d"
11
import type { Agent } from "@/types/agents.d"
12
@@ -15,6 +16,13 @@ export interface CustomerAgentsHealthcheckQuery {
16
days?: number
17
}
18
19
+export interface ProvisioningDefaultSettingsPayload {
20
+ clusterName: string
21
+ clusterKey: string
22
+ masterIp: string
23
+ grafanaUrl: string
24
+}
25
+
26
export default {
27
getCustomers(code?: string) {
28
return HttpClient.get<FlaskBaseResponse & { customers?: Customer[]; customer?: Customer }>(
@@ -121,5 +129,32 @@ export default {
129
return HttpClient.get<FlaskBaseResponse & { available_subscriptions: string[] }>(
130
`/customer_provisioning/provision/subscriptions`
131
)
132
+ },
133
+ getProvisioningDefaultSettings() {
134
+ return HttpClient.get<
135
+ FlaskBaseResponse & { customer_provisioning_default_settings: CustomerProvisioningDefaultSettings }
136
+ >(`/customer_provisioning/default_settings`)
137
+ },
138
+ setProvisioningDefaultSettings(payload: ProvisioningDefaultSettingsPayload) {
139
+ return HttpClient.post<
140
+ FlaskBaseResponse & { customer_provisioning_default_settings: CustomerProvisioningDefaultSettings }
141
+ >(`/customer_provisioning/default_settings`, {
142
+ id: 0,
143
+ cluster_name: payload.clusterName,
144
+ cluster_key: payload.clusterKey,
145
+ master_ip: payload.masterIp,
146
+ grafana_url: payload.grafanaUrl
147
+ })
148
+ },
149
+ updateProvisioningDefaultSettings(payload: ProvisioningDefaultSettingsPayload) {
150
+ return HttpClient.put<
151
+ FlaskBaseResponse & { customer_provisioning_default_settings: CustomerProvisioningDefaultSettings }
152
+ >(`/customer_provisioning/default_settings`, {
153
+ id: 0,
154
+ cluster_name: payload.clusterName,
155
+ cluster_key: payload.clusterKey,
156
+ master_ip: payload.masterIp,
157
+ grafana_url: payload.grafanaUrl
158
+ })
159
}
160
}
frontend/src/components/AuthForm/index.vue
-11
@@ -112,17 +112,6 @@ onBeforeMount(() => {
112
line-height: 1.3;
113
color: var(--fg-secondary-color);
114
}
115
-
116
- .social-btns {
117
- .b-icon {
118
- margin-right: 16px;
119
-
120
- img {
121
- display: block;
122
- height: 20px;
123
- }
124
- }
125
- }
115
}
116
117
.form-fade-enter-active,
frontend/src/components/activeResponse/ActiveResponseInvokeForm.vue
+1
-1
@@ -8,7 +8,7 @@
8
<n-select v-model:value="form.action" :options="invokeActionOptions" />
9
</n-form-item>
10
<n-form-item label="IP Address" path="ip">
11
- <n-input v-model:value.trim="form.ip" placeholder="Input the IP Address..." />
11
+ <n-input v-model:value.trim="form.ip" placeholder="Input the IP Address..." clearable />
12
</n-form-item>
13
</div>
14
</n-form>
frontend/src/components/customers/CustomersList.vue
+1
-1
@@ -5,7 +5,7 @@
5
Total:
6
<strong class="font-mono">{{ totalCustomers }}</strong>
7
</div>
8
- <div>
8
+ <div class="flex items-center gap-3">
9
<slot></slot>
10
</div>
11
</div>
frontend/src/components/customers/provision/CustomerDefaultSettingForm.vue
new
+239
@@ -0,0 +1,239 @@
1
+<template>
2
+ <n-spin :show="loading" class="customer-provisioning-default-settings-form">
3
+ <n-form :label-width="80" :model="form" :rules="rules" ref="formRef">
4
+ <div class="flex flex-col gap-4">
5
+ <div class="flex flex-wrap gap-4">
6
+ <div v-for="(_, key) of form" :key="key" class="grow">
7
+ <n-form-item :label="fieldsMeta[key].label" :path="key" class="grow">
8
+ <n-input
9
+ v-model:value.trim="form[key]"
10
+ :placeholder="fieldsMeta[key].placeholder"
11
+ clearable
12
+ />
13
+ </n-form-item>
14
+ </div>
15
+ </div>
16
+ <div class="flex justify-between gap-4">
17
+ <div class="flex gap-4">
18
+ <slot name="additionalActions"></slot>
19
+ </div>
20
+ <div class="flex gap-4">
21
+ <n-button @click="reset()" :disabled="loading">Reset</n-button>
22
+ <n-button
23
+ type="primary"
24
+ :disabled="!isValid"
25
+ @click="validate()"
26
+ :loading="submittingDefaultSettings"
27
+ >
28
+ Submit
29
+ </n-button>
30
+ </div>
31
+ </div>
32
+ </div>
33
+ </n-form>
34
+ </n-spin>
35
+</template>
36
+
37
+<script setup lang="ts">
38
+import { computed, onBeforeMount, onMounted, ref, watch } from "vue"
39
+import Api from "@/api"
40
+import {
41
+ useMessage,
42
+ NForm,
43
+ NFormItem,
44
+ NInput,
45
+ NButton,
46
+ NSpin,
47
+ type FormValidationError,
48
+ type FormInst,
49
+ type FormRules,
50
+ type FormItemRule
51
+} from "naive-ui"
52
+import type { CustomerProvisioningDefaultSettings } from "@/types/customers.d"
53
+import _trim from "lodash/trim"
54
+import _get from "lodash/get"
55
+import isURL from "validator/es/lib/isURL"
56
+import isIP from "validator/es/lib/isIP"
57
+
58
+const emit = defineEmits<{
59
+ (e: "update:loading", value: boolean): void
60
+ (
61
+ e: "mounted",
62
+ value: {
63
+ load: () => void
64
+ }
65
+ ): void
66
+}>()
67
+
68
+const loadingDefaultSettings = ref(false)
69
+const submittingDefaultSettings = ref(false)
70
+const loading = computed(() => loadingDefaultSettings.value || submittingDefaultSettings.value)
71
+const message = useMessage()
72
+const form = ref<Omit<CustomerProvisioningDefaultSettings, "id">>(getClearForm())
73
+const formRef = ref<FormInst | null>(null)
74
+const isNew = ref(true)
75
+
76
+const rules: FormRules = {
77
+ cluster_name: {
78
+ message: "Please input the Cluster Name",
79
+ trigger: ["input", "blur"]
80
+ },
81
+ cluster_key: {
82
+ message: "Please input the Cluster Key",
83
+ trigger: ["input", "blur"]
84
+ },
85
+ master_ip: {
86
+ validator: validateIp,
87
+ trigger: ["blur"]
88
+ },
89
+ grafana_url: {
90
+ validator: validateUrl,
91
+ trigger: ["blur"]
92
+ }
93
+}
94
+
95
+const fieldsMeta = {
96
+ cluster_name: {
97
+ label: "Cluster Name",
98
+ placeholder: "Insert the Cluster Name"
99
+ },
100
+ cluster_key: {
101
+ label: "Cluster Key",
102
+ placeholder: "Insert the Cluster Key"
103
+ },
104
+ master_ip: {
105
+ label: "Master IP",
106
+ placeholder: "Insert the Master IP"
107
+ },
108
+ grafana_url: {
109
+ label: "Grafana URL",
110
+ placeholder: "Insert the Grafana URL"
111
+ }
112
+}
113
+
114
+const isValid = computed(() => {
115
+ let valid = true
116
+
117
+ for (const key in rules) {
118
+ const rule = rules[key] as FormRules
119
+
120
+ if (rule.required && !_trim(_get(form.value, key))) {
121
+ valid = false
122
+ }
123
+ }
124
+
125
+ return valid
126
+})
127
+
128
+function validate() {
129
+ if (!formRef.value) return
130
+
131
+ formRef.value.validate((errors?: Array<FormValidationError>) => {
132
+ if (!errors) {
133
+ submit()
134
+ } else {
135
+ message.warning("You must fill in the required fields correctly.")
136
+ return false
137
+ }
138
+ })
139
+}
140
+
141
+function getClearForm(settings?: Omit<CustomerProvisioningDefaultSettings, "id">) {
142
+ return {
143
+ cluster_name: settings?.cluster_name || "",
144
+ cluster_key: settings?.cluster_key || "",
145
+ master_ip: settings?.master_ip || "",
146
+ grafana_url: settings?.grafana_url || ""
147
+ }
148
+}
149
+
150
+function reset() {
151
+ if (!loading.value) {
152
+ form.value = getClearForm()
153
+ }
154
+}
155
+
156
+function submit() {
157
+ submittingDefaultSettings.value = true
158
+
159
+ const method = isNew.value ? "setProvisioningDefaultSettings" : "updateProvisioningDefaultSettings"
160
+
161
+ const payload = {
162
+ clusterName: form.value.cluster_name,
163
+ clusterKey: form.value.cluster_key,
164
+ masterIp: form.value.master_ip,
165
+ grafanaUrl: form.value.grafana_url
166
+ }
167
+
168
+ Api.customers[method](payload)
169
+ .then(res => {
170
+ if (res.data.success) {
171
+ isNew.value = false
172
+ } else {
173
+ message.warning(res.data?.message || "An error occurred. Please try again later.")
174
+ }
175
+ })
176
+ .catch(err => {
177
+ message.error(err.response?.data?.message || "An error occurred. Please try again later.")
178
+ })
179
+ .finally(() => {
180
+ submittingDefaultSettings.value = false
181
+ })
182
+}
183
+
184
+function setForm(settings?: CustomerProvisioningDefaultSettings) {
185
+ form.value = getClearForm(settings)
186
+}
187
+
188
+function validateIp(rule: FormItemRule, value: string) {
189
+ if (value && !isIP(value)) {
190
+ return new Error("Please input a valid IP Address")
191
+ }
192
+
193
+ return true
194
+}
195
+
196
+function validateUrl(rule: FormItemRule, value: string) {
197
+ if (value && !isURL(value)) {
198
+ return new Error("Please input a valid URL")
199
+ }
200
+
201
+ return true
202
+}
203
+
204
+function getProvisioningDefaultSettings() {
205
+ loadingDefaultSettings.value = true
206
+
207
+ Api.customers
208
+ .getProvisioningDefaultSettings()
209
+ .then(res => {
210
+ if (res.data.success) {
211
+ isNew.value = false
212
+ setForm(res.data?.customer_provisioning_default_settings)
213
+ }
214
+ })
215
+ .finally(() => {
216
+ loadingDefaultSettings.value = false
217
+ })
218
+}
219
+
220
+function load() {
221
+ if (!loadingDefaultSettings.value) {
222
+ getProvisioningDefaultSettings()
223
+ }
224
+}
225
+
226
+watch(loading, val => {
227
+ emit("update:loading", val)
228
+})
229
+
230
+onBeforeMount(() => {
231
+ getProvisioningDefaultSettings()
232
+})
233
+
234
+onMounted(() => {
235
+ emit("mounted", {
236
+ load
237
+ })
238
+})
239
+</script>
frontend/src/components/customers/provision/CustomerDefaultSettingsButton.vue
new
+38
@@ -0,0 +1,38 @@
1
+<template>
2
+ <n-button size="small" secondary @click="showForm = true" :loading="loading">
3
+ <template #icon>
4
+ <Icon :name="SettingsIcon" :size="14"></Icon>
5
+ </template>
6
+ Default Settings
7
+ </n-button>
8
+
9
+ <n-modal
10
+ v-model:show="showForm"
11
+ display-directive="show"
12
+ preset="card"
13
+ :style="{ maxWidth: 'min(600px, 90vw)', minHeight: 'min(300px, 90vh)', overflow: 'hidden' }"
14
+ title="Customer Provisioning Default Settings"
15
+ :bordered="false"
16
+ segmented
17
+ >
18
+ <CustomerDefaultSettingForm @mounted="settingsFormCTX = $event" v-model:loading="loading" />
19
+ </n-modal>
20
+</template>
21
+
22
+<script setup lang="ts">
23
+import { ref, watch } from "vue"
24
+import { NButton, NModal } from "naive-ui"
25
+import Icon from "@/components/common/Icon.vue"
26
+import CustomerDefaultSettingForm from "./CustomerDefaultSettingForm.vue"
27
+
28
+const SettingsIcon = "carbon:settings-edit"
29
+const settingsFormCTX = ref<{ load: () => void } | null>(null)
30
+const showForm = ref(false)
31
+const loading = ref(false)
32
+
33
+watch(showForm, val => {
34
+ if (val) {
35
+ settingsFormCTX.value?.load()
36
+ }
37
+})
38
+</script>
frontend/src/components/customers/provision/CustomerProvisionWizard.vue
+37
-8
@@ -235,13 +235,14 @@ import {
235
type FormItemRule,
236
type FormValidationError
237
} from "naive-ui"
238
-import type { CustomerMeta, CustomerProvision } from "@/types/customers.d"
238
+import type { CustomerMeta, CustomerProvision, CustomerProvisioningDefaultSettings } from "@/types/customers.d"
239
import Icon from "@/components/common/Icon.vue"
240
import Api from "@/api"
241
import isURL from "validator/es/lib/isURL"
242
import isPort from "validator/es/lib/isPort"
243
import isIP from "validator/es/lib/isIP"
244
import { onBeforeMount } from "vue"
245
+import _uniqBy from "lodash/uniqBy"
246
247
const emit = defineEmits<{
248
(e: "update:loading", value: boolean): void
@@ -262,6 +263,7 @@ const ArrowLeftIcon = "carbon:arrow-left"
263
const loading = ref(false)
264
const loadingSubscriptions = ref(false)
265
const loadingDashboards = ref(false)
266
+const loadingDefaultSettings = ref(false)
267
const message = useMessage()
268
const current = ref<number>(1)
269
const currentStatus = ref<StepsProps["status"]>("process")
@@ -398,7 +400,7 @@ function validateAtLeastOne(rule: FormItemRule, value: string[]) {
400
return true
401
}
402
401
-function getClearForm(): CustomerProvision {
403
+function getClearForm(settings?: CustomerProvisioningDefaultSettings): CustomerProvision {
404
return {
405
// step1
406
customer_name: customerName.value,
@@ -425,10 +427,10 @@ function getClearForm(): CustomerProvision {
427
wazuh_registration_port: "",
428
wazuh_logs_port: "",
429
wazuh_api_port: "",
428
- wazuh_cluster_name: "",
429
- wazuh_cluster_key: "",
430
- wazuh_master_ip: "",
431
- grafana_url: ""
430
+ wazuh_cluster_name: settings?.cluster_name || "",
431
+ wazuh_cluster_key: settings?.cluster_key || "",
432
+ wazuh_master_ip: settings?.master_ip || "",
433
+ grafana_url: settings?.grafana_url || ""
434
}
435
}
436
@@ -446,6 +448,21 @@ function prev() {
448
current.value--
449
}
450
451
+function getProvisioningDefaultSettings() {
452
+ loadingDefaultSettings.value = true
453
+
454
+ Api.customers
455
+ .getProvisioningDefaultSettings()
456
+ .then(res => {
457
+ if (res.data.success) {
458
+ setForm(res.data?.customer_provisioning_default_settings)
459
+ }
460
+ })
461
+ .finally(() => {
462
+ loadingDefaultSettings.value = false
463
+ })
464
+}
465
+
466
function getSubscriptions() {
467
loadingSubscriptions.value = true
468
@@ -453,7 +470,10 @@ function getSubscriptions() {
470
.getProvisioningSubscriptions()
471
.then(res => {
472
if (res.data.success) {
456
- subscriptionOptions.value = (res.data?.available_subscriptions || []).map(o => ({ label: o, value: o }))
473
+ subscriptionOptions.value = _uniqBy(
474
+ (res.data?.available_subscriptions || []).map(o => ({ label: o, value: o })),
475
+ "value"
476
+ )
477
} else {
478
message.warning(res.data?.message || "An error occurred. Please try again later.")
479
}
@@ -473,7 +493,10 @@ function getDashboards() {
493
.getProvisioningDashboards()
494
.then(res => {
495
if (res.data.success) {
476
- dashboardOptions.value = (res.data?.available_dashboards || []).map(o => ({ label: o, value: o }))
496
+ dashboardOptions.value = _uniqBy(
497
+ (res.data?.available_dashboards || []).map(o => ({ label: o, value: o })),
498
+ "value"
499
+ )
500
} else {
501
message.warning(res.data?.message || "An error occurred. Please try again later.")
502
}
@@ -511,6 +534,11 @@ function reset() {
534
currentStatus.value = "process"
535
slideFormDirection.value = "right"
536
current.value = 1
537
+ setForm()
538
+}
539
+
540
+function setForm(settings?: CustomerProvisioningDefaultSettings) {
541
+ form.value = getClearForm(settings)
542
}
543
544
async function submit() {
@@ -540,6 +568,7 @@ async function submit() {
568
}
569
570
onBeforeMount(() => {
571
+ getProvisioningDefaultSettings()
572
getSubscriptions()
573
getDashboards()
574
})
frontend/src/types/customers.d.ts
+8
@@ -89,3 +89,11 @@ export interface CustomerDecomissionedData {
89
stream_deleted: string
90
index_deleted: string
91
}
92
+
93
+export interface CustomerProvisioningDefaultSettings {
94
+ id: number
95
+ cluster_name: string
96
+ cluster_key: string
97
+ master_ip: string
98
+ grafana_url: string
99
+}
frontend/src/views/Customers.vue
+2
@@ -1,6 +1,7 @@
1
<template>
2
<div class="page">
3
<CustomersList :highlight="highlight" :reload="reload" @reloaded="reload = false">
4
+ <CustomerDefaultSettingsButton />
5
<CustomerCreationButton v-model:openForm="openForm" @submitted="reload = true" />
6
</CustomersList>
7
</div>
@@ -9,6 +10,7 @@
10
<script setup lang="ts">
11
import CustomersList from "@/components/customers/CustomersList.vue"
12
import CustomerCreationButton from "@/components/customers/CustomerCreationButton.vue"
13
+import CustomerDefaultSettingsButton from "@/components/customers/provision/CustomerDefaultSettingsButton.vue"
14
import { onBeforeMount, onMounted, onUnmounted, ref } from "vue"
15
import { useRoute, useRouter } from "vue-router"
16
import { emitter } from "@/emitter"
frontend/tsconfig.vitest.json
+2
-1
@@ -2,6 +2,7 @@
2
"extends": "./tsconfig.app.json",
3
"compilerOptions": {
4
"composite": true,
5
- "types": ["node", "jsdom"]
5
+ "types": ["node", "jsdom"],
6
+ "allowImportingTsExtensions": true
7
}
8
}
frontend/vitest.config.ts
+2
-2
@@ -1,7 +1,7 @@
1
import { fileURLToPath } from "node:url"
2
import { mergeConfig, defineConfig, configDefaults } from "vitest/config"
3
-import viteConfig from "./vite.config"
4
-
3
+import viteConfig from "./vite.config.mts"
4
+// TODO: fix vite.config.mts import
5
export default mergeConfig(
6
viteConfig,
7
defineConfig({