@cryptotaxi247 / CoPilot / commits / fdef46e6

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({