@cryptotaxi247 / CoPilot / commits / 2ed4d435

query_builder for more modular wazuh-indexer searches (#58)

taylor_socfortress committed Jul 24, 2023 at 13:41 UTC 2ed4d4351674b17a5d1f9e520cd9760ddeaff9fc
4 files changed +334 -101
backend/app/routes/alerts.py
+15 -23
@@ -41,25 +41,9 @@ def get_alerts_by_agent(agent_name: str) -> jsonify:
41 containing all its associated data.
42 """
43 service = AlertsService()
44 - alerts = service.collect_alerts_by_agent_name(agent_name=agent_name)
45 - return jsonify(alerts)
46 -
47 -
48 -@bp.route("/alerts/top_10", methods=["GET"])
49 -def get_top_10_alerts() -> jsonify:
50 - """
51 - Retrieves top 10 alerts from the AlertsService per index.
52 -
53 - This endpoint retrieves top 10 alerts from the AlertsService. It does this by creating an instance of
54 - the AlertsService class and calling its `collect_alerts` method. The result is a list of top 10 alerts currently
55 - available.
56 -
57 - Returns:
58 - jsonify: A JSON response containing a list of alerts. Each item in the list is a dictionary representing an alert,
59 - containing all its associated data.
60 - """
61 - service = AlertsService()
62 - alerts = service.collect_alerts(size=10) # replace `collect_top_10_alerts` with `collect_alerts(size=10)`
44 + size = request.args.get("size", default=10, type=int)
45 + timerange = request.args.get("timerange", default="24h", type=str)
46 + alerts = service.collect_alerts_by_agent_name(agent_name=agent_name, size=size, timerange=timerange)
47 return jsonify(alerts)
48
49
@@ -76,8 +60,10 @@ def get_alerts_by_index(index_name: str) -> jsonify:
60 jsonify: A JSON response containing a list of alerts. Each item in the list is a dictionary representing an alert,
61 containing all its associated data.
62 """
63 + size = request.args.get("size", default=10, type=int)
64 + timerange = request.args.get("timerange", default="24h", type=str)
65 service = AlertsService()
80 - alerts = service.collect_alerts_by_index(index_name=index_name, size=1000)
66 + alerts = service.collect_alerts_by_index(index_name=index_name, size=size, timerange=timerange)
67 return jsonify(alerts)
68
69
@@ -94,8 +80,10 @@ def get_hosts() -> jsonify:
80 jsonify: A JSON response containing a list of hosts. Each item in the list is a dictionary representing a host,
81 containing all its associated data.
82 """
83 + size = request.args.get("size", default=10, type=int)
84 + timerange = request.args.get("timerange", default="24h", type=str)
85 service = AlertsService()
98 - hosts = service.collect_alerts_by_host()
86 + hosts = service.collect_alerts_by_host(size=size, timerange=timerange)
87 return jsonify(hosts)
88
89
@@ -112,8 +100,10 @@ def get_rules() -> jsonify:
100 jsonify: A JSON response containing a list of rules. Each item in the list is a dictionary representing a rule,
101 containing all its associated data.
102 """
103 + size = request.args.get("size", default=10, type=int)
104 + timerange = request.args.get("timerange", default="24h", type=str)
105 service = AlertsService()
116 - rules = service.collect_alerts_by_rule()
106 + rules = service.collect_alerts_by_rule(size=size, timerange=timerange)
107 return jsonify(rules)
108
109
@@ -130,8 +120,10 @@ def get_rules_by_host() -> jsonify:
120 jsonify: A JSON response containing a list of rules. Each item in the list is a dictionary representing a rule,
121 containing all its associated data.
122 """
123 + size = request.args.get("size", default=10, type=int)
124 + timerange = request.args.get("timerange", default="24h", type=str)
125 service = AlertsService()
134 - rules = service.collect_alerts_by_rule_per_host()
126 + rules = service.collect_alerts_by_rule_per_host(size=size, timerange=timerange)
127 return jsonify(rules)
128
129
backend/app/services/WazuhIndexer/alerts.py
+127 -37
@@ -1,13 +1,14 @@
1 -from datetime import datetime
2 -from datetime import timedelta
1 from typing import Any
2 from typing import Dict
3 +from typing import Iterable
4 +from typing import Tuple
5
6 from elasticsearch7 import Elasticsearch
7 from loguru import logger
8
9 from app.services.ask_socfortress.universal import AskSocfortressService
10 from app.services.DFIR_IRIS.alerts import IRISAlertsService
11 +from app.services.WazuhIndexer.universal import QueryBuilder
12 from app.services.WazuhIndexer.universal import UniversalService
13
14
@@ -88,8 +89,10 @@ class AlertsService:
89 return indices_validation
90
91 alerts_summary = []
92 + # matches = [("syslog_level", "ALERT"), ("agent_name", "WIN-39O01J5F7G5")]
93 + matches = [("syslog_level", "ALERT")]
94 for index_name in indices_validation["indices"]:
92 - alerts = self._collect_alerts(index_name, size=size, timerange=timerange)
95 + alerts = self._collect_alerts(index_name, size=size, timerange=timerange, matches=matches)
96 if alerts["success"] and len(alerts["alerts"]) > 0:
97 alerts_summary.append(
98 {
@@ -129,14 +132,22 @@ class AlertsService:
132 "alerts_summary": alerts_summary,
133 }
134
132 - def collect_alerts_by_index(self, index_name: str, size: int) -> Dict[str, Any]:
135 + def collect_alerts_by_index(self, index_name: str, size: int, timerange: str) -> Dict[str, Any]:
136 """
137 Collects alerts from the given index.
138 +
139 + Args:
140 + index_name (str): The name of the index to query.
141 + size (int): The maximum number of alerts to return.
142 + timerange (str): The time range to collect alerts from. This is a string like "24h", "1w", etc.
143 +
144 + Returns:
145 + Dict[str, Any]: A dictionary containing success status, a message, and potentially the alerts from the given index.
146 """
147 if not self.is_valid_index(index_name):
148 return self._error_response("Invalid index name")
149
139 - alerts = self._collect_alerts(index_name=index_name, size=size)
150 + alerts = self._collect_alerts(index_name=index_name, size=size, timerange=timerange)
151 if not alerts["success"]:
152 return alerts
153
@@ -147,12 +158,14 @@ class AlertsService:
158 "total_alerts": len(alerts["alerts"]),
159 }
160
150 - def collect_alerts_by_agent_name(self, agent_name: str) -> Dict[str, Any]:
161 + def collect_alerts_by_agent_name(self, agent_name: str, size: int, timerange: str) -> Dict[str, Any]:
162 """
163 Collects alerts associated with a given agent name.
164
165 Args:
166 agent_name (str): The agent name associated with the alerts.
167 + size (int): The maximum number of alerts to return.
168 + timerange (str): The time range to collect alerts from. This is a string like "24h", "1w", etc.
169
170 Returns:
171 Dict[str, Any]: A dictionary containing success status, a message, and potentially the alerts associated with the agent.
@@ -162,8 +175,9 @@ class AlertsService:
175 return indices_validation
176
177 alerts_by_agent_dict = {}
178 + matches = [("syslog_level", "ALERT"), ("agent_name", f"{agent_name}")]
179 for index_name in indices_validation["indices"]:
166 - alerts = self._collect_alerts(index_name=index_name, size=1000)
180 + alerts = self._collect_alerts(index_name=index_name, size=size, timerange=timerange, matches=matches)
181 if alerts["success"]:
182 for alert in alerts["alerts"]:
183 if alert["_source"]["agent_name"] == agent_name:
@@ -177,9 +191,16 @@ class AlertsService:
191 "alerts_by_agent": alerts_by_agent_list,
192 }
193
180 - def collect_alerts_by_host(self) -> Dict[str, int]:
194 + def collect_alerts_by_host(self, size: int, timerange: str) -> Dict[str, int]:
195 """
196 Collects the number of alerts per host.
197 +
198 + Args:
199 + size (int): The maximum number of alerts to return.
200 + timerange (str): The time range to collect alerts from. This is a string like "24h", "1w", etc.
201 +
202 + Returns:
203 + Dict[str, int]: A dictionary containing success status and the number of alerts per host or an error message.
204 """
205 indices_validation = self._collect_indices_and_validate()
206 if not indices_validation["success"]:
@@ -187,7 +208,7 @@ class AlertsService:
208
209 alerts_by_host_dict = {}
210 for index_name in indices_validation["indices"]:
190 - alerts = self._collect_alerts(index_name=index_name, size=1000)
211 + alerts = self._collect_alerts(index_name=index_name, size=size, timerange=timerange)
212 if alerts["success"]:
213 for alert in alerts["alerts"]:
214 host = alert["_source"]["agent_name"]
@@ -201,9 +222,16 @@ class AlertsService:
222 "alerts_by_host": alerts_by_host_list,
223 }
224
204 - def collect_alerts_by_rule(self) -> Dict[str, int]:
225 + def collect_alerts_by_rule(self, size: int, timerange: str) -> Dict[str, int]:
226 """
227 Collects the number of alerts per rule.
228 +
229 + Args:
230 + size (int): The maximum number of alerts to return.
231 + timerange (str): The time range to collect alerts from. This is a string like "24h", "1w", etc.
232 +
233 + Returns:
234 + Dict[str, int]: A dictionary containing success status and the number of alerts per rule or an error message.
235 """
236 indices_validation = self._collect_indices_and_validate()
237 if not indices_validation["success"]:
@@ -211,7 +239,7 @@ class AlertsService:
239
240 alerts_by_rule_dict = {}
241 for index_name in indices_validation["indices"]:
214 - alerts = self._collect_alerts(index_name=index_name, size=1000)
242 + alerts = self._collect_alerts(index_name=index_name, size=size, timerange=timerange)
243 if alerts["success"]:
244 for alert in alerts["alerts"]:
245 rule = alert["_source"]["rule_description"]
@@ -225,9 +253,16 @@ class AlertsService:
253 "alerts_by_rule": alerts_by_rule_list,
254 }
255
228 - def collect_alerts_by_rule_per_host(self) -> Dict[str, int]:
256 + def collect_alerts_by_rule_per_host(self, size: int, timerange: str) -> Dict[str, int]:
257 """
258 Collects the number of alerts per rule per host.
259 +
260 + Args:
261 + size (int): The maximum number of alerts to return.
262 + timerange (str): The time range to collect alerts from. This is a string like "24h", "1w", etc.
263 +
264 + Returns:
265 + Dict[str, int]: A dictionary containing success status and the number of alerts per rule per host or an error message.
266 """
267 indices_validation = self._collect_indices_and_validate()
268 if not indices_validation["success"]:
@@ -235,7 +270,7 @@ class AlertsService:
270
271 alerts_by_rule_per_host_dict = {}
272 for index_name in indices_validation["indices"]:
238 - alerts = self._collect_alerts(index_name=index_name, size=1000)
273 + alerts = self._collect_alerts(index_name=index_name, size=size, timerange=timerange)
274 if alerts["success"]:
275 for alert in alerts["alerts"]:
276 rule = alert["_source"]["rule_description"]
@@ -261,7 +296,47 @@ class AlertsService:
296 """
297 return {"message": message, "success": False}
298
264 - def _collect_alerts(self, index_name: str, size: int = None, timerange: str = "24h") -> Dict[str, object]:
299 + # def _collect_alerts(self, index_name: str, size: int = None, timerange: str = "24h") -> Dict[str, object]:
300 + # """
301 + # Elasticsearch query to get the most recent alerts where the `rule_level` is 12 or higher or the
302 + # `syslog_level` field is `ALERT` and return the results in descending order by the `timestamp_utc` field.
303 + # The number of alerts to return can be limited by the `size` parameter.
304 +
305 + # Args:
306 + # index_name (str): The name of the index to query.
307 + # size (int, optional): The maximum number of alerts to return. If None, all alerts are returned.
308 + # timerange (str, optional): The time range to collect alerts from. This is a string like "24h", "1w", etc.
309 +
310 + # Returns:
311 + # Dict[str, object]: A dictionary containing success status and alerts or an error message.
312 + # """
313 + # logger.info(f"Collecting alerts from {index_name}")
314 + # query = self._build_query(timerange=timerange) # Use the provided query
315 + # try:
316 + # alerts = self.es.search(index=index_name, body=query, size=size)
317 + # alerts_list = [alert for alert in alerts["hits"]["hits"]]
318 +
319 + # # Iterate over each alert and invoke invoke_socfortress function
320 + # for alert in alerts_list:
321 + # ask_socfortress = self.asksocfortress_service.invoke_asksocfortress(alert["_source"]["rule_description"])
322 + # alert["ask_socfortress"] = ask_socfortress # Add the result to the alert
323 +
324 + # return {
325 + # "message": "Successfully collected alerts",
326 + # "success": True,
327 + # "alerts": alerts_list, # Return the alerts list with the added results
328 + # }
329 + # except Exception as e:
330 + # logger.error(f"Failed to collect alerts: {e}")
331 + # return {"message": "Failed to collect alerts", "success": False}
332 +
333 + def _collect_alerts(
334 + self,
335 + index_name: str,
336 + size: int = None,
337 + timerange: str = "24h",
338 + matches: Iterable[Tuple[str, str]] = None,
339 + ) -> Dict[str, object]:
340 """
341 Elasticsearch query to get the most recent alerts where the `rule_level` is 12 or higher or the
342 `syslog_level` field is `ALERT` and return the results in descending order by the `timestamp_utc` field.
@@ -271,12 +346,27 @@ class AlertsService:
346 index_name (str): The name of the index to query.
347 size (int, optional): The maximum number of alerts to return. If None, all alerts are returned.
348 timerange (str, optional): The time range to collect alerts from. This is a string like "24h", "1w", etc.
349 + matches (Iterable[Tuple[str, str]], optional): A list of tuples representing the field and value to match.
350 + I.E: [("syslog_level", "ALERT"), ("agent_name", "WIN-39O01J5F7G5")]
351
352 Returns:
353 Dict[str, object]: A dictionary containing success status and alerts or an error message.
354 """
355 logger.info(f"Collecting alerts from {index_name}")
279 - query = self._build_query(timerange=timerange) # Use the provided query
356 +
357 + # Use QueryBuilder to construct the query
358 + query_builder = QueryBuilder()
359 + query_builder.add_time_range(timerange)
360 + if matches is not None:
361 + query_builder.add_matches(matches)
362 + else:
363 + query_builder.add_matches([("syslog_level", "ALERT")])
364 + query_builder.add_range("rule_level", "12")
365 + query_builder.add_sort("timestamp_utc")
366 +
367 + # Get the final query
368 + query = query_builder.build()
369 +
370 try:
371 alerts = self.es.search(index=index_name, body=query, size=size)
372 alerts_list = [alert for alert in alerts["hits"]["hits"]]
@@ -314,28 +404,28 @@ class AlertsService:
404 logger.error(f"Failed to collect alert: {e}")
405 return {"message": "Failed to collect alert", "success": False}
406
317 - @staticmethod
318 - def _get_time_range_start(timerange: str) -> str:
319 - """
320 - Determines the start time of the time range based on the current time and the provided timerange.
321 -
322 - Args:
323 - timerange (str): The time range to collect alerts from. This is a string like "24h", "1w", etc.
324 -
325 - Returns:
326 - str: A string representing the start time of the time range in ISO format.
327 - """
328 - if timerange.endswith("h"):
329 - delta = timedelta(hours=int(timerange[:-1]))
330 - elif timerange.endswith("d"):
331 - delta = timedelta(days=int(timerange[:-1]))
332 - elif timerange.endswith("w"):
333 - delta = timedelta(weeks=int(timerange[:-1]))
334 - else:
335 - raise ValueError("Invalid timerange format. Expected a string like '24h', '1d', '1w', etc.")
336 -
337 - start = datetime.utcnow() - delta
338 - return start.isoformat() + "Z" # Elasticsearch expects the time in ISO format with a Z at the end
407 + # @staticmethod
408 + # def _get_time_range_start(timerange: str) -> str:
409 + # """
410 + # Determines the start time of the time range based on the current time and the provided timerange.
411 +
412 + # Args:
413 + # timerange (str): The time range to collect alerts from. This is a string like "24h", "1w", etc.
414 +
415 + # Returns:
416 + # str: A string representing the start time of the time range in ISO format.
417 + # """
418 + # if timerange.endswith("h"):
419 + # delta = timedelta(hours=int(timerange[:-1]))
420 + # elif timerange.endswith("d"):
421 + # delta = timedelta(days=int(timerange[:-1]))
422 + # elif timerange.endswith("w"):
423 + # delta = timedelta(weeks=int(timerange[:-1]))
424 + # else:
425 + # raise ValueError("Invalid timerange format. Expected a string like '24h', '1d', '1w', etc.")
426 +
427 + # start = datetime.utcnow() - delta
428 + # return start.isoformat() + "Z" # Elasticsearch expects the time in ISO format with a Z at the end
429
430 @staticmethod
431 def _build_query(timerange: str) -> Dict[str, object]:
backend/app/services/WazuhIndexer/universal.py
+62
@@ -1,5 +1,9 @@
1 # from datetime import datetime
2 +from datetime import datetime
3 +from datetime import timedelta
4 from typing import Dict
5 +from typing import Iterable
6 +from typing import Tuple
7
8 # import requests
9 from elasticsearch7 import Elasticsearch
@@ -11,6 +15,8 @@ from loguru import logger
15 from app.models.connectors import Connector
16 from app.models.connectors import connector_factory
17
18 +# from app.services.WazuhIndexer.alerts import AlertsService
19 +
20 # from typing import List
21
22
@@ -150,3 +156,59 @@ class UniversalService:
156 except Exception as e:
157 logger.error(f"Failed to run query: {e}")
158 return {"message": "Failed to run query", "success": False}
159 +
160 +
161 +class QueryBuilder:
162 + @staticmethod
163 + def _get_time_range_start(timerange: str) -> str:
164 + """
165 + Determines the start time of the time range based on the current time and the provided timerange.
166 +
167 + Args:
168 + timerange (str): The time range to collect alerts from. This is a string like "24h", "1w", etc.
169 +
170 + Returns:
171 + str: A string representing the start time of the time range in ISO format.
172 + """
173 + if timerange.endswith("h"):
174 + delta = timedelta(hours=int(timerange[:-1]))
175 + elif timerange.endswith("d"):
176 + delta = timedelta(days=int(timerange[:-1]))
177 + elif timerange.endswith("w"):
178 + delta = timedelta(weeks=int(timerange[:-1]))
179 + else:
180 + raise ValueError("Invalid timerange format. Expected a string like '24h', '1d', '1w', etc.")
181 +
182 + start = datetime.utcnow() - delta
183 + return start.isoformat() + "Z" # Elasticsearch expects the time in ISO format with a Z at the end
184 +
185 + def __init__(self):
186 + self.query = {
187 + "query": {
188 + "bool": {
189 + "must": [],
190 + },
191 + },
192 + "sort": [],
193 + }
194 +
195 + def add_time_range(self, timerange: str):
196 + start = self._get_time_range_start(timerange)
197 + self.query["query"]["bool"]["must"].append({"range": {"timestamp_utc": {"gte": start, "lte": "now"}}})
198 + return self
199 +
200 + def add_matches(self, matches: Iterable[Tuple[str, str]]):
201 + for field, value in matches:
202 + self.query["query"]["bool"]["must"].append({"match": {field: value}})
203 + return self
204 +
205 + def add_range(self, field: str, value: str):
206 + self.query["query"]["bool"]["must"].append({"range": {field: {"gte": value}}})
207 + return self
208 +
209 + def add_sort(self, field: str, order: str = "desc"):
210 + self.query["sort"].append({field: {"order": order}})
211 + return self
212 +
213 + def build(self):
214 + return self.query
backend/app/static/swagger.json
+130 -41
@@ -949,7 +949,29 @@
949 "/alerts": {
950 "get": {
951 "summary": "Get alerts",
952 - "description": "Endpoint to get alerts.",
952 + "description": "Endpoint to get alerts. Retrieves all alerts from the AlertsService.",
953 + "parameters": [
954 + {
955 + "name": "size",
956 + "in": "query",
957 + "description": "The number of alerts to return. Defaults to 10.",
958 + "required": false,
959 + "schema": {
960 + "type": "integer",
961 + "default": 10
962 + }
963 + },
964 + {
965 + "name": "timerange",
966 + "in": "query",
967 + "description": "The time range to consider for alerts. Defaults to '24h'.",
968 + "required": false,
969 + "schema": {
970 + "type": "string",
971 + "default": "24h"
972 + }
973 + }
974 + ],
975 "responses": {
976 "200": {
977 "description": "Successful operation",
@@ -996,6 +1018,26 @@
1018 "description": "The name of the host to get alerts from.",
1019 "required": true,
1020 "type": "string"
1021 + },
1022 + {
1023 + "name": "size",
1024 + "in": "query",
1025 + "description": "The number of alerts to return. Defaults to 10.",
1026 + "required": false,
1027 + "schema": {
1028 + "type": "integer",
1029 + "default": 10
1030 + }
1031 + },
1032 + {
1033 + "name": "timerange",
1034 + "in": "query",
1035 + "description": "The time range to consider for alerts. Defaults to '24h'.",
1036 + "required": false,
1037 + "schema": {
1038 + "type": "string",
1039 + "default": "24h"
1040 + }
1041 }
1042 ],
1043 "responses": {
@@ -1065,46 +1107,7 @@
1107 }
1108 }
1109 },
1068 - "operationId": "getAlertsFromIndex",
1069 - "tags": ["Wazuh-Indexer"]
1070 - }
1071 - },
1072 - "/alerts/top_10": {
1073 - "get": {
1074 - "summary": "Get top 10 alerts",
1075 - "description": "Endpoint to get top 10 alerts.",
1076 - "responses": {
1077 - "200": {
1078 - "description": "Successful operation",
1079 - "content": {
1080 - "application/json": {
1081 - "schema": {
1082 - "type": "object",
1083 - "properties": {
1084 - "alerts": {
1085 - "type": "array",
1086 - "items": {
1087 - "type": "object",
1088 - "description": "Alert details"
1089 - }
1090 - }
1091 - }
1092 - }
1093 - }
1094 - }
1095 - },
1096 - "default": {
1097 - "description": "Unexpected error",
1098 - "content": {
1099 - "application/json": {
1100 - "schema": {
1101 - "$ref": "#/components/schemas/Error"
1102 - }
1103 - }
1104 - }
1105 - }
1106 - },
1107 - "operationId": "getTop10Alerts",
1110 + "operationId": "getAlertsFromAgent",
1111 "tags": ["Wazuh-Indexer"]
1112 }
1113 },
@@ -1119,6 +1122,26 @@
1122 "description": "The name of the index to get alerts from.",
1123 "required": true,
1124 "type": "string"
1125 + },
1126 + {
1127 + "name": "size",
1128 + "in": "query",
1129 + "description": "The number of alerts to return. Defaults to 10.",
1130 + "required": false,
1131 + "schema": {
1132 + "type": "integer",
1133 + "default": 10
1134 + }
1135 + },
1136 + {
1137 + "name": "timerange",
1138 + "in": "query",
1139 + "description": "The time range to consider for alerts. Defaults to '24h'.",
1140 + "required": false,
1141 + "schema": {
1142 + "type": "string",
1143 + "default": "24h"
1144 + }
1145 }
1146 ],
1147 "responses": {
@@ -1196,6 +1219,28 @@
1219 "get": {
1220 "summary": "Get hosts with alerts",
1221 "description": "Endpoint to get hosts with alerts.",
1222 + "parameters": [
1223 + {
1224 + "name": "size",
1225 + "in": "query",
1226 + "description": "The number of alerts to return. Defaults to 10.",
1227 + "required": false,
1228 + "schema": {
1229 + "type": "integer",
1230 + "default": 10
1231 + }
1232 + },
1233 + {
1234 + "name": "timerange",
1235 + "in": "query",
1236 + "description": "The time range to consider for alerts. Defaults to '24h'.",
1237 + "required": false,
1238 + "schema": {
1239 + "type": "string",
1240 + "default": "24h"
1241 + }
1242 + }
1243 + ],
1244 "responses": {
1245 "200": {
1246 "description": "Successful operation",
@@ -1235,6 +1280,28 @@
1280 "get": {
1281 "summary": "Get rules with alerts",
1282 "description": "Endpoint to get rules with alerts.",
1283 + "parameters": [
1284 + {
1285 + "name": "size",
1286 + "in": "query",
1287 + "description": "The number of alerts to return. Defaults to 10.",
1288 + "required": false,
1289 + "schema": {
1290 + "type": "integer",
1291 + "default": 10
1292 + }
1293 + },
1294 + {
1295 + "name": "timerange",
1296 + "in": "query",
1297 + "description": "The time range to consider for alerts. Defaults to '24h'.",
1298 + "required": false,
1299 + "schema": {
1300 + "type": "string",
1301 + "default": "24h"
1302 + }
1303 + }
1304 + ],
1305 "responses": {
1306 "200": {
1307 "description": "Successful operation",
@@ -1274,6 +1341,28 @@
1341 "get": {
1342 "summary": "Get rules with alerts per host",
1343 "description": "Endpoint to get rules with alerts per host",
1344 + "parameters": [
1345 + {
1346 + "name": "size",
1347 + "in": "query",
1348 + "description": "The number of alerts to return. Defaults to 10.",
1349 + "required": false,
1350 + "schema": {
1351 + "type": "integer",
1352 + "default": 10
1353 + }
1354 + },
1355 + {
1356 + "name": "timerange",
1357 + "in": "query",
1358 + "description": "The time range to consider for alerts. Defaults to '24h'.",
1359 + "required": false,
1360 + "schema": {
1361 + "type": "string",
1362 + "default": "24h"
1363 + }
1364 + }
1365 + ],
1366 "responses": {
1367 "200": {
1368 "description": "Successful operation",