@cryptotaxi247 / CoPilot / commits / 2d054ad5

Alert processing (#133)

* Add alert removal in analyze_wazuh_alerts function * Add MonitoringWazuhAlertsRequestModel to run_analysis endpoint * Add process_id field to WazuhSourceModel and update check_event_exclusion function * Refactor monitoring alert routes and add provision route for wazuh monitoring alerts * Refactor Event model to use a dictionary for fields * Add Graylog event notifications endpoint * Add event definition check and provision Wazuh monitoring alert * Remove logging statement in provision_webhook function * Add URL whitelist endpoint to Graylog management routes * Add Graylog URL whitelist functionality * Add typing and error handling to provision_webhook function * Add provision_alert_definition function and provision alert definition for Wazuh syslog level alert * Refactor provision_wazuh_monitoring_alert_route function * precommit fixes * Refactor provision_wazuh_monitoring_alert_route() to accept request parameters * Replace underscores with spaces in monitoring alert names * Add alert to document when creating or updating alert in IRIS * Refactor monitoring alert provisioning * added MonitoringAlerts apis * added MonitoringAlerts tab * added alert provisioning * Replace spaces with underscores and convert to uppercase in validate_alert_name method * precommit fixes --------- Co-authored-by: Davide Di Modica <webmaster.ddm@gmail.com>

taylor_socfortress committed Feb 6, 2024 at 11:30 UTC 2d054ad505863877d9a22dcac9734d240fd3cc4e
28 files changed +1368 -49
backend/app/connectors/graylog/routes/management.py
+20
@@ -17,8 +17,10 @@ from app.connectors.graylog.schema.management import StopInputBody
17 from app.connectors.graylog.schema.management import StopInputResponse
18 from app.connectors.graylog.schema.management import StopStreamBody
19 from app.connectors.graylog.schema.management import StopStreamResponse
20 +from app.connectors.graylog.schema.management import UrlWhitelistEntryResponse
21 from app.connectors.graylog.services.collector import get_index_names
22 from app.connectors.graylog.services.collector import get_input_ids
23 +from app.connectors.graylog.services.collector import get_url_whitelist_entries
24 from app.connectors.graylog.services.management import delete_index
25 from app.connectors.graylog.services.management import start_input
26 from app.connectors.graylog.services.management import start_stream
@@ -131,6 +133,24 @@ async def verify_stream_id(stop_stream_body: StopStreamBody) -> StopStreamBody:
133 return stop_stream_body
134
135
136 +@graylog_management_router.get(
137 + "/url_whitelist",
138 + response_model=UrlWhitelistEntryResponse,
139 + description="Get the URL whitelist entries.",
140 + dependencies=[Security(AuthHandler().get_current_user, scopes=["admin"])],
141 +)
142 +async def get_url_whitelist() -> UrlWhitelistEntryResponse:
143 + """
144 + Get the URL whitelist entries.
145 +
146 + Returns:
147 + - UrlWhitelistEntryResponse: The response containing the URL whitelist entries.
148 + """
149 + logger.info("Getting URL whitelist entries")
150 +
151 + return await get_url_whitelist_entries()
152 +
153 +
154 @graylog_management_router.delete(
155 "/index",
156 response_model=DeletedIndexResponse,
backend/app/connectors/graylog/routes/monitoring.py
+19
@@ -3,8 +3,10 @@ from fastapi import Security
3 from loguru import logger
4
5 from app.auth.utils import AuthHandler
6 +from app.connectors.graylog.schema.monitoring import GraylogEventNotificationsResponse
7 from app.connectors.graylog.schema.monitoring import GraylogMessagesResponse
8 from app.connectors.graylog.schema.monitoring import GraylogMetricsResponse
9 +from app.connectors.graylog.services.monitoring import get_event_notifications
10 from app.connectors.graylog.services.monitoring import get_messages
11 from app.connectors.graylog.services.monitoring import get_metrics
12
@@ -50,3 +52,20 @@ async def get_all_metrics() -> GraylogMetricsResponse:
52 """
53 logger.info("Fetching all graylog metrics")
54 return await get_metrics()
55 +
56 +
57 +@graylog_monitoring_router.get(
58 + "/event_notifications",
59 + response_model=GraylogEventNotificationsResponse,
60 + description="Get all event notifications",
61 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
62 +)
63 +async def get_all_event_notifications() -> GraylogEventNotificationsResponse:
64 + """
65 + Fetches all graylog event notifications.
66 +
67 + Returns:
68 + GraylogEventNotificationsResponse: The response containing all the event notifications.
69 + """
70 + logger.info("Fetching all graylog event notifications")
71 + return await get_event_notifications()
backend/app/connectors/graylog/schema/events.py
+1 -5
@@ -89,15 +89,11 @@ class Context(BaseModel):
89 streams: Dict[str, Stream]
90
91
92 -class Fields(BaseModel):
93 - test: str
94 -
95 -
92 class Event(BaseModel):
93 alert: bool
94 event_definition_id: str
95 event_definition_type: str
100 - fields: Fields
96 + fields: Dict[str, str]
97 group_by_fields: Dict[str, str]
98 id: str
99 key: Optional[str]
backend/app/connectors/graylog/schema/management.py
+20
@@ -1,3 +1,5 @@
1 +from typing import List
2 +
3 from pydantic import BaseModel
4
5
@@ -44,3 +46,21 @@ class StartStreamBody(BaseModel):
46 class StartStreamResponse(BaseModel):
47 success: bool
48 message: str
49 +
50 +
51 +class UrlWhitelistEntry(BaseModel):
52 + id: str
53 + type: str
54 + title: str
55 + value: str
56 +
57 +
58 +class UrlWhitelistEntries(BaseModel):
59 + entries: List[UrlWhitelistEntry]
60 + disabled: bool
61 +
62 +
63 +class UrlWhitelistEntryResponse(BaseModel):
64 + success: bool
65 + message: str
66 + url_whitelist_entries: UrlWhitelistEntries
backend/app/connectors/graylog/schema/monitoring.py
+40
@@ -51,3 +51,43 @@ class GraylogMetricsResponse(BaseModel):
51 uncommitted_journal_entries: int
52 message: str
53 success: bool
54 +
55 +
56 +#### ! Graylog Event Notifications ! ####
57 +class GraylogEventNotificationsBasicAuth(BaseModel):
58 + is_set: bool
59 +
60 +
61 +class GraylogEventNotificationsApiSecret(BaseModel):
62 + is_set: bool
63 +
64 +
65 +class GraylogEventNotificationsConfig(BaseModel):
66 + type: str
67 + basic_auth: GraylogEventNotificationsBasicAuth
68 + api_key: str
69 + api_secret: GraylogEventNotificationsApiSecret
70 + url: str
71 +
72 +
73 +class GraylogEventNotificationsNotification(BaseModel):
74 + id: str
75 + title: str
76 + description: str
77 + config: GraylogEventNotificationsConfig
78 +
79 +
80 +class GraylogEventNotifications(BaseModel):
81 + total: int
82 + page: int
83 + per_page: int
84 + count: int
85 + notifications: List[GraylogEventNotificationsNotification]
86 + query: str
87 + grand_total: int
88 +
89 +
90 +class GraylogEventNotificationsResponse(BaseModel):
91 + event_notifications: GraylogEventNotifications
92 + message: str
93 + success: bool
backend/app/connectors/graylog/services/collector.py
+25
@@ -11,6 +11,7 @@ from app.connectors.graylog.schema.collector import GraylogIndicesResponse
11 from app.connectors.graylog.schema.collector import GraylogInputsResponse
12 from app.connectors.graylog.schema.collector import RunningInput
13 from app.connectors.graylog.schema.collector import RunningInputsResponse
14 +from app.connectors.graylog.schema.management import UrlWhitelistEntryResponse
15 from app.connectors.graylog.utils.universal import send_get_request
16
17
@@ -168,3 +169,27 @@ async def get_input_ids() -> List[str]:
169 return [input.id for input in inputs_collected]
170 else:
171 return []
172 +
173 +
174 +async def get_url_whitelist_entries() -> UrlWhitelistEntryResponse:
175 + """
176 + Retrieves the URL whitelist entries from Graylog.
177 +
178 + Returns:
179 + UrlWhitelistEntryResponse: The response object containing the URL whitelist entries.
180 + """
181 + logger.info("Getting URL whitelist entries from Graylog")
182 + response = await send_get_request(endpoint="/api/system/urlwhitelist")
183 + logger.info(f"URL whitelist entries response: {response}")
184 + if response["success"]:
185 + try:
186 + url_whitelist_entries = response["data"]
187 + except KeyError:
188 + raise HTTPException(status_code=500, detail="Failed to collect URL whitelist entries")
189 + return UrlWhitelistEntryResponse(
190 + url_whitelist_entries=url_whitelist_entries,
191 + success=True,
192 + message="URL whitelist entries collected successfully",
193 + )
194 + else:
195 + return UrlWhitelistEntryResponse(url_whitelist_entries=[], success=False, message="Failed to collect URL whitelist entries")
backend/app/connectors/graylog/services/monitoring.py
+25
@@ -1,6 +1,7 @@
1 from fastapi import HTTPException
2 from loguru import logger
3
4 +from app.connectors.graylog.schema.monitoring import GraylogEventNotificationsResponse
5 from app.connectors.graylog.schema.monitoring import GraylogMessages
6 from app.connectors.graylog.schema.monitoring import GraylogMessagesResponse
7 from app.connectors.graylog.schema.monitoring import GraylogMetricsResponse
@@ -142,3 +143,27 @@ async def get_metrics() -> GraylogMetricsResponse:
143 success=False,
144 message="Failed to collect metrics",
145 )
146 +
147 +
148 +async def get_event_notifications() -> GraylogEventNotificationsResponse:
149 + """
150 + Retrieves event notifications from Graylog.
151 +
152 + Returns:
153 + GraylogEventNotificationsResponse: The response object containing the collected event notifications.
154 + """
155 + logger.info("Getting event notifications from Graylog")
156 + event_notifications_collected = await send_get_request(endpoint="/api/events/notifications")
157 + try:
158 + if event_notifications_collected["success"]:
159 + return GraylogEventNotificationsResponse(
160 + event_notifications=event_notifications_collected["data"],
161 + success=True,
162 + message="Event notifications collected successfully",
163 + )
164 + except KeyError as e:
165 + raise HTTPException(status_code=500, detail=f"Failed to collect event notifications key: {e}")
166 + except Exception as e:
167 + raise HTTPException(status_code=500, detail=f"Failed to collect event notifications: {e}")
168 +
169 + return GraylogEventNotificationsResponse(event_notifications=[], success=False, message="Failed to collect event notifications")
backend/app/connectors/graylog/utils/universal.py
+5 -2
@@ -224,11 +224,14 @@ async def send_put_request(endpoint: str, data: Optional[Dict[str, Any]] = None,
224 json=data,
225 verify=False,
226 )
227 - if response.status_code != 200:
227 + logger.info(f"Response from PUT request: {response.status_code} {response.text}")
228 + if response.status_code not in [200, 204]:
229 raise HTTPException(
230 status_code=404,
230 - detail=f"Failed to send PUT request to {endpoint} with error: {response.json()['message']}",
231 + detail=f"Failed to send PUT request to {endpoint} with error: {response.json().get('message', '')}",
232 )
233 + if response.status_code == 204:
234 + return {"data": None, "success": True, "message": "Successfully sent PUT request, no content returned"}
235 return {"data": response.json(), "success": True, "message": "Successfully retrieved data"}
236 except HTTPException as e:
237 raise e
backend/app/integrations/monitoring_alert/routes/monitoring_alert.py
+11 -6
@@ -19,6 +19,9 @@ from app.integrations.monitoring_alert.schema.monitoring_alert import (
19 from app.integrations.monitoring_alert.schema.monitoring_alert import (
20 MonitoringAlertsRequestModel,
21 )
22 +from app.integrations.monitoring_alert.schema.monitoring_alert import (
23 + MonitoringWazuhAlertsRequestModel,
24 +)
25 from app.integrations.monitoring_alert.schema.monitoring_alert import (
26 WazuhAnalysisResponse,
27 )
@@ -118,8 +121,8 @@ async def create_monitoring_alert(
121
122
123 @monitoring_alerts_router.post("/run_analysis/wazuh", response_model=WazuhAnalysisResponse)
121 -async def run_analysis(
122 - customer_code: str,
124 +async def run_wazuh_analysis(
125 + request: MonitoringWazuhAlertsRequestModel,
126 session: AsyncSession = Depends(get_db),
127 ) -> WazuhAnalysisResponse:
128 """
@@ -131,18 +134,20 @@ async def run_analysis(
134 2. Call the anlayze_wazuh_alerts function to analyze the alerts.
135
136 Args:
134 - customer_code (str): The customer code.
137 + request (MonitoringWazuhAlertsRequestModel): The customer code.
138 session (AsyncSession, optional): The database session. Defaults to Depends(get_db).
139
140 Returns:
141 WazuhAnalysisResponse: The response containing the analysis results.
142 """
140 - logger.info(f"Running analysis for customer_code: {customer_code}")
143 + logger.info(f"Running analysis for customer_code: {request.customer_code}")
144
142 - customer_meta = await get_customer_meta(customer_code, session)
145 + customer_meta = await get_customer_meta(request.customer_code, session)
146
147 monitoring_alerts = await session.execute(
145 - select(MonitoringAlerts).where(MonitoringAlerts.customer_code == customer_code and MonitoringAlerts.alert_source == "WAZUH"),
148 + select(MonitoringAlerts).where(
149 + MonitoringAlerts.customer_code == request.customer_code and MonitoringAlerts.alert_source == "WAZUH",
150 + ),
151 )
152 monitoring_alerts = monitoring_alerts.scalars().all()
153
backend/app/integrations/monitoring_alert/routes/provision.py new
+104
@@ -0,0 +1,104 @@
1 +from fastapi import APIRouter
2 +from fastapi import HTTPException
3 +from loguru import logger
4 +
5 +from app.connectors.graylog.routes.events import get_all_event_definitions
6 +from app.connectors.graylog.schema.events import GraylogEventDefinitionsResponse
7 +from app.integrations.monitoring_alert.schema.provision import AvailableMonitoringAlerts
8 +from app.integrations.monitoring_alert.schema.provision import (
9 + AvailableMonitoringAlertsResponse,
10 +)
11 +from app.integrations.monitoring_alert.schema.provision import (
12 + ProvisionMonitoringAlertRequest,
13 +)
14 +from app.integrations.monitoring_alert.schema.provision import (
15 + ProvisionWazuhMonitoringAlertResponse,
16 +)
17 +from app.integrations.monitoring_alert.services.provision import (
18 + provision_wazuh_monitoring_alert,
19 +)
20 +from app.schedulers.models.scheduler import CreateSchedulerRequest
21 +from app.schedulers.scheduler import add_scheduler_jobs
22 +
23 +monitoring_alerts_provision_router = APIRouter()
24 +
25 +
26 +# Define your provision functions
27 +async def invoke_provision_wazuh_monitoring_alert(request: ProvisionMonitoringAlertRequest):
28 + # Provision the Wazuh monitoring alert
29 + await provision_wazuh_monitoring_alert(request)
30 + await add_scheduler_jobs(
31 + CreateSchedulerRequest(
32 + function_name="invoke_wazuh_monitoring_alert",
33 + time_interval=5,
34 + job_id="invoke_wazuh_monitoring_alert",
35 + ),
36 + )
37 +
38 +
39 +# ! Comment out for now ! #
40 +# async def provision_other_alert(request):
41 +# # Provision the other alert
42 +# pass
43 +
44 +# Create a dictionary that maps alert names to provision functions
45 +PROVISION_FUNCTIONS = {
46 + "WAZUH_SYSLOG_LEVEL_ALERT": invoke_provision_wazuh_monitoring_alert,
47 + # "OTHER_ALERT": provision_other_alert,
48 + # Add more alert names and functions as needed
49 +}
50 +
51 +
52 +async def check_if_event_definition_exists(event_definition: str) -> bool:
53 + """
54 + Check if the event definition exists.
55 +
56 + Args:
57 + event_definition (str): The event definition to check.
58 +
59 + Returns:
60 + bool: True if the event definition exists, False otherwise.
61 + """
62 + event_definitions_response = await get_all_event_definitions()
63 + if not event_definitions_response.success:
64 + raise HTTPException(status_code=500, detail="Failed to collect event definitions")
65 + event_definitions_response = GraylogEventDefinitionsResponse(**event_definitions_response.dict())
66 + logger.info(f"Event definitions collected: {event_definitions_response.event_definitions}")
67 + if event_definition in [event_definition.title for event_definition in event_definitions_response.event_definitions]:
68 + raise HTTPException(status_code=400, detail=f"Event definition {event_definition} already exists")
69 + return False
70 +
71 +
72 +@monitoring_alerts_provision_router.get(
73 + "/available",
74 + response_model=AvailableMonitoringAlertsResponse,
75 + description="Get the available monitoring alerts.",
76 +)
77 +async def get_available_monitoring_alerts_route() -> AvailableMonitoringAlertsResponse:
78 + """
79 + Get the available monitoring alerts.
80 + """
81 + alerts = [{"name": alert.name.replace("_", " "), "value": alert.value} for alert in AvailableMonitoringAlerts]
82 + return AvailableMonitoringAlertsResponse(success=True, message="Alerts retrieved successfully", available_monitoring_alerts=alerts)
83 +
84 +
85 +@monitoring_alerts_provision_router.post(
86 + "/provision",
87 + response_model=ProvisionWazuhMonitoringAlertResponse,
88 + description="Provisions monitoring alerts.",
89 +)
90 +async def provision_monitoring_alert_route(
91 + request: ProvisionMonitoringAlertRequest,
92 +) -> ProvisionWazuhMonitoringAlertResponse:
93 + await check_if_event_definition_exists(request.alert_name.replace("_", " "))
94 +
95 + # Look up the provision function based on request.alert_name
96 + provision_function = PROVISION_FUNCTIONS.get(request.alert_name)
97 +
98 + if provision_function is None:
99 + raise HTTPException(status_code=400, detail=f"No provision function found for alert name {request.alert_name}")
100 +
101 + # Invoke the provision function
102 + await provision_function(request)
103 +
104 + return ProvisionWazuhMonitoringAlertResponse(success=True, message="Wazuh monitoring alerts provisioned.")
backend/app/integrations/monitoring_alert/schema/monitoring_alert.py
+8
@@ -23,6 +23,10 @@ class MonitoringAlertsRequestModel(BaseModel):
23 orm_mode = True
24
25
26 +class MonitoringWazuhAlertsRequestModel(BaseModel):
27 + customer_code: str
28 +
29 +
30 class GraylogEventFields(BaseModel):
31 ALERT_ID: str = Field(..., description="Unique identifier for the alert", example="65f6a260-c1f3-11ee-93bc-86000046278a")
32 ALERT_SOURCE: str = Field(..., description="Source of the alert", example="WAZUH")
@@ -88,6 +92,10 @@ class WazuhSourceModel(BaseModel):
92 rule_level: int = Field(..., description="The level of the rule.")
93 rule_description: str = Field(..., description="The description of the rule.")
94 timestamp: str = Field(..., description="The timestamp of the alert.")
95 + process_id: Optional[str] = Field(
96 + "n/a",
97 + description="The process id of the alert.",
98 + )
99 timestamp_utc: Optional[str] = Field(
100 None,
101 description="The UTC timestamp of the alert.",
backend/app/integrations/monitoring_alert/schema/provision.py new
+167
@@ -0,0 +1,167 @@
1 +from enum import Enum
2 +from typing import Dict
3 +from typing import List
4 +from typing import Optional
5 +
6 +from fastapi import HTTPException
7 +from pydantic import BaseModel
8 +from pydantic import Field
9 +from pydantic import validator
10 +
11 +
12 +class AvailableMonitoringAlerts(str, Enum):
13 + """
14 + The available monitoring alerts.
15 + """
16 +
17 + WAZUH_SYSLOG_LEVEL_ALERT = (
18 + "This alert monitors the SYSLOG_LEVEL field in the Wazuh logs. When the level is ALERT, "
19 + "it triggers an alert that is created within DFIR-IRIS. Ensure that you have a pipeline "
20 + "rule that sets the SYSLOG_LEVEL field to ALERT when the Wazuh rule level is greater than 11."
21 + )
22 +
23 +
24 +class AvailableMonitoringAlertsResponse(BaseModel):
25 + """
26 + The available monitoring alerts response.
27 + """
28 +
29 + success: bool
30 + message: str
31 + available_monitoring_alerts: List[Dict[str, str]]
32 +
33 +
34 +class ProvisionMonitoringAlertRequest(BaseModel):
35 + search_within_last: int = Field(
36 + ...,
37 + description="The time in seconds to search within for the alert.",
38 + )
39 + execute_every: int = Field(
40 + ...,
41 + description="The time in seconds to execute the alert search.",
42 + )
43 + alert_name: str = Field(
44 + "WAZUH_SYSLOG_LEVEL_ALERT",
45 + description="The name of the alert to provision.",
46 + )
47 +
48 + @validator("alert_name")
49 + def validate_alert_name(cls, v):
50 + v = v.replace(" ", "_").upper()
51 + if v not in AvailableMonitoringAlerts.__members__:
52 + raise HTTPException(
53 + status_code=400,
54 + detail=f"Invalid alert name: {v}. Must be one of: {', '.join(AvailableMonitoringAlerts.__members__)}",
55 + )
56 + return v
57 +
58 + @validator("search_within_last", "execute_every")
59 + def validate_non_zero(cls, v):
60 + if v == 0:
61 + raise HTTPException(status_code=400, detail=f"Invalid value: {v}. Must be greater than 0.")
62 + return v
63 +
64 +
65 +class ProvisionWazuhMonitoringAlertResponse(BaseModel):
66 + success: bool
67 + message: str
68 +
69 +
70 +########## ! GRAYLOG WHITELIST URL CREATION ! ##########
71 +class GraylogUrlWhitelistEntryConfig(BaseModel):
72 + id: str = Field(
73 + ...,
74 + description="The ID of the URL whitelist entry.",
75 + )
76 + title: str = Field(
77 + ...,
78 + description="The title of the URL whitelist entry.",
79 + )
80 + type: str = Field(
81 + "literal",
82 + description="The type of the URL whitelist entry.",
83 + )
84 + value: str = Field(
85 + ...,
86 + description="The value of the URL whitelist entry.",
87 + )
88 +
89 +
90 +class GraylogUrlWhitelistEntries(BaseModel):
91 + entries: List[GraylogUrlWhitelistEntryConfig]
92 + disabled: bool
93 +
94 +
95 +########## ! GRAYLOG WEBHOOK CREATION ! ##########
96 +class GraylogAlertWebhookConfig(BaseModel):
97 + url: str = Field(
98 + ...,
99 + description="The URL to use for the webhook.",
100 + )
101 + api_key: Optional[str] = Field(
102 + None,
103 + description="The API key to use for the webhook.",
104 + )
105 + api_secret: Optional[str] = Field(
106 + None,
107 + description="The API secret to use for the webhook.",
108 + )
109 + basic_auth: Optional[str] = Field(
110 + None,
111 + description="The basic auth to use for the webhook.",
112 + )
113 + type: str = Field(
114 + ...,
115 + description="The type of the webhook.",
116 + )
117 +
118 +
119 +class GraylogAlertWebhookNotificationModel(BaseModel):
120 + title: str
121 + description: str
122 + config: GraylogAlertWebhookConfig
123 +
124 +
125 +########## ! GRAYLOG EVENT CREATION ! ##########
126 +class GraylogAlertProvisionProvider(BaseModel):
127 + template: str
128 + type: str = Field(..., alias="type")
129 + require_values: bool
130 +
131 +
132 +class GraylogAlertProvisionFieldSpecItem(BaseModel):
133 + data_type: str
134 + providers: List[GraylogAlertProvisionProvider]
135 +
136 +
137 +class GraylogAlertProvisionConfig(BaseModel):
138 + query: str
139 + query_parameters: List
140 + streams: List
141 + search_within_ms: int
142 + execute_every_ms: int
143 + group_by: List
144 + series: List
145 + conditions: Dict
146 + type: str = Field(..., alias="type")
147 +
148 +
149 +class GraylogAlertProvisionNotificationSettings(BaseModel):
150 + grace_period_ms: int
151 + backlog_size: Optional[int] = None
152 +
153 +
154 +class GraylogAlertProvisionNotification(BaseModel):
155 + notification_id: str
156 +
157 +
158 +class GraylogAlertProvisionModel(BaseModel):
159 + title: str
160 + description: str
161 + priority: int
162 + config: GraylogAlertProvisionConfig
163 + field_spec: Dict[str, GraylogAlertProvisionFieldSpecItem]
164 + key_spec: List
165 + notification_settings: GraylogAlertProvisionNotificationSettings
166 + notifications: List[GraylogAlertProvisionNotification]
167 + alert: bool
backend/app/integrations/monitoring_alert/services/provision.py new
+286
@@ -0,0 +1,286 @@
1 +import os
2 +from typing import Optional
3 +
4 +from dotenv import load_dotenv
5 +from fastapi import HTTPException
6 +from loguru import logger
7 +
8 +from app.connectors.graylog.routes.monitoring import get_all_event_notifications
9 +from app.connectors.graylog.schema.management import UrlWhitelistEntryResponse
10 +from app.connectors.graylog.schema.monitoring import GraylogEventNotificationsResponse
11 +from app.connectors.graylog.services.collector import get_url_whitelist_entries
12 +from app.connectors.graylog.utils.universal import send_post_request
13 +from app.connectors.graylog.utils.universal import send_put_request
14 +from app.integrations.monitoring_alert.schema.provision import (
15 + GraylogAlertProvisionConfig,
16 +)
17 +from app.integrations.monitoring_alert.schema.provision import (
18 + GraylogAlertProvisionFieldSpecItem,
19 +)
20 +from app.integrations.monitoring_alert.schema.provision import (
21 + GraylogAlertProvisionModel,
22 +)
23 +from app.integrations.monitoring_alert.schema.provision import (
24 + GraylogAlertProvisionNotification,
25 +)
26 +from app.integrations.monitoring_alert.schema.provision import (
27 + GraylogAlertProvisionNotificationSettings,
28 +)
29 +from app.integrations.monitoring_alert.schema.provision import (
30 + GraylogAlertProvisionProvider,
31 +)
32 +from app.integrations.monitoring_alert.schema.provision import (
33 + GraylogAlertWebhookNotificationModel,
34 +)
35 +from app.integrations.monitoring_alert.schema.provision import (
36 + GraylogUrlWhitelistEntries,
37 +)
38 +from app.integrations.monitoring_alert.schema.provision import (
39 + GraylogUrlWhitelistEntryConfig,
40 +)
41 +from app.integrations.monitoring_alert.schema.provision import (
42 + ProvisionMonitoringAlertRequest,
43 +)
44 +from app.integrations.monitoring_alert.schema.provision import (
45 + ProvisionWazuhMonitoringAlertResponse,
46 +)
47 +
48 +load_dotenv()
49 +import uuid
50 +
51 +
52 +async def convert_seconds_to_milliseconds(seconds: int) -> int:
53 + """
54 + Convert seconds to milliseconds.
55 +
56 + Args:
57 + seconds (int): The seconds to convert.
58 +
59 + Returns:
60 + int: The milliseconds.
61 + """
62 + return seconds * 1000
63 +
64 +
65 +async def generate_random_id() -> str:
66 + """
67 + Generate a random id.
68 +
69 + Returns:
70 + str: The random id.
71 + """
72 + return str(uuid.uuid4())
73 +
74 +
75 +async def check_if_url_whitelist_entry_exists(url: str) -> bool:
76 + """
77 + Check if the url whitelist entry exists.
78 +
79 + Args:
80 + url (str): The url to check.
81 +
82 + Returns:
83 + bool: True if the url whitelist entry exists, False otherwise.
84 + """
85 + url_whitelist_entries_response = await get_url_whitelist_entries()
86 + if not url_whitelist_entries_response.success:
87 + raise HTTPException(status_code=500, detail="Failed to collect url whitelist entries")
88 + url_whitelist_entries_response = UrlWhitelistEntryResponse(**url_whitelist_entries_response.dict())
89 + logger.info(f"Url whitelist entries collected: {url_whitelist_entries_response.url_whitelist_entries}")
90 + if url in [url_whitelist_entry.value for url_whitelist_entry in url_whitelist_entries_response.url_whitelist_entries.entries]:
91 + logger.info(f"Url whitelist entry {url} already exists")
92 + return True
93 + return False
94 +
95 +
96 +async def build_url_whitelisted_entries(whitelist_url_model: GraylogUrlWhitelistEntryConfig) -> GraylogUrlWhitelistEntries:
97 + """
98 + Builds the URL Whitelisted Entries model.
99 +
100 + Returns:
101 + GraylogUrlWhitelistEntries: The URL Whitelisted Entries model.
102 + """
103 + url_whitelist_entries_response = await get_url_whitelist_entries()
104 + if not url_whitelist_entries_response.success:
105 + raise HTTPException(status_code=500, detail="Failed to collect url whitelist entries")
106 + url_whitelist_entries_response = UrlWhitelistEntryResponse(**url_whitelist_entries_response.dict())
107 + logger.info(f"Url whitelist entries collected: {url_whitelist_entries_response}")
108 + url_whitelist_entries = url_whitelist_entries_response.url_whitelist_entries.entries
109 + url_whitelist_entries.append(whitelist_url_model)
110 + return GraylogUrlWhitelistEntries(
111 + entries=url_whitelist_entries,
112 + disabled=False,
113 + )
114 +
115 +
116 +async def provision_webhook_url_whitelist(whitelist_url_model: GraylogUrlWhitelistEntries) -> bool:
117 + """
118 + Provisions a webhook URL for Graylog.
119 +
120 + Args:
121 + whitelist_url_model (GraylogUrlWhitelistEntryConfig): The webhook URL model.
122 +
123 + Returns:
124 + bool: True if the webhook URL was provisioned successfully, False otherwise.
125 + """
126 + logger.info(f"Provisioning URL Whitelist: {whitelist_url_model.dict()}")
127 + response = await send_put_request(endpoint="/api/system/urlwhitelist", data=whitelist_url_model.dict())
128 + logger.info(f"URL Whitelist provisioned: {response}")
129 + if response["success"]:
130 + return True
131 + raise HTTPException(status_code=500, detail="Failed to provision URL Whitelist")
132 +
133 +
134 +async def check_if_event_notification_exists(event_notification: str) -> bool:
135 + """
136 + Check if the event notification exists.
137 +
138 + Args:
139 + event_notification (str): The event notification to check.
140 +
141 + Returns:
142 + bool: True if the event notification exists, False otherwise.
143 + """
144 + event_notifications_response = await get_all_event_notifications()
145 + if not event_notifications_response.success:
146 + raise HTTPException(status_code=500, detail="Failed to collect event notifications")
147 + event_notifications_response = GraylogEventNotificationsResponse(**event_notifications_response.dict())
148 + logger.info(f"Event notifications collected: {event_notifications_response.event_notifications}")
149 + if event_notification in [
150 + event_notification.title for event_notification in event_notifications_response.event_notifications.notifications
151 + ]:
152 + return True
153 + return False
154 +
155 +
156 +async def provision_webhook(webhook_model: GraylogAlertWebhookNotificationModel) -> Optional[str]:
157 + """
158 + Provisions a webhook for Graylog alerts.
159 +
160 + Args:
161 + webhook_model (GraylogAlertWebhookNotificationModel): The webhook model.
162 +
163 + Returns:
164 + bool: True if the webhook was provisioned successfully, False otherwise.
165 + """
166 + response = await send_post_request(endpoint="/api/events/notifications", data=webhook_model.dict())
167 + if response["success"]:
168 + logger.info(f"response: {response}")
169 + return response["data"]["id"]
170 + raise HTTPException(status_code=500, detail="Failed to provision webhook")
171 +
172 +
173 +async def provision_alert_definition(alert_definition_model: GraylogAlertProvisionModel) -> bool:
174 + """
175 + Provisions an alert definition for Graylog.
176 +
177 + Args:
178 + alert_definition_model (GraylogAlertProvisionModel): The alert definition model.
179 +
180 + Returns:
181 + bool: True if the alert definition was provisioned successfully, False otherwise.
182 + """
183 + response = await send_post_request(endpoint="/api/events/definitions", data=alert_definition_model.dict())
184 + if response["success"]:
185 + return True
186 + raise HTTPException(status_code=500, detail="Failed to provision alert definition")
187 +
188 +
189 +async def provision_wazuh_monitoring_alert(request: ProvisionMonitoringAlertRequest) -> ProvisionWazuhMonitoringAlertResponse:
190 + """
191 + Provisions Wazuh monitoring alerts.
192 +
193 + Returns:
194 + ProvisionWazuhMonitoringAlertResponse: The response indicating the success of provisioning the monitoring alerts.
195 + """
196 + #
197 + logger.info(f"Invoking provision_wazuh_monitoring_alert with request: {request.dict()}")
198 + notification_exists = await check_if_event_notification_exists("SEND TO COPILOT")
199 + if not notification_exists:
200 + url_whitelisted = await check_if_url_whitelist_entry_exists(f"http://{os.getenv('SERVER_IP')}:5000/monitoring_alerts/create")
201 + if not url_whitelisted:
202 + logger.info("Provisioning URL Whitelist")
203 + whitelisted_urls = await build_url_whitelisted_entries(
204 + whitelist_url_model=GraylogUrlWhitelistEntryConfig(
205 + id=await generate_random_id(),
206 + value=f"http://{os.getenv('SERVER_IP')}:5000/monitoring_alerts/create",
207 + title="SEND TO COPILOT",
208 + type="literal",
209 + ),
210 + )
211 + await provision_webhook_url_whitelist(whitelisted_urls)
212 +
213 + logger.info("Provisioning SEND TO COPILOT Webhook")
214 + notification_id = await provision_webhook(
215 + GraylogAlertWebhookNotificationModel(
216 + title="SEND TO COPILOT",
217 + description="Send alert to Copilot",
218 + config={"url": f"http://{os.getenv('SERVER_IP')}:5000/monitoring_alerts/create", "type": "http-notification-v1"},
219 + ),
220 + )
221 + logger.info(f"SEND TO COPILOT Webhook provisioned with id: {notification_id}")
222 + await provision_alert_definition(
223 + GraylogAlertProvisionModel(
224 + title="WAZUH SYSLOG LEVEL ALERT",
225 + description="Alert on Wazuh syslog level equal to ALERT",
226 + priority=2,
227 + config=GraylogAlertProvisionConfig(
228 + type="aggregation-v1",
229 + query="syslog_level:ALERT AND syslog_type:wazuh",
230 + query_parameters=[],
231 + streams=[],
232 + group_by=[],
233 + series=[],
234 + conditions={
235 + "expression": None,
236 + },
237 + search_within_ms=await convert_seconds_to_milliseconds(request.search_within_last),
238 + execute_every_ms=await convert_seconds_to_milliseconds(request.execute_every),
239 + ),
240 + field_spec={
241 + "ALERT_ID": GraylogAlertProvisionFieldSpecItem(
242 + data_type="string",
243 + providers=[
244 + GraylogAlertProvisionProvider(
245 + type="template-v1",
246 + template="${source._id}",
247 + require_values=True,
248 + ),
249 + ],
250 + ),
251 + "CUSTOMER_CODE": GraylogAlertProvisionFieldSpecItem(
252 + data_type="string",
253 + providers=[
254 + GraylogAlertProvisionProvider(
255 + type="template-v1",
256 + template="${source.agent_labels_customer}",
257 + require_values=True,
258 + ),
259 + ],
260 + ),
261 + "ALERT_SOURCE": GraylogAlertProvisionFieldSpecItem(
262 + data_type="string",
263 + providers=[
264 + GraylogAlertProvisionProvider(
265 + type="template-v1",
266 + template="WAZUH",
267 + require_values=True,
268 + ),
269 + ],
270 + ),
271 + },
272 + key_spec=[],
273 + notification_settings=GraylogAlertProvisionNotificationSettings(
274 + grace_period_ms=0,
275 + backlog_size=None,
276 + ),
277 + notifications=[
278 + GraylogAlertProvisionNotification(
279 + notification_id=notification_id,
280 + ),
281 + ],
282 + alert=True,
283 + ),
284 + )
285 +
286 + return ProvisionWazuhMonitoringAlertResponse(success=True, message="Wazuh monitoring alerts provisioned successfully")
backend/app/integrations/monitoring_alert/services/wazuh.py
+28 -2
@@ -19,6 +19,12 @@ 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.alert_escalation.schema.general_alert import (
23 + CreateAlertRequest as AddAlertRequest,
24 +)
25 +from app.integrations.alert_escalation.services.general_alert import (
26 + add_alert_to_document,
27 +)
28 from app.integrations.monitoring_alert.models.monitoring_alert import MonitoringAlerts
29 from app.integrations.monitoring_alert.schema.monitoring_alert import (
30 FilterAlertsRequest,
@@ -33,6 +39,7 @@ from app.integrations.monitoring_alert.schema.monitoring_alert import (
39 from app.integrations.monitoring_alert.schema.monitoring_alert import (
40 WazuhIrisAlertPayload,
41 )
42 +from app.integrations.monitoring_alert.utils.db_operations import remove_alert_id
43 from app.integrations.utils.alerts import get_asset_type_id
44 from app.integrations.utils.alerts import validate_ioc_type
45 from app.utils import get_customer_alert_settings
@@ -151,9 +158,11 @@ async def fetch_alert_details(alert: MonitoringAlerts) -> WazuhAlertModel:
158
159
160 async def check_event_exclusion(alert_details: WazuhAlertModel, alert_detail_service: AlertDetailsService, session: AsyncSession):
161 + logger.info("Checking if alert is excluded due to multi exclusion.")
162 + logger.info(f"Alert details: {alert_details}")
163 event_exclude_result = await alert_detail_service.collect_alert_timeline_process_id(
164 agent_name=alert_details._source["agent_name"],
156 - process_id=getattr(alert_details._source, "process_id", "n/a"),
165 + process_id=alert_details._source.get("process_id", "n/a"),
166 index=alert_details._index,
167 session=session,
168 )
@@ -445,7 +454,16 @@ async def analyze_wazuh_alerts(
454 iris_alert_id = await check_if_open_alert_exists_in_iris(alert_details)
455 if iris_alert_id == []:
456 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)
457 + iris_alert_id = await create_and_update_alert_in_iris(alert_details, session)
458 + await remove_alert_id(alert.alert_id, session)
459 + es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
460 + await add_alert_to_document(
461 + es_client=es_client,
462 + alert=AddAlertRequest(alert_id=alert_details._id, index_name=alert_details._index),
463 + soc_alert_id=iris_alert_id,
464 + session=session,
465 + )
466 +
467 else:
468 logger.info(f"Alert {iris_alert_id} exists in IRIS. Updating alert with the asset.")
469 # Fetch the current list of assets from the alert to avoid overwriting them
@@ -457,6 +475,14 @@ async def analyze_wazuh_alerts(
475 current_assets.append(dict(IrisAsset(**asset_payload.to_dict())))
476 current_assets = await remove_duplicate_assets(current_assets)
477 await update_alert_with_assets(client, alert_client, iris_alert_id, current_assets)
478 + await remove_alert_id(alert.alert_id, session)
479 + es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
480 + await add_alert_to_document(
481 + es_client=es_client,
482 + alert=AddAlertRequest(alert_id=alert_details.id, index_name=alert_details.index),
483 + soc_alert_id=iris_alert_id,
484 + session=session,
485 + )
486
487 return WazuhAnalysisResponse(
488 success=True,
backend/app/integrations/monitoring_alert/utils/db_operations.py new
+27
@@ -0,0 +1,27 @@
1 +from loguru import logger
2 +from sqlalchemy.ext.asyncio import AsyncSession
3 +from sqlalchemy.future import select
4 +
5 +from app.integrations.monitoring_alert.models.monitoring_alert import MonitoringAlerts
6 +
7 +
8 +async def remove_alert_id(alert_id: str, session: AsyncSession) -> None:
9 + """
10 + Remove the alert with the given alert_id from the database.
11 +
12 + Args:
13 + alert_id (str): The alert_id.
14 + session (AsyncSession): The database session.
15 + """
16 + logger.info(f"Removing alert with alert_id: {alert_id}")
17 +
18 + alert = await session.execute(select(MonitoringAlerts).where(MonitoringAlerts.alert_id == alert_id))
19 + alert = alert.scalars().first()
20 +
21 + if not alert:
22 + logger.error(f"Alert with alert_id: {alert_id} not found")
23 +
24 + await session.delete(alert)
25 + await session.commit()
26 + logger.info(f"Alert with alert_id: {alert_id} removed")
27 + return None
backend/app/routers/monitoring_alert.py
+4
@@ -3,9 +3,13 @@ from fastapi import APIRouter
3 from app.integrations.monitoring_alert.routes.monitoring_alert import (
4 monitoring_alerts_router,
5 )
6 +from app.integrations.monitoring_alert.routes.provision import (
7 + monitoring_alerts_provision_router,
8 +)
9
10 # Instantiate the APIRouter
11 router = APIRouter()
12
13 # Include the Monitoring Alert related routes
14 router.include_router(monitoring_alerts_router, prefix="/monitoring_alert", tags=["monitoring_alert"])
15 +router.include_router(monitoring_alerts_provision_router, prefix="/monitoring_alert", tags=["provision_monitoring_alert"])
backend/app/schedulers/scheduler.py
+2
@@ -9,6 +9,7 @@ from app.schedulers.models.scheduler import JobMetadata
9 from app.schedulers.services.agent_sync import agent_sync
10 from app.schedulers.services.invoke_mimecast import invoke_mimecast_integration
11 from app.schedulers.services.invoke_mimecast import invoke_mimecast_integration_ttp
12 +from app.schedulers.services.monitoring_alert import invoke_wazuh_monitoring_alert
13
14
15 def init_scheduler():
@@ -78,6 +79,7 @@ def get_function_by_name(function_name: str):
79 "agent_sync": agent_sync,
80 "invoke_mimecast_integration": invoke_mimecast_integration,
81 "invoke_mimecast_integration_ttp": invoke_mimecast_integration_ttp,
82 + "invoke_wazuh_monitoring_alert": invoke_wazuh_monitoring_alert,
83 # Add other function mappings here
84 }
85 return function_map.get(function_name, lambda: ValueError(f"Function {function_name} not found"))
backend/app/schedulers/services/monitoring_alert.py new
+54
@@ -0,0 +1,54 @@
1 +from datetime import datetime
2 +
3 +from dotenv import load_dotenv
4 +from loguru import logger
5 +from sqlalchemy import select
6 +
7 +from app.db.db_session import get_db_session
8 +from app.db.db_session import get_sync_db_session
9 +from app.db.universal_models import CustomersMeta
10 +from app.integrations.monitoring_alert.routes.monitoring_alert import run_wazuh_analysis
11 +from app.integrations.monitoring_alert.schema.monitoring_alert import (
12 + MonitoringWazuhAlertsRequestModel,
13 +)
14 +from app.integrations.monitoring_alert.schema.monitoring_alert import (
15 + WazuhAnalysisResponse,
16 +)
17 +from app.schedulers.models.scheduler import JobMetadata
18 +
19 +load_dotenv()
20 +
21 +
22 +async def invoke_wazuh_monitoring_alert() -> WazuhAnalysisResponse:
23 + """
24 + Invokes the Wazuh monitoring alerts scheduled job.
25 +
26 + Returns:
27 + WazuhAnalysisResponse: The response indicating the success of invoking the monitoring alerts.
28 + """
29 + logger.info("Invoking Wazuh monitoring alerts scheduled job.")
30 + customer_codes = []
31 + async with get_db_session() as session:
32 + stmt = select(CustomersMeta)
33 + result = await session.execute(stmt)
34 + customer_codes = [row.customer_code for row in result.scalars()]
35 + logger.info(f"customer_codes: {customer_codes}")
36 + for customer_code in customer_codes:
37 + await run_wazuh_analysis(
38 + MonitoringWazuhAlertsRequestModel(customer_code=customer_code),
39 + session,
40 + )
41 + # Close the session
42 + await session.close()
43 + with get_sync_db_session() as session:
44 + # Synchronous ORM operations
45 + job_metadata = session.query(JobMetadata).filter_by(job_id="invoke_wazuh_monitoring_alerts").one_or_none()
46 + if job_metadata:
47 + job_metadata.last_success = datetime.utcnow()
48 + session.add(job_metadata)
49 + session.commit()
50 + else:
51 + # Handle the case where job_metadata does not exist
52 + logger.error("JobMetadata for 'invoke_wazuh_monitoring_alerts' not found.")
53 +
54 + return WazuhAnalysisResponse(success=True, message="Wazuh monitoring alerts invoked.")
package-lock.json
+24 -25
@@ -23,7 +23,7 @@
23 "dayjs": "^1.11.10",
24 "detect-touch-device": "^1.1.6",
25 "echarts": "^5.4.3",
26 - "jose": "^5.2.0",
26 + "jose": "^5.2.1",
27 "js-md5": "^0.8.3",
28 "lodash": "^4.17.21",
29 "markdown-it-highlightjs": "^4.0.1",
@@ -57,7 +57,7 @@
57 "@types/markdown-it": "^13.0.7",
58 "@types/markdown-it-highlightjs": "^3.3.4",
59 "@types/node": "^20.11.16",
60 - "@types/validator": "^13.11.8",
60 + "@types/validator": "^13.11.9",
61 "@vitejs/plugin-vue": "^5.0.3",
62 "@vitejs/plugin-vue-jsx": "^3.1.0",
63 "@vue/eslint-config-prettier": "^9.0.0",
@@ -74,13 +74,13 @@
74 "json5": "^2.2.3",
75 "npm-run-all": "^4.1.5",
76 "picocolors": "^1.0.0",
77 - "postcss": "^8.4.33",
78 - "prettier": "^3.2.4",
77 + "postcss": "^8.4.34",
78 + "prettier": "^3.2.5",
79 "sass": "^1.70.0",
80 "start-server-and-test": "^2.0.3",
81 "tailwind-config-viewer": "^1.7.3",
82 "tailwindcss": "^3.4.1",
83 - "taze": "^0.13.2",
83 + "taze": "^0.13.3",
84 "ts-node": "^10.9.2",
85 "typescript": "~5.3.3",
86 "unplugin-vue-components": "^0.26.0",
@@ -665,7 +665,6 @@
665 },
666 "node_modules/@clack/prompts/node_modules/is-unicode-supported": {
667 "version": "1.3.0",
668 - "extraneous": true,
668 "inBundle": true,
669 "license": "MIT",
670 "engines": {
@@ -2448,9 +2447,9 @@
2447 "dev": true
2448 },
2449 "node_modules/@types/validator": {
2451 - "version": "13.11.8",
2452 - "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.8.tgz",
2453 - "integrity": "sha512-c/hzNDBh7eRF+KbCf+OoZxKbnkpaK/cKp9iLQWqB7muXtM+MtL9SUUH8vCFcLn6dH1Qm05jiexK0ofWY7TfOhQ==",
2450 + "version": "13.11.9",
2451 + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.9.tgz",
2452 + "integrity": "sha512-FCTsikRozryfayPuiI46QzH3fnrOoctTjvOYZkho9BTFLCOZ2rgZJHMOVgCOfttjPJcgOx52EpkY0CMfy87MIw==",
2453 "dev": true
2454 },
2455 "node_modules/@types/web-bluetooth": {
@@ -7269,9 +7268,9 @@
7268 }
7269 },
7270 "node_modules/jose": {
7272 - "version": "5.2.0",
7273 - "resolved": "https://registry.npmjs.org/jose/-/jose-5.2.0.tgz",
7274 - "integrity": "sha512-oW3PCnvyrcm1HMvGTzqjxxfnEs9EoFOFWi2HsEGhlFVOXxTE3K9GKWVMFoFw06yPUqwpvEWic1BmtUZBI/tIjw==",
7271 + "version": "5.2.1",
7272 + "resolved": "https://registry.npmjs.org/jose/-/jose-5.2.1.tgz",
7273 + "integrity": "sha512-qiaQhtQRw6YrOaOj0v59h3R6hUY9NvxBmmnMfKemkqYmBB0tEc97NbLP7ix44VP5p9/0YHG8Vyhzuo5YBNwviA==",
7274 "funding": {
7275 "url": "https://github.com/sponsors/panva"
7276 }
@@ -9473,9 +9472,9 @@
9472 }
9473 },
9474 "node_modules/postcss": {
9476 - "version": "8.4.33",
9477 - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz",
9478 - "integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==",
9475 + "version": "8.4.34",
9476 + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.34.tgz",
9477 + "integrity": "sha512-4eLTO36woPSocqZ1zIrFD2K1v6wH7pY1uBh0JIM2KKfrVtGvPFiAku6aNOP0W1Wr9qwnaCsF0Z+CrVnryB2A8Q==",
9478 "funding": [
9479 {
9480 "type": "opencollective",
@@ -9625,9 +9624,9 @@
9624 }
9625 },
9626 "node_modules/prettier": {
9628 - "version": "3.2.4",
9629 - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.4.tgz",
9630 - "integrity": "sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ==",
9627 + "version": "3.2.5",
9628 + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz",
9629 + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==",
9630 "bin": {
9631 "prettier": "bin/prettier.cjs"
9632 },
@@ -10578,9 +10577,9 @@
10577 "integrity": "sha512-MW8Qs6vbzo0pHmDpFSYPna+lwpZ6Zk1ancbajw/7E8TKtHdV+1DfZZD+kKJEhG/cAoB/i+LiT+5msZOqj0DwRA=="
10578 },
10579 "node_modules/semver": {
10581 - "version": "7.5.4",
10582 - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
10583 - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
10580 + "version": "7.6.0",
10581 + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
10582 + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
10583 "dependencies": {
10584 "lru-cache": "^6.0.0"
10585 },
@@ -11530,9 +11529,9 @@
11529 "dev": true
11530 },
11531 "node_modules/taze": {
11533 - "version": "0.13.2",
11534 - "resolved": "https://registry.npmjs.org/taze/-/taze-0.13.2.tgz",
11535 - "integrity": "sha512-G2S3f4kFjcYu0Xdxv/vgx5NmPuJAhhI16weQs0P8J/pjCo4Ieip37k1/TPg32pgggLq1GQtwlDuAui/LQ+XH9g==",
11532 + "version": "0.13.3",
11533 + "resolved": "https://registry.npmjs.org/taze/-/taze-0.13.3.tgz",
11534 + "integrity": "sha512-+NgEyx6F+Ydpwo/9JsWdH40wHGrlZIaYjGtcErg21mdynP/dwTCgOa/gkvOZov1U1brJQCdShG0wifesRucr7A==",
11535 "dev": true,
11536 "dependencies": {
11537 "@antfu/ni": "^0.21.12",
@@ -11544,7 +11543,7 @@
11543 "pacote": "^17.0.6",
11544 "picocolors": "^1.0.0",
11545 "prompts": "^2.4.2",
11547 - "semver": "^7.5.4",
11546 + "semver": "^7.6.0",
11547 "unconfig": "^0.3.11",
11548 "yargs": "^17.7.2"
11549 },
package.json
+5 -5
@@ -47,7 +47,7 @@
47 "dayjs": "^1.11.10",
48 "detect-touch-device": "^1.1.6",
49 "echarts": "^5.4.3",
50 - "jose": "^5.2.0",
50 + "jose": "^5.2.1",
51 "js-md5": "^0.8.3",
52 "lodash": "^4.17.21",
53 "markdown-it-highlightjs": "^4.0.1",
@@ -81,7 +81,7 @@
81 "@types/markdown-it": "^13.0.7",
82 "@types/markdown-it-highlightjs": "^3.3.4",
83 "@types/node": "^20.11.16",
84 - "@types/validator": "^13.11.8",
84 + "@types/validator": "^13.11.9",
85 "@vitejs/plugin-vue": "^5.0.3",
86 "@vitejs/plugin-vue-jsx": "^3.1.0",
87 "@vue/eslint-config-prettier": "^9.0.0",
@@ -98,13 +98,13 @@
98 "json5": "^2.2.3",
99 "npm-run-all": "^4.1.5",
100 "picocolors": "^1.0.0",
101 - "postcss": "^8.4.33",
102 - "prettier": "^3.2.4",
101 + "postcss": "^8.4.34",
102 + "prettier": "^3.2.5",
103 "sass": "^1.70.0",
104 "start-server-and-test": "^2.0.3",
105 "tailwind-config-viewer": "^1.7.3",
106 "tailwindcss": "^3.4.1",
107 - "taze": "^0.13.2",
107 + "taze": "^0.13.3",
108 "ts-node": "^10.9.2",
109 "typescript": "~5.3.3",
110 "unplugin-vue-components": "^0.26.0",
src/api/index.ts
+3 -1
@@ -13,6 +13,7 @@ import customers from "./customers"
13 import logs from "./logs"
14 import flow from "./flow"
15 import integrations from "./integrations"
16 +import monitoringAlerts from "./monitoringAlerts"
17
18 export default {
19 agents,
@@ -29,5 +30,6 @@ export default {
30 customers,
31 logs,
32 flow,
32 - integrations
33 + integrations,
34 + monitoringAlerts
35 }
src/api/monitoringAlerts.ts new
+23
@@ -0,0 +1,23 @@
1 +import { type FlaskBaseResponse } from "@/types/flask.d"
2 +import { HttpClient } from "./httpClient"
3 +import type { AvailableMonitoringAlert } from "@/types/monitoringAlerts"
4 +
5 +export interface ProvisionsMonitoringAlertParams {
6 + searchWithinLast: number
7 + executeEvery: number
8 +}
9 +
10 +export default {
11 + getAvailableMonitoringAlerts() {
12 + return HttpClient.get<FlaskBaseResponse & { available_monitoring_alerts: AvailableMonitoringAlert[] }>(
13 + `/monitoring_alert/available`
14 + )
15 + },
16 + provisionsMonitoringAlert(alertName: string, params: ProvisionsMonitoringAlertParams) {
17 + return HttpClient.post<FlaskBaseResponse>(`/monitoring_alert/provision`, {
18 + search_within_last: params.searchWithinLast,
19 + execute_every: params.executeEvery,
20 + alert_name: alertName
21 + })
22 + }
23 +}
src/assets/scss/helpers.scss
+3
@@ -110,3 +110,6 @@
110 .text-error-color {
111 color: var(--error-color);
112 }
113 +.text-success-color {
114 + color: var(--success-color);
115 +}
src/components/graylog/Events/List.vue
+5
@@ -55,6 +55,10 @@ import Icon from "@/components/common/Icon.vue"
55 import type { EventDefinition } from "@/types/graylog/event-definition.d"
56 import type { SelectMixedOption } from "naive-ui/es/select/src/interface"
57
58 +const emit = defineEmits<{
59 + (e: "loaded", value: EventDefinition[]): void
60 +}>()
61 +
62 const props = defineProps<{ highlight: string | null | undefined }>()
63 const { highlight } = toRefs(props)
64
@@ -102,6 +106,7 @@ function getData() {
106 if (res.data.success) {
107 events.value = res.data.event_definitions || []
108 total.value = events.value.length || 0
109 + emit("loaded", events.value)
110
111 nextTick(() => {
112 setTimeout(() => {
src/components/graylog/MonitoringAlerts/Item.vue new
+246
@@ -0,0 +1,246 @@
1 +<template>
2 + <div class="item flex flex-col gap-2 px-5 py-3">
3 + <div class="header-box flex justify-between gap-4">
4 + <div class="name">{{ alert.name }}</div>
5 + <div class="badge flex mb-2">
6 + <Badge :type="isEnabled ? 'active' : 'muted'">
7 + <template #iconRight>
8 + <Icon :name="isEnabled ? EnabledIcon : DisabledIcon" :size="13"></Icon>
9 + </template>
10 + <template #label>
11 + <span class="whitespace-nowrap">
12 + {{ isEnabled ? "Enabled" : "Not Enabled" }}
13 + </span>
14 + </template>
15 + </Badge>
16 + </div>
17 + </div>
18 + <div class="main-box flex justify-between gap-4">
19 + <div class="content">{{ alert.value }}</div>
20 + <div class="actions-box">
21 + <n-button
22 + v-if="!isEnabled"
23 + :loading="loadingProvision"
24 + type="success"
25 + secondary
26 + @click="openFormDialog()"
27 + >
28 + <template #icon><Icon :name="EnableIcon"></Icon></template>
29 + Enable
30 + </n-button>
31 + </div>
32 + </div>
33 + <div class="footer-box flex justify-between items-center gap-4">
34 + <div class="actions-box">
35 + <n-button
36 + v-if="!isEnabled"
37 + :loading="loadingProvision"
38 + type="success"
39 + secondary
40 + size="small"
41 + @click="openFormDialog()"
42 + >
43 + <template #icon><Icon :name="EnableIcon"></Icon></template>
44 + Enable
45 + </n-button>
46 + </div>
47 + </div>
48 +
49 + <n-modal
50 + :title="alert.name"
51 + v-model:show="showFormDialog"
52 + preset="card"
53 + segmented
54 + :mask-closable="false"
55 + :close-on-esc="false"
56 + :style="{ maxWidth: 'min(500px, 90vw)', minHeight: 'min(350px, 90vh)', overflow: 'hidden' }"
57 + >
58 + <n-spin :show="loadingProvision">
59 + <n-form ref="formRef" :model="formModel" :rules="formRules">
60 + <n-form-item path="searchWithinLast" label="Search Within Last (time in seconds)">
61 + <n-input-number
62 + :min="1"
63 + v-model:value="formModel.searchWithinLast"
64 + @keydown.enter.prevent
65 + placeholder="Input time in seconds"
66 + clearable
67 + class="w-full"
68 + />
69 + </n-form-item>
70 + <n-form-item path="executeEvery" label="Execute Every (time in seconds)">
71 + <n-input-number
72 + :min="1"
73 + v-model:value="formModel.executeEvery"
74 + @keydown.enter.prevent
75 + placeholder="Input time in seconds"
76 + clearable
77 + class="w-full"
78 + />
79 + </n-form-item>
80 + </n-form>
81 + </n-spin>
82 + <template #footer>
83 + <div class="flex justify-end gap-3">
84 + <n-button @click="closeFormDialog()">Close</n-button>
85 + <n-button :loading="loadingProvision" type="success" @click="validateForm">Enable</n-button>
86 + </div>
87 + </template>
88 + </n-modal>
89 + </div>
90 +</template>
91 +
92 +<script setup lang="ts">
93 +import type { AvailableMonitoringAlert } from "@/types/monitoringAlerts"
94 +import { ref } from "vue"
95 +import Icon from "@/components/common/Icon.vue"
96 +import Badge from "@/components/common/Badge.vue"
97 +import {
98 + NButton,
99 + NSpin,
100 + useMessage,
101 + NModal,
102 + type FormRules,
103 + NForm,
104 + NFormItem,
105 + NInputNumber,
106 + type FormValidationError
107 +} from "naive-ui"
108 +import Api from "@/api"
109 +import type { ProvisionsMonitoringAlertParams } from "@/api/monitoringAlerts"
110 +
111 +const emit = defineEmits<{
112 + (e: "provisioned"): void
113 +}>()
114 +
115 +const { alert, isEnabled } = defineProps<{ alert: AvailableMonitoringAlert; isEnabled: boolean }>()
116 +
117 +const DisabledIcon = "carbon:subtract"
118 +const EnabledIcon = "ph:check-bold"
119 +const EnableIcon = "carbon:play"
120 +
121 +const loadingProvision = ref(false)
122 +const showFormDialog = ref(false)
123 +const message = useMessage()
124 +
125 +const formRef = ref()
126 +const formModel = ref<{ searchWithinLast: null | number; executeEvery: null | number }>(getClearFormModel())
127 +const formRules: FormRules = {
128 + searchWithinLast: [
129 + {
130 + required: true
131 + }
132 + ],
133 + executeEvery: [
134 + {
135 + required: true
136 + }
137 + ]
138 +}
139 +
140 +function getClearFormModel() {
141 + return {
142 + searchWithinLast: null,
143 + executeEvery: null
144 + }
145 +}
146 +
147 +function resetFormModel() {
148 + formModel.value = getClearFormModel()
149 +}
150 +
151 +function openFormDialog() {
152 + resetFormModel()
153 + showFormDialog.value = true
154 +}
155 +
156 +function closeFormDialog() {
157 + resetFormModel()
158 + showFormDialog.value = false
159 +}
160 +
161 +function validateForm(e: MouseEvent) {
162 + e.preventDefault()
163 + formRef.value?.validate((errors: Array<FormValidationError> | undefined) => {
164 + if (!errors) {
165 + provisionsMonitoringAlert()
166 + } else {
167 + for (const err of errors) {
168 + message.error(err[0].message || "Invalid fields")
169 + }
170 + }
171 + })
172 +}
173 +
174 +function provisionsMonitoringAlert() {
175 + if (formModel.value.searchWithinLast && formModel.value.executeEvery) {
176 + loadingProvision.value = true
177 +
178 + const params: ProvisionsMonitoringAlertParams = {
179 + searchWithinLast: formModel.value.searchWithinLast,
180 + executeEvery: formModel.value.executeEvery
181 + }
182 +
183 + Api.monitoringAlerts
184 + .provisionsMonitoringAlert(alert.name, params)
185 + .then(res => {
186 + if (res.data.success) {
187 + message.success("Alert Provisioned Successfully")
188 + emit("provisioned")
189 + } else {
190 + message.warning(res.data?.message || "An error occurred. Please try again later.")
191 + }
192 + })
193 + .catch(err => {
194 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
195 + })
196 + .finally(() => {
197 + loadingProvision.value = false
198 + })
199 + }
200 +}
201 +</script>
202 +
203 +<style lang="scss" scoped>
204 +.item {
205 + border-radius: var(--border-radius);
206 + background-color: var(--bg-color);
207 + transition: all 0.2s var(--bezier-ease);
208 + border: var(--border-small-050);
209 +
210 + .header-box {
211 + font-size: 13px;
212 +
213 + .name {
214 + font-family: var(--font-family-mono);
215 + word-break: break-word;
216 + color: var(--fg-secondary-color);
217 + }
218 + }
219 + .main-box {
220 + .content {
221 + word-break: break-word;
222 + }
223 + }
224 +
225 + .footer-box {
226 + display: none;
227 + font-size: 13px;
228 + margin-top: 10px;
229 + }
230 +
231 + &:hover {
232 + box-shadow: 0px 0px 0px 1px inset var(--primary-color);
233 + }
234 +
235 + @container (max-width: 450px) {
236 + .main-box {
237 + .actions-box {
238 + display: none;
239 + }
240 + }
241 + .footer-box {
242 + display: flex;
243 + }
244 + }
245 +}
246 +</style>
src/components/graylog/MonitoringAlerts/List.vue new
+177
@@ -0,0 +1,177 @@
1 +<template>
2 + <div class="monitoring-alert-list">
3 + <div class="header flex items-center justify-end gap-2" ref="header">
4 + <div class="info grow flex gap-2">
5 + <n-popover overlap placement="bottom-start">
6 + <template #trigger>
7 + <div class="bg-color border-radius">
8 + <n-button size="small" class="!cursor-help">
9 + <template #icon>
10 + <Icon :name="InfoIcon"></Icon>
11 + </template>
12 + </n-button>
13 + </div>
14 + </template>
15 + <div class="flex flex-col gap-2">
16 + <div class="box">
17 + Total :
18 + <code>{{ total }}</code>
19 + </div>
20 + <div class="box text-success-color">
21 + Enabled :
22 + <code>{{ enabledTotal }}</code>
23 + </div>
24 + </div>
25 + </n-popover>
26 + </div>
27 + <n-pagination
28 + v-model:page="currentPage"
29 + v-model:page-size="pageSize"
30 + :page-slot="pageSlot"
31 + :show-size-picker="showSizePicker"
32 + :page-sizes="pageSizes"
33 + :item-count="total"
34 + :simple="simpleMode"
35 + />
36 + </div>
37 + <n-spin :show="loading">
38 + <div class="list my-3">
39 + <template v-if="alerts.length">
40 + <MonitoringAlert
41 + v-for="alert of itemsPaginated"
42 + :key="alert.name"
43 + :alert="alert"
44 + :is-enabled="isEnabled(alert)"
45 + class="mb-2"
46 + @provisioned="getData()"
47 + />
48 + </template>
49 + <template v-else>
50 + <n-empty description="No items found" class="justify-center h-48" v-if="!loading" />
51 + </template>
52 + </div>
53 + </n-spin>
54 + <div class="footer flex justify-end">
55 + <n-pagination
56 + v-model:page="currentPage"
57 + :page-size="pageSize"
58 + :item-count="total"
59 + :page-slot="6"
60 + v-if="itemsPaginated.length > 3"
61 + />
62 + </div>
63 + </div>
64 +</template>
65 +
66 +<script setup lang="ts">
67 +import { ref, onBeforeMount, computed } from "vue"
68 +import { useMessage, NSpin, NPopover, NButton, NEmpty, NPagination } from "naive-ui"
69 +import Api from "@/api"
70 +import MonitoringAlert from "./Item.vue"
71 +import type { AvailableMonitoringAlert } from "@/types/monitoringAlerts"
72 +import Icon from "@/components/common/Icon.vue"
73 +import type { EventDefinition } from "@/types/graylog/event-definition"
74 +
75 +const { eventsList } = defineProps<{ eventsList: EventDefinition[] }>()
76 +
77 +const message = useMessage()
78 +const loadingEvents = ref(false)
79 +const loadingAlerts = ref(false)
80 +const alerts = ref<AvailableMonitoringAlert[]>([])
81 +const events = ref<EventDefinition[]>([])
82 +
83 +const loading = computed(() => loadingAlerts.value || loadingEvents.value)
84 +
85 +const pageSize = ref(25)
86 +const currentPage = ref(1)
87 +const simpleMode = ref(false)
88 +const showSizePicker = ref(true)
89 +const pageSizes = [10, 25, 50, 100]
90 +const header = ref()
91 +const pageSlot = ref(8)
92 +
93 +const InfoIcon = "carbon:information"
94 +
95 +const total = computed<number>(() => {
96 + return alerts.value.length || 0
97 +})
98 +
99 +const enabledTotal = computed<number>(() => {
100 + return enabledList.value.length || 0
101 +})
102 +
103 +const enabledList = computed<AvailableMonitoringAlert[]>(() => {
104 + return alerts.value.filter(alert => {
105 + const eventIndex = events.value.findIndex(event => event.title === alert.name)
106 + return eventIndex !== -1
107 + })
108 +})
109 +
110 +const itemsPaginated = computed(() => {
111 + const from = (currentPage.value - 1) * pageSize.value
112 + const to = currentPage.value * pageSize.value
113 +
114 + return alerts.value.slice(from, to)
115 +})
116 +
117 +function isEnabled(alert: AvailableMonitoringAlert): boolean {
118 + return enabledList.value.findIndex(o => o.name === alert.name) !== -1
119 +}
120 +
121 +function getData() {
122 + loadingAlerts.value = true
123 +
124 + Api.monitoringAlerts
125 + .getAvailableMonitoringAlerts()
126 + .then(res => {
127 + if (res.data.success) {
128 + alerts.value = res.data.available_monitoring_alerts || []
129 + } else {
130 + message.warning(res.data?.message || "An error occurred. Please try again later.")
131 + }
132 + })
133 + .catch(err => {
134 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
135 + })
136 + .finally(() => {
137 + loadingAlerts.value = false
138 + })
139 +}
140 +
141 +function getEvents() {
142 + loadingEvents.value = true
143 +
144 + Api.graylog
145 + .getEventDefinitions()
146 + .then(res => {
147 + if (res.data.success) {
148 + events.value = res.data.event_definitions || []
149 + } else {
150 + message.warning(res.data?.message || "An error occurred. Please try again later.")
151 + }
152 + })
153 + .catch(err => {
154 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
155 + })
156 + .finally(() => {
157 + loadingEvents.value = false
158 + })
159 +}
160 +
161 +onBeforeMount(() => {
162 + getData()
163 +
164 + if (eventsList.length && !events.value.length) {
165 + events.value = eventsList
166 + } else {
167 + getEvents()
168 + }
169 +})
170 +</script>
171 +
172 +<style lang="scss" scoped>
173 +.list {
174 + container-type: inline-size;
175 + min-height: 200px;
176 +}
177 +</style>
src/types/monitoringAlerts.d.ts new
+4
@@ -0,0 +1,4 @@
1 +export interface AvailableMonitoringAlert {
2 + name: string
3 + value: string
4 +}
src/views/graylog/Management.vue
+32 -3
@@ -8,11 +8,14 @@
8 <Alerts @click-event="gotoEventsPage($event)" />
9 </n-tab-pane>
10 <n-tab-pane name="events" tab="Events" display-directive="show:lazy">
11 - <Events :highlight="highlightEvent" />
11 + <Events :highlight="highlightEvent" @loaded="events = $event" />
12 </n-tab-pane>
13 <n-tab-pane name="streams" tab="Streams" display-directive="show:lazy">
14 <Streams />
15 </n-tab-pane>
16 + <n-tab-pane name="provisioning" tab="Alert Provisioning" display-directive="show:lazy">
17 + <MonitoringAlerts :events-list="events" />
18 + </n-tab-pane>
19 <template #suffix>
20 <n-button ghost type="primary" size="small" @click="showInputDrawer = true">Inputs</n-button>
21 </template>
@@ -33,22 +36,48 @@
36 </template>
37
38 <script setup lang="ts">
36 -import { ref } from "vue"
39 +import { onBeforeMount, ref } from "vue"
40 import { NTabs, NTabPane, NButton, NDrawer, NDrawerContent } from "naive-ui"
41 import Messages from "@/components/graylog/Messages/List.vue"
42 import Alerts from "@/components/graylog/Alerts/List.vue"
43 import Events from "@/components/graylog/Events/List.vue"
44 import Streams from "@/components/graylog/Streams/List.vue"
45 +import MonitoringAlerts from "@/components/graylog/MonitoringAlerts/List.vue"
46 import Inputs from "@/components/graylog/Inputs/List.vue"
47 +import type { EventDefinition } from "@/types/graylog/event-definition"
48 +import { useRoute, useRouter } from "vue-router"
49 +import { watch } from "vue"
50 +
51 +const tabsList = ["messages", "alerts", "events", "streams", "provisioning"]
52 +const drawersList = ["inputs"]
53
44 -const activeTab = ref<string | undefined>(undefined)
54 +const activeTab = ref<string | undefined>(tabsList[0])
55 const highlightEvent = ref<string | undefined>(undefined)
56 const showInputDrawer = ref(false)
57 +const events = ref<EventDefinition[]>([])
58 +const route = useRoute()
59 +const router = useRouter()
60
61 function gotoEventsPage(event_definition_id: string) {
62 activeTab.value = "events"
63 highlightEvent.value = event_definition_id
64 }
65 +
66 +watch(activeTab, val => {
67 + router.replace({ hash: "#" + val })
68 +})
69 +
70 +onBeforeMount(() => {
71 + const hash = route.hash ? route.hash.slice(1) : ""
72 + if (hash && tabsList.includes(hash)) {
73 + activeTab.value = hash
74 + }
75 + if (hash && drawersList.includes(hash)) {
76 + if (hash === "inputs") {
77 + showInputDrawer.value = true
78 + }
79 + }
80 +})
81 </script>
82
83 <style lang="scss" scoped></style>