@cryptotaxi247 / CoPilot / commits / 1cebd2ef

Alert limiting (#128)

* Add MonitoringAlerts model * Add monitoring alert router to app.include_router() * Add GraylogPostRequest model and update create_monitoring_alert function * Refactor create_monitoring_alert function to initialize MonitoringAlerts with specific fields * Fix alert_index assignment in create_monitoring_alert function * Add GraylogPostResponse model and update create_monitoring_alert endpoint * Add route for running analysis on monitoring alerts This commit adds a new route `/run_analysis/wazuh` to the `monitoring_alert.py` file. The route is used to run analysis on the monitoring alerts. It retrieves all the monitoring alerts from the database where the customer code matches the provided code and the alert source is WAZUH. The retrieved alerts are then analyzed using the `analyze_wazuh_alerts` function. The commit also includes the addition of the `WazuhAnalysisResponse` model in the `monitoring_alert.py` file. This model represents the response containing the analysis results. * Refactor monitoring_alert.py and schema/monitoring_alert.py * Add alert creation and analysis functionality * Add IrisTags and FilterAlertsRequest models * Add asset and IOC payload to alert creation in IRIS * Add alert_status_id field to FilterAlertsRequest and construct_params * Refactor monitoring_alert module and add new routes and precommit fixes * Add WazuhIrisAlertContext and WazuhIrisAlertPayload classes and remove duplicate assets from the asset list

taylor_socfortress committed Feb 3, 2024 at 10:20 UTC 1cebd2ef26dc8cdfce82963579f63e1e7aba37e0
8 files changed +877
backend/app/db/all_models.py
+1
@@ -12,3 +12,4 @@ from app.integrations.alert_creation_settings.models.alert_creation_settings imp
12 )
13 from app.integrations.models.customer_integration_settings import CustomerIntegrations
14 from app.schedulers.models.scheduler import JobMetadata
15 +from app.integrations.monitoring_alert.models.monitoring_alert import MonitoringAlerts
backend/app/integrations/alert_creation/general/schema/alert.py
+7
@@ -179,6 +179,13 @@ class IrisIoc(BaseModel):
179 return self.dict(exclude_none=True)
180
181
182 +class IrisTags(BaseModel):
183 + rule_id: str = Field(..., description="Rule ID from the alert", example="001")
184 +
185 + def to_dict(self):
186 + return self.dict(exclude_none=True)
187 +
188 +
189 class IrisAlertContext(BaseModel):
190 customer_iris_id: int = Field(
191 ...,
backend/app/integrations/monitoring_alert/models/monitoring_alert.py new
+13
@@ -0,0 +1,13 @@
1 +from typing import Optional
2 +
3 +from sqlmodel import Field
4 +from sqlmodel import SQLModel
5 +
6 +
7 +class MonitoringAlerts(SQLModel, table=True):
8 + __tablename__ = "monitoring_alerts"
9 + id: Optional[int] = Field(default=None, primary_key=True)
10 + alert_id: str = Field(max_length=1024, nullable=False)
11 + alert_index: str = Field(max_length=1024, nullable=False)
12 + customer_code: str = Field(max_length=50, nullable=False)
13 + alert_source: str = Field(max_length=1024, nullable=False)
backend/app/integrations/monitoring_alert/routes/monitoring_alert.py new
+157
@@ -0,0 +1,157 @@
1 +from typing import List
2 +
3 +from fastapi import APIRouter
4 +from fastapi import Depends
5 +from fastapi import HTTPException
6 +from fastapi import Security
7 +from loguru import logger
8 +from sqlalchemy.ext.asyncio import AsyncSession
9 +from sqlalchemy.future import select
10 +
11 +from app.auth.utils import AuthHandler
12 +from app.db.db_session import get_db
13 +from app.db.universal_models import CustomersMeta
14 +from app.integrations.monitoring_alert.models.monitoring_alert import MonitoringAlerts
15 +from app.integrations.monitoring_alert.schema.monitoring_alert import GraylogPostRequest
16 +from app.integrations.monitoring_alert.schema.monitoring_alert import (
17 + GraylogPostResponse,
18 +)
19 +from app.integrations.monitoring_alert.schema.monitoring_alert import (
20 + MonitoringAlertsRequestModel,
21 +)
22 +from app.integrations.monitoring_alert.schema.monitoring_alert import (
23 + WazuhAnalysisResponse,
24 +)
25 +from app.integrations.monitoring_alert.services.wazuh import analyze_wazuh_alerts
26 +
27 +monitoring_alerts_router = APIRouter()
28 +
29 +
30 +async def get_customer_meta(customer_code: str, session: AsyncSession) -> CustomersMeta:
31 + """
32 + Get the customer meta for the given customer_code.
33 +
34 + Args:
35 + customer_code (str): The customer code.
36 + session (AsyncSession): The database session.
37 +
38 + Returns:
39 + CustomersMeta: The customer meta.
40 + """
41 + logger.info(f"Getting customer meta for customer_code: {customer_code}")
42 +
43 + customer_meta = await session.execute(select(CustomersMeta).where(CustomersMeta.customer_code == customer_code))
44 + customer_meta = customer_meta.scalars().first()
45 +
46 + if not customer_meta:
47 + raise HTTPException(status_code=404, detail="Customer not found")
48 +
49 + return customer_meta
50 +
51 +
52 +@monitoring_alerts_router.get(
53 + "/list",
54 + response_model=List[MonitoringAlertsRequestModel],
55 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
56 +)
57 +async def list_monitoring_alerts(
58 + session: AsyncSession = Depends(get_db),
59 +) -> List[MonitoringAlertsRequestModel]:
60 + """
61 + List all monitoring alerts.
62 +
63 + Args:
64 + session (AsyncSession, optional): The database session. Defaults to Depends(get_db).
65 +
66 + Returns:
67 + List[MonitoringAlertsRequestModel]: The list of monitoring alerts.
68 + """
69 + logger.info("Listing monitoring alerts")
70 +
71 + monitoring_alerts = await session.execute(select(MonitoringAlerts))
72 + monitoring_alerts = monitoring_alerts.scalars().all()
73 +
74 + return monitoring_alerts
75 +
76 +
77 +@monitoring_alerts_router.post("/create", response_model=GraylogPostResponse)
78 +async def create_monitoring_alert(
79 + monitoring_alert: GraylogPostRequest,
80 + session: AsyncSession = Depends(get_db),
81 +) -> GraylogPostResponse:
82 + """
83 + Create a new monitoring alert. This receives the alert from Graylog and stores it in the database.
84 +
85 + Args:
86 + monitoring_alert (MonitoringAlertsRequestModel): The monitoring alert details.
87 + session (AsyncSession, optional): The database session. Defaults to Depends(get_db).
88 +
89 + Returns:
90 + MonitoringAlertsRequestModel: The created monitoring alert.
91 + """
92 + logger.info(f"Creating monitoring alert: {monitoring_alert}")
93 + logger.info(f"Found index name {monitoring_alert.event.alert_index}")
94 +
95 + customer_meta = await session.execute(
96 + select(CustomersMeta).where(CustomersMeta.customer_code == monitoring_alert.event.fields.CUSTOMER_CODE),
97 + )
98 + customer_meta = customer_meta.scalars().first()
99 +
100 + if not customer_meta:
101 + raise HTTPException(status_code=404, detail="Customer not found")
102 +
103 + try:
104 + monitoring_alert = MonitoringAlerts(
105 + alert_id=monitoring_alert.event.fields.ALERT_ID,
106 + alert_index=monitoring_alert.event.alert_index,
107 + customer_code=monitoring_alert.event.fields.CUSTOMER_CODE,
108 + alert_source=monitoring_alert.event.fields.ALERT_SOURCE,
109 + )
110 + session.add(monitoring_alert)
111 + await session.commit()
112 + await session.refresh(monitoring_alert)
113 + except Exception as e:
114 + logger.error(f"Error creating monitoring alert: {e}")
115 + raise HTTPException(status_code=500, detail="Error creating monitoring alert")
116 +
117 + return GraylogPostResponse(success=True, message="Monitoring alert created successfully")
118 +
119 +
120 +@monitoring_alerts_router.post("/run_analysis/wazuh", response_model=WazuhAnalysisResponse)
121 +async def run_analysis(
122 + customer_code: str,
123 + session: AsyncSession = Depends(get_db),
124 +) -> WazuhAnalysisResponse:
125 + """
126 + This route is used to run analysis on the monitoring alerts.
127 +
128 + 1. Get all the monitoring alerts from the database where the customer_code matches the customer_code provided
129 + and the alert_source is WAZUH.
130 +
131 + 2. Call the anlayze_wazuh_alerts function to analyze the alerts.
132 +
133 + Args:
134 + customer_code (str): The customer code.
135 + session (AsyncSession, optional): The database session. Defaults to Depends(get_db).
136 +
137 + Returns:
138 + WazuhAnalysisResponse: The response containing the analysis results.
139 + """
140 + logger.info(f"Running analysis for customer_code: {customer_code}")
141 +
142 + customer_meta = await get_customer_meta(customer_code, session)
143 +
144 + monitoring_alerts = await session.execute(
145 + select(MonitoringAlerts).where(MonitoringAlerts.customer_code == customer_code and MonitoringAlerts.alert_source == "WAZUH"),
146 + )
147 + monitoring_alerts = monitoring_alerts.scalars().all()
148 +
149 + logger.info(f"Found {len(monitoring_alerts)} monitoring alerts")
150 +
151 + if not monitoring_alerts:
152 + raise HTTPException(status_code=404, detail="No monitoring alerts found")
153 +
154 + # Call the analyze_wazuh_alerts function to analyze the alerts
155 + await analyze_wazuh_alerts(monitoring_alerts, customer_meta, session)
156 +
157 + return WazuhAnalysisResponse(success=True, message="Analysis completed successfully")
backend/app/integrations/monitoring_alert/schema/monitoring_alert.py new
+222
@@ -0,0 +1,222 @@
1 +from enum import Enum
2 +from typing import Any
3 +from typing import Dict
4 +from typing import List
5 +from typing import Optional
6 +
7 +from pydantic import BaseModel
8 +from pydantic import Extra
9 +from pydantic import Field
10 +
11 +from app.integrations.alert_creation.general.schema.alert import IrisAsset
12 +from app.integrations.alert_creation.general.schema.alert import IrisIoc
13 +
14 +
15 +class MonitoringAlertsRequestModel(BaseModel):
16 + id: Optional[int] = None
17 + alert_id: str
18 + alert_index: str
19 + customer_code: str
20 + alert_source: str
21 +
22 + class Config:
23 + orm_mode = True
24 +
25 +
26 +class GraylogEventFields(BaseModel):
27 + ALERT_ID: str = Field(..., description="Unique identifier for the alert", example="65f6a260-c1f3-11ee-93bc-86000046278a")
28 + ALERT_SOURCE: str = Field(..., description="Source of the alert", example="WAZUH")
29 + CUSTOMER_CODE: str = Field(..., description="Customer code associated with the alert", example="00002")
30 +
31 +
32 +class GraylogEvent(BaseModel):
33 + id: str = Field(..., description="Unique identifier for the event", example="01HNNF2YCM5SSV3KDQJSRK0EV0")
34 + event_definition_type: str = Field(..., description="Type of event definition", example="aggregation-v1")
35 + event_definition_id: str = Field(..., description="Identifier for the event definition", example="65bd28505e9a2d550cf521e7")
36 + origin_context: str = Field(
37 + ...,
38 + description="Context from which the event originated",
39 + example="urn:graylog:message:es:wazuh_00002_290:65f6a260-c1f3-11ee-93bc-86000046278a",
40 + )
41 + timestamp: str = Field(..., description="Timestamp when the event occurred", example="2024-02-02T17:49:22.694Z")
42 + timestamp_processing: str = Field(..., description="Timestamp when the event was processed", example="2024-02-02T17:50:26.708Z")
43 + timerange_start: Optional[str] = Field(None, description="Start of the timerange for the event", example=None)
44 + timerange_end: Optional[str] = Field(None, description="End of the timerange for the event", example=None)
45 + streams: List[str] = Field(..., description="List of streams associated with the event", example=[])
46 + source_streams: List[str] = Field(..., description="List of source streams for the event", example=["645a3a6123e5cc30bbc0e5dc"])
47 + message: str = Field(..., description="Message associated with the event", example="COPILOT TESTING WAZUH")
48 + source: str = Field(..., description="Source of the event", example="ASHGRL02")
49 + key_tuple: List[str] = Field(..., description="Tuple keys associated with the event", example=[])
50 + key: str = Field(..., description="Key associated with the event", example="")
51 + priority: int = Field(..., description="Priority of the event", example=2)
52 + alert: bool = Field(..., description="Indicates if the event is an alert", example=True)
53 + fields: GraylogEventFields = Field(..., description="Custom fields for the event")
54 + group_by_fields: Dict[str, Any] = Field(..., description="Fields used to group events", example={})
55 +
56 + @property
57 + def alert_index(self) -> str:
58 + return self.origin_context.split(":")[4]
59 +
60 +
61 +class GraylogPostRequest(BaseModel):
62 + event_definition_id: str = Field(..., description="Identifier for the event definition", example="65bd28505e9a2d550cf521e7")
63 + event_definition_type: str = Field(..., description="Type of the event definition", example="aggregation-v1")
64 + event_definition_title: str = Field(..., description="Title of the event definition", example="COPILOT TESTING WAZUH")
65 + event_definition_description: Optional[str] = Field(None, description="Description of the event definition", example="")
66 + job_definition_id: str = Field(..., description="Identifier for the job definition", example="65bd284b5e9a2d550cf521dc")
67 + job_trigger_id: str = Field(..., description="Identifier for the job trigger", example="65bd2b625e9a2d550cf528e4")
68 + event: GraylogEvent = Field(..., description="Event details")
69 + backlog: List[str] = Field(..., description="List of backlog items associated with the event", example=[])
70 +
71 +
72 +class GraylogPostResponse(BaseModel):
73 + success: bool = Field(..., description="Indicates if the request was successful", example=True)
74 + message: str = Field(..., description="Message associated with the response", example="Event processed successfully")
75 +
76 +
77 +class WazuhAnalysisResponse(BaseModel):
78 + success: bool = Field(..., description="Indicates if the request was successful", example=True)
79 + message: str = Field(..., description="Message associated with the response", example="Analysis completed successfully")
80 +
81 +
82 +# ! Wazuh Indexer Schema ! #
83 +class WazuhSourceModel(BaseModel):
84 + agent_name: str = Field(..., description="The name of the agent.")
85 + agent_id: str = Field(..., description="The id of the agent.")
86 + agent_labels_customer: str = Field(..., description="The customer of the agent.")
87 + rule_id: str = Field(..., description="The id of the rule.")
88 + rule_level: int = Field(..., description="The level of the rule.")
89 + rule_description: str = Field(..., description="The description of the rule.")
90 + timestamp: str = Field(..., description="The timestamp of the alert.")
91 + timestamp_utc: Optional[str] = Field(
92 + None,
93 + description="The UTC timestamp of the alert.",
94 + )
95 +
96 + class Config:
97 + extra = Extra.allow
98 +
99 +
100 +class WazuhAlertModel(BaseModel):
101 + _index: str
102 + _id: str
103 + _version: int
104 + _source: WazuhSourceModel
105 + asset_type_id: Optional[int] = Field(
106 + None,
107 + description="The asset type id of the alert which is needed for when we add the asset to IRIS.",
108 + )
109 + ioc_value: Optional[str] = Field(
110 + None,
111 + description="The IoC value of the alert which is needed for when we add the IoC to IRIS.",
112 + )
113 + ioc_type: Optional[str] = Field(
114 + None,
115 + description="The IoC type of the alert which is needed for when we add the IoC to IRIS.",
116 + )
117 +
118 + class Config:
119 + extra = Extra.allow
120 +
121 +
122 +class SortOrder(Enum):
123 + desc = "desc"
124 + asc = "asc"
125 +
126 +
127 +class FilterAlertsRequest(BaseModel):
128 + per_page: int = Field(1000, description="The number of alerts to return per page.")
129 + page: int = Field(1, description="The page number to return.")
130 + sort: SortOrder = Field(SortOrder.desc, description="The sort order for the alerts.")
131 + alert_tags: str = Field(..., description="The tags of the alert.")
132 + alert_status_id: int = Field(3, description="The status of the alert. Default to assigned.", example=3)
133 +
134 +
135 +class WazuhIrisAlertContext(BaseModel):
136 + customer_iris_id: int = Field(
137 + ...,
138 + description="IRIS ID of the customer",
139 + example=1,
140 + )
141 + customer_name: str = Field(
142 + ...,
143 + description="Name of the customer",
144 + example="SOCFortress",
145 + )
146 + customer_cases_index: str = Field(
147 + ...,
148 + description="IRIS case index name in the Wazuh-Indexer",
149 + example="dfir_iris_00001",
150 + )
151 + alert_name: str = Field(
152 + ...,
153 + description="Name of the alert",
154 + example="Intrusion Detected",
155 + )
156 + alert_level: int = Field(..., description="Severity level of the alert", example=3)
157 + rule_id: str = Field(
158 + ...,
159 + description="ID of the rule that triggered the alert",
160 + example="2001",
161 + )
162 + rule_mitre_id: Optional[str] = Field(
163 + "n/a",
164 + description="MITRE ATT&CK ID of the rule",
165 + example="T1234",
166 + )
167 + rule_mitre_tactic: Optional[str] = Field(
168 + "n/a",
169 + description="MITRE ATT&CK Tactic",
170 + example="Execution",
171 + )
172 + rule_mitre_technique: Optional[str] = Field(
173 + "n/a",
174 + description="MITRE ATT&CK Technique",
175 + example="Scripting",
176 + )
177 +
178 +
179 +class WazuhIrisAlertPayload(BaseModel):
180 + alert_title: str = Field(
181 + ...,
182 + description="Title of the alert",
183 + example="Intrusion Detected",
184 + )
185 + alert_description: str = Field(
186 + ...,
187 + description="Description of the alert",
188 + example="Intrusion Detected by Firewall",
189 + )
190 + alert_source: str = Field(..., description="Source of the alert", example="Wazuh")
191 + assets: List[IrisAsset] = Field(..., description="List of affected assets")
192 + alert_status_id: int = Field(..., description="Status ID of the alert", example=3)
193 + alert_severity_id: int = Field(
194 + ...,
195 + description="Severity ID of the alert",
196 + example=5,
197 + )
198 + alert_customer_id: int = Field(
199 + ...,
200 + description="Customer ID related to the alert",
201 + example=1,
202 + )
203 + alert_source_content: Dict[str, Any] = Field(
204 + ...,
205 + description="Original content from the alert source",
206 + )
207 + alert_context: WazuhIrisAlertContext = Field(
208 + ...,
209 + description="Contextual information about the alert",
210 + )
211 + alert_iocs: Optional[List[IrisIoc]] = Field(
212 + None,
213 + description="List of IoCs related to the alert",
214 + )
215 + alert_source_event_time: str = Field(
216 + ...,
217 + description="Timestamp of the alert",
218 + example="2021-01-01T00:00:00.000Z",
219 + )
220 +
221 + def to_dict(self):
222 + return self.dict(exclude_none=True)
backend/app/integrations/monitoring_alert/services/wazuh.py new
+464
@@ -0,0 +1,464 @@
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.agents.routes.agents import get_agent
10 +from app.agents.schema.agents import AgentsResponse
11 +from app.connectors.dfir_iris.utils.universal import fetch_and_validate_data
12 +from app.connectors.dfir_iris.utils.universal import initialize_client_and_alert
13 +from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client
14 +from app.db.universal_models import CustomersMeta
15 +from app.integrations.alert_creation.general.schema.alert import CreateAlertRequest
16 +from app.integrations.alert_creation.general.schema.alert import IrisAsset
17 +from app.integrations.alert_creation.general.schema.alert import IrisIoc
18 +from app.integrations.alert_creation.general.schema.alert import ValidIocFields
19 +from app.integrations.alert_creation.general.services.alert_multi_exclude import (
20 + AlertDetailsService,
21 +)
22 +from app.integrations.monitoring_alert.models.monitoring_alert import MonitoringAlerts
23 +from app.integrations.monitoring_alert.schema.monitoring_alert import (
24 + FilterAlertsRequest,
25 +)
26 +from app.integrations.monitoring_alert.schema.monitoring_alert import WazuhAlertModel
27 +from app.integrations.monitoring_alert.schema.monitoring_alert import (
28 + WazuhAnalysisResponse,
29 +)
30 +from app.integrations.monitoring_alert.schema.monitoring_alert import (
31 + WazuhIrisAlertContext,
32 +)
33 +from app.integrations.monitoring_alert.schema.monitoring_alert import (
34 + WazuhIrisAlertPayload,
35 +)
36 +from app.integrations.utils.alerts import get_asset_type_id
37 +from app.integrations.utils.alerts import validate_ioc_type
38 +from app.utils import get_customer_alert_settings
39 +
40 +
41 +def valid_ioc_fields() -> Set[str]:
42 + """
43 + Getter for the set of valid IoC fields.
44 + Returns
45 + -------
46 + Set[str]
47 + The set of valid IoC fields.
48 + """
49 + return {field.value for field in ValidIocFields}
50 +
51 +
52 +async def construct_alert_source_link(alert_details: CreateAlertRequest, session: AsyncSession) -> str:
53 + """
54 + Construct the alert source link for the alert details.
55 + Parameters
56 + ----------
57 + alert_details: CreateAlertRequest
58 + The alert details.
59 + Returns
60 + -------
61 + str
62 + The alert source link.
63 + """
64 + # Check if the alert has a process id and that it is not "No process ID found"
65 + if hasattr(alert_details, "process_id") and alert_details.process_id != "No process ID found":
66 + query_string = f"%22query%22:%22process_id:%5C%22{alert_details.process_id}%5C%22%20AND%20"
67 + else:
68 + query_string = f"%22query%22:%22_id:%5C%22{alert_details.id}%5C%22%20AND%20"
69 +
70 + grafana_url = (await get_customer_alert_settings(customer_code=alert_details.agent_labels_customer, session=session)).grafana_url
71 +
72 + return (
73 + f"{grafana_url}/explore?left=%5B%22now-6h%22,%22now%22,%22WAZUH%22,%7B%22refId%22:%22A%22,"
74 + f"{query_string}"
75 + f"agent_name:%5C%22{alert_details.agent_name}%5C%22%22,"
76 + "%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,"
77 + "%22bucketAggs%22:%5B%5D,%22timeField%22:%22timestamp%22%7D%5D"
78 + )
79 +
80 +
81 +async def build_ioc_payload(alert_details: CreateAlertRequest) -> Optional[IrisIoc]:
82 + """
83 + Builds an IoC payload based on the provided alert details.
84 +
85 + Args:
86 + alert_details (CreateAlertRequest): The details of the alert.
87 +
88 + Returns:
89 + Optional[IrisIoc]: The constructed IoC payload, or None if no valid IoC fields are found.
90 + """
91 + for field in valid_ioc_fields():
92 + if hasattr(alert_details, field):
93 + ioc_value = getattr(alert_details, field)
94 + ioc_type = await validate_ioc_type(ioc_value=ioc_value)
95 + return IrisIoc(
96 + ioc_value=ioc_value,
97 + ioc_description="IoC found in alert",
98 + ioc_tlp_id=1,
99 + ioc_type_id=ioc_type,
100 + )
101 + return None
102 +
103 +
104 +async def build_asset_payload(agent_data: AgentsResponse, alert_details: CreateAlertRequest, session: AsyncSession) -> IrisAsset:
105 + """
106 + Build the payload for an IrisAsset object based on the agent data and alert details.
107 +
108 + Args:
109 + agent_data (AgentsResponse): The response containing agent data.
110 + alert_details: The details of the alert.
111 +
112 + Returns:
113 + IrisAsset: The constructed IrisAsset object.
114 + """
115 + # Get the agent_id based on the hostname from the Agents table
116 + if agent_data.success:
117 + return IrisAsset(
118 + asset_name=agent_data.agents[0].hostname,
119 + asset_ip=agent_data.agents[0].ip_address,
120 + asset_description=await construct_alert_source_link(alert_details, session=session),
121 + asset_type_id=await get_asset_type_id(agent_data.agents[0].os),
122 + asset_tags=f"agent_id:{agent_data.agents[0].agent_id}",
123 + )
124 + return IrisAsset()
125 +
126 +
127 +async def fetch_wazuh_indexer_details(alert_id: str, index: str) -> WazuhAlertModel:
128 + """
129 + Fetch the Wazuh alert details from the Wazuh-Indexer.
130 +
131 + Args:
132 + alert_id (str): The alert ID.
133 + index (str): The index.
134 +
135 + Returns:
136 + CollectAlertsResponse: The response from the Wazuh-Indexer.
137 + """
138 + logger.info(f"Fetching Wazuh alert details for alert_id: {alert_id} and index: {index}")
139 +
140 + es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
141 + response = es_client.get(index=index, id=alert_id)
142 +
143 + return WazuhAlertModel(**response)
144 +
145 +
146 +async def fetch_alert_details(alert: MonitoringAlerts) -> WazuhAlertModel:
147 + logger.info(f"Analyzing Wazuh alert: {alert.alert_id}")
148 + alert_details = await fetch_wazuh_indexer_details(alert.alert_id, alert.alert_index)
149 + logger.info(f"Alert details: {alert_details}")
150 + return alert_details
151 +
152 +
153 +async def check_event_exclusion(alert_details: WazuhAlertModel, alert_detail_service: AlertDetailsService, session: AsyncSession):
154 + event_exclude_result = await alert_detail_service.collect_alert_timeline_process_id(
155 + agent_name=alert_details._source["agent_name"],
156 + process_id=getattr(alert_details._source, "process_id", "n/a"),
157 + index=alert_details._index,
158 + session=session,
159 + )
160 + if event_exclude_result is True:
161 + raise HTTPException(
162 + status_code=400,
163 + detail="Alert excluded due to multi exclusion as set in the config.ini file.",
164 + )
165 + logger.info("Alert is not excluded due to multi exclusion.")
166 +
167 +
168 +async def check_if_open_alert_exists_in_iris(alert_details: WazuhAlertModel) -> list:
169 + """
170 + Check if the alert exists in IRIS.
171 +
172 + Args:
173 + alert_details (WazuhAlertModel): The alert details.
174 + session (AsyncSession): The database session.
175 +
176 + Returns:
177 + bool: True if the alert exists in IRIS, False otherwise.
178 + """
179 + client, alert_client = await initialize_client_and_alert("DFIR-IRIS")
180 + request = FilterAlertsRequest(alert_tags=alert_details._source["rule_id"])
181 + params = construct_params(request)
182 + alert_exists = await fetch_and_validate_data(client, lambda: alert_client.filter_alerts(**params))
183 + logger.info(f"Alert exists: {alert_exists['data']['alerts']}")
184 + return alert_exists["data"]["alerts"][0]["alert_id"] if alert_exists["data"]["alerts"] else []
185 +
186 +
187 +def construct_params(request: FilterAlertsRequest) -> dict:
188 + """
189 + Constructs the parameters for the alert filtering request.
190 +
191 + Args:
192 + request (FilterAlertsRequest): The request object containing filtering criteria.
193 +
194 + Returns:
195 + dict: A dictionary of parameters for the alert filtering request.
196 + """
197 + params = {
198 + "page": request.page,
199 + "per_page": request.per_page,
200 + "sort": request.sort,
201 + "alert_tags": request.alert_tags,
202 + "alert_status_id": request.alert_status_id,
203 + # Add more parameters here as needed
204 + }
205 +
206 + # Remove parameters that have a value of None
207 + return {k: v for k, v in params.items() if v is not None}
208 +
209 +
210 +async def build_alert_context_payload(
211 + alert_details: CreateAlertRequest,
212 + agent_data: AgentsResponse,
213 + session: AsyncSession,
214 +) -> WazuhIrisAlertContext:
215 + """
216 + Builds the payload for the alert context.
217 +
218 + Args:
219 + alert_details (CreateAlertRequest): The details of the alert.
220 + agent_data (AgentsResponse): The agent data.
221 + session (AsyncSession): The async session.
222 +
223 + Returns:
224 + WazuhIrisAlertContext: The built alert context payload.
225 + """
226 + return WazuhIrisAlertContext(
227 + customer_iris_id=(
228 + await get_customer_alert_settings(customer_code=alert_details.agent_labels_customer, session=session)
229 + ).iris_customer_id,
230 + customer_name=(await get_customer_alert_settings(customer_code=alert_details.agent_labels_customer, session=session)).customer_name,
231 + customer_cases_index=(
232 + await get_customer_alert_settings(customer_code=alert_details.agent_labels_customer, session=session)
233 + ).iris_index,
234 + alert_name=alert_details.rule_description,
235 + alert_level=alert_details.rule_level,
236 + rule_id=alert_details.rule_id,
237 + rule_mitre_id=getattr(alert_details, "rule_mitre_id", "No rule mitre id found"),
238 + rule_mitre_tactic=getattr(
239 + alert_details,
240 + "rule_mitre_tactic",
241 + "No rule mitre tactic found",
242 + ),
243 + rule_mitre_technique=getattr(
244 + alert_details,
245 + "rule_mitre_technique",
246 + "No rule mitre technique found",
247 + ),
248 + )
249 +
250 +
251 +async def build_alert_payload(
252 + alert_details: CreateAlertRequest,
253 + agent_data,
254 + ioc_payload: Optional[IrisIoc],
255 + session: AsyncSession,
256 +) -> WazuhIrisAlertPayload:
257 + """
258 + Builds the payload for an alert based on the provided alert details, agent data, IoC payload, and session.
259 +
260 + Args:
261 + alert_details (CreateAlertRequest): The details of the alert.
262 + agent_data: The agent data associated with the alert.
263 + ioc_payload (Optional[IrisIoc]): The IoC payload associated with the alert.
264 + session (AsyncSession): The session used for database operations.
265 +
266 + Returns:
267 + WazuhIrisAlertPayload: The built alert payload.
268 + """
269 + asset_payload = await build_asset_payload(agent_data, alert_details=alert_details, session=session)
270 + context_payload = await build_alert_context_payload(alert_details=alert_details, agent_data=agent_data, session=session)
271 + timefield = (await get_customer_alert_settings(customer_code=alert_details.agent_labels_customer, session=session)).timefield
272 + # Get the timefield value from the alert_details
273 + if hasattr(alert_details, timefield):
274 + alert_details.time_field = getattr(alert_details, timefield)
275 + logger.info(f"Alert has context: {context_payload}")
276 + if ioc_payload:
277 + logger.info(f"Alert has IoC: {ioc_payload}")
278 + return WazuhIrisAlertPayload(
279 + alert_title=alert_details.rule_description,
280 + alert_description=alert_details.rule_description,
281 + alert_source="COPILOT WAZUH ANALYSIS",
282 + assets=[asset_payload],
283 + alert_status_id=3,
284 + alert_severity_id=5,
285 + alert_customer_id=(
286 + await get_customer_alert_settings(customer_code=alert_details.agent_labels_customer, session=session)
287 + ).iris_customer_id,
288 + alert_source_content=alert_details.to_dict(),
289 + alert_context=context_payload,
290 + alert_iocs=[ioc_payload],
291 + alert_source_event_time=alert_details.time_field,
292 + )
293 + else:
294 + logger.info("Alert does not have IoC")
295 + return WazuhIrisAlertPayload(
296 + alert_title=alert_details.rule_description,
297 + alert_description=alert_details.rule_description,
298 + alert_source="COPILOT WAZUH ANALYSIS",
299 + assets=[asset_payload],
300 + alert_status_id=3,
301 + alert_severity_id=5,
302 + alert_customer_id=(
303 + await get_customer_alert_settings(customer_code=alert_details.agent_labels_customer, session=session)
304 + ).iris_customer_id,
305 + alert_source_content=alert_details.to_dict(),
306 + alert_context=context_payload,
307 + alert_source_event_time=alert_details.time_field,
308 + )
309 +
310 +
311 +async def create_alert_details(alert_details: WazuhAlertModel) -> CreateAlertRequest:
312 + """
313 + Create an alert details object from the Wazuh alert details.
314 +
315 + Args:
316 + alert_details (WazuhAlertModel): The Wazuh alert details.
317 +
318 + Returns:
319 + CreateAlertRequest: The alert details object.
320 + """
321 + return CreateAlertRequest(
322 + index=alert_details._index,
323 + id=alert_details._id,
324 + rule_id=alert_details._source["rule_id"],
325 + rule_level=alert_details._source["rule_level"],
326 + rule_description=alert_details._source["rule_description"],
327 + agent_name=alert_details._source["agent_name"],
328 + agent_ip=alert_details._source["agent_ip"],
329 + agent_id=alert_details._source["agent_id"],
330 + agent_labels_customer=alert_details._source["agent_labels_customer"],
331 + timestamp=alert_details._source["timestamp"],
332 + timestamp_utc=alert_details._source["timestamp_utc"],
333 + process_id=alert_details._source.get("process_id", "No process ID found"),
334 + )
335 +
336 +
337 +async def create_and_update_alert_in_iris(alert_details: WazuhAlertModel, session: AsyncSession) -> int:
338 + """
339 + Creates the alert, then updates the alert with the asset and IoC if available.
340 +
341 + Args:
342 + alert_details (WazuhAlertModel): The details of the alert.
343 + session (AsyncSession): The async session object.
344 +
345 + Returns:
346 + int: The ID of the created alert in IRIS.
347 + """
348 + logger.info("Alert does not exist in IRIS. Creating alert.")
349 + alert_details = await create_alert_details(alert_details)
350 + agent_details = await get_agent(alert_details.agent_id, session)
351 + ioc_payload = await build_ioc_payload(alert_details)
352 + iris_alert_payload = await build_alert_payload(
353 + alert_details=alert_details,
354 + agent_data=agent_details,
355 + ioc_payload=ioc_payload,
356 + session=session,
357 + )
358 + client, alert_client = await initialize_client_and_alert("DFIR-IRIS")
359 + result = await fetch_and_validate_data(
360 + client,
361 + alert_client.add_alert,
362 + iris_alert_payload.to_dict(),
363 + )
364 + alert_id = result["data"]["alert_id"]
365 + logger.info(f"Successfully created alert {alert_id} in IRIS.")
366 + await fetch_and_validate_data(
367 + client,
368 + alert_client.update_alert,
369 + alert_id,
370 + {"alert_tags": f"{alert_details.rule_id}"},
371 + )
372 + # Update the alert with the asset payload
373 + await fetch_and_validate_data(
374 + client,
375 + alert_client.update_alert,
376 + alert_id,
377 + {"assets": [dict(IrisAsset(**iris_alert_payload.assets[0].to_dict()))]},
378 + )
379 + if ioc_payload:
380 + await fetch_and_validate_data(
381 + client,
382 + alert_client.update_alert,
383 + alert_id,
384 + {"iocs": [dict(IrisIoc(**iris_alert_payload.alert_iocs[0].to_dict()))]},
385 + )
386 + return alert_id
387 +
388 +
389 +async def get_current_assets(client, alert_client, iris_alert_id):
390 + result = await fetch_and_validate_data(client, alert_client.get_alert, iris_alert_id)
391 + return result["data"]["assets"]
392 +
393 +
394 +async def update_alert_with_assets(client, alert_client, iris_alert_id, current_assets):
395 + await fetch_and_validate_data(
396 + client,
397 + alert_client.update_alert,
398 + iris_alert_id,
399 + {"assets": current_assets},
400 + )
401 +
402 +
403 +async def remove_duplicate_assets(current_assets):
404 + """
405 + Removes duplicate assets from the given list of current_assets.
406 +
407 + Args:
408 + current_assets (list): A list of dictionaries representing current assets.
409 +
410 + Returns:
411 + list: A list of dictionaries with duplicate assets removed.
412 + """
413 + current_assets = list({d["asset_name"]: d for d in current_assets}.values())
414 + current_assets_str = [json.dumps(d, sort_keys=True) for d in current_assets]
415 + current_assets_str = list(set(current_assets_str))
416 + current_assets = [json.loads(s) for s in current_assets_str]
417 + return current_assets
418 +
419 +
420 +async def analyze_wazuh_alerts(
421 + monitoring_alerts: MonitoringAlerts,
422 + customer_meta: CustomersMeta,
423 + session: AsyncSession,
424 +) -> WazuhAnalysisResponse:
425 + """
426 + Analyze the given Wazuh alerts and create an alert if necessary. Otherwise update the existing alert with the asset.
427 +
428 + 1. For each alert, extract the metadata from the Wazuh-Indexer.
429 + 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.
430 + The alert will contain the asset and IoC if available.
431 + 3. Get the current list of assets from the alert to avoid overwriting them.
432 +
433 + Args:
434 + monitoring_alerts (MonitoringAlerts): The monitoring alert details.
435 + session (AsyncSession): The database session.
436 +
437 + Returns:
438 + WazuhAnalysisResponse: The analysis response.
439 + """
440 + logger.info(f"Analyzing Wazuh alerts with customer_meta: {customer_meta}")
441 + alert_detail_service = await AlertDetailsService.create()
442 + for alert in monitoring_alerts:
443 + alert_details = await fetch_alert_details(alert)
444 + await check_event_exclusion(alert_details, alert_detail_service, session)
445 + iris_alert_id = await check_if_open_alert_exists_in_iris(alert_details)
446 + if iris_alert_id == []:
447 + logger.info(f"Alert {alert_details._id} does not exist in IRIS. Creating alert.")
448 + await create_and_update_alert_in_iris(alert_details, session)
449 + else:
450 + logger.info(f"Alert {iris_alert_id} exists in IRIS. Updating alert with the asset.")
451 + # Fetch the current list of assets from the alert to avoid overwriting them
452 + client, alert_client = await initialize_client_and_alert("DFIR-IRIS")
453 + current_assets = await get_current_assets(client, alert_client, iris_alert_id)
454 + alert_details = await create_alert_details(alert_details)
455 + agent_details = await get_agent(alert_details.agent_id, session)
456 + asset_payload = await build_asset_payload(agent_data=agent_details, alert_details=alert_details, session=session)
457 + current_assets.append(dict(IrisAsset(**asset_payload.to_dict())))
458 + current_assets = await remove_duplicate_assets(current_assets)
459 + await update_alert_with_assets(client, alert_client, iris_alert_id, current_assets)
460 +
461 + return WazuhAnalysisResponse(
462 + success=True,
463 + message="Wazuh alerts analyzed successfully",
464 + )
backend/app/routers/monitoring_alert.py new
+11
@@ -0,0 +1,11 @@
1 +from fastapi import APIRouter
2 +
3 +from app.integrations.monitoring_alert.routes.monitoring_alert import (
4 + monitoring_alerts_router,
5 +)
6 +
7 +# Instantiate the APIRouter
8 +router = APIRouter()
9 +
10 +# Include the Monitoring Alert related routes
11 +router.include_router(monitoring_alerts_router, prefix="/monitoring_alert", tags=["monitoring_alert"])
backend/copilot.py
+2
@@ -38,6 +38,7 @@ from app.routers import influxdb
38 from app.routers import integrations
39 from app.routers import logs
40 from app.routers import mimecast
41 +from app.routers import monitoring_alert
42 from app.routers import office365
43 from app.routers import scheduler
44 from app.routers import shuffle
@@ -105,6 +106,7 @@ app.include_router(integrations.router)
106 app.include_router(office365.router)
107 app.include_router(mimecast.router)
108 app.include_router(scheduler.router)
109 +app.include_router(monitoring_alert.router)
110
111
112 @app.on_event("startup")