alerts by host and more modular alerts.py (#24)
taylor_socfortress committed
Jul 14, 2023 at 12:59 UTC
8ea0e2447696b18b9968d63b3520fcb70ccca0f4
4 files changed
+116
-51
backend/app/routes/alerts.py
+37
-1
@@ -20,5 +20,41 @@ def get_alerts() -> jsonify:
20
containing all its associated data.
21
"""
22
service = AlertsService()
23
- alerts = service.collect_alerts()
23
+ alerts = service.collect_alerts(size=1000) # replace `collect_all_alerts` with `collect_alerts(size=1000)`
24
return jsonify(alerts)
25
+
26
+
27
+@bp.route("/alerts/top_10", methods=["GET"])
28
+def get_top_10_alerts() -> jsonify:
29
+ """
30
+ Retrieves top 10 alerts from the AlertsService.
31
+
32
+ This endpoint retrieves top 10 alerts from the AlertsService. It does this by creating an instance of
33
+ the AlertsService class and calling its `collect_alerts` method. The result is a list of top 10 alerts currently
34
+ available.
35
+
36
+ Returns:
37
+ jsonify: A JSON response containing a list of alerts. Each item in the list is a dictionary representing an alert,
38
+ containing all its associated data.
39
+ """
40
+ service = AlertsService()
41
+ alerts = service.collect_alerts(size=10) # replace `collect_top_10_alerts` with `collect_alerts(size=10)`
42
+ return jsonify(alerts)
43
+
44
+
45
+@bp.route("/alerts/hosts", methods=["GET"])
46
+def get_hosts() -> jsonify:
47
+ """
48
+ Retrieves all hosts from the AlertsService that have an alert.
49
+
50
+ This endpoint retrieves all available hosts from the AlertsService. It does this by creating an instance of
51
+ the AlertsService class and calling its `collect_alerts_by_host` method. The result is a list of all hosts currently
52
+ available.
53
+
54
+ Returns:
55
+ jsonify: A JSON response containing a list of hosts. Each item in the list is a dictionary representing a host,
56
+ containing all its associated data.
57
+ """
58
+ service = AlertsService()
59
+ hosts = service.collect_alerts_by_host()
60
+ return jsonify(hosts)
backend/app/services/WazuhIndexer/alerts.py
+68
-43
@@ -1,3 +1,4 @@
1
+from typing import Any
2
from typing import Dict
3
4
from elasticsearch7 import Elasticsearch
@@ -9,12 +10,6 @@ from app.services.WazuhIndexer.universal import UniversalService
10
class AlertsService:
11
"""
12
A service class that encapsulates the logic for pulling alerts from the Wazuh-Indexer.
12
-
13
- Attributes:
14
- connector_url (str): The url to the Wazuh-Indexer.
15
- connector_username (str): The username for the Wazuh-Indexer.
16
- connector_password (str): The password for the Wazuh-Indexer.
17
- es (Elasticsearch): The Elasticsearch client.
13
"""
14
15
SKIP_INDEX_NAMES: Dict[str, bool] = {
@@ -26,11 +21,12 @@ class AlertsService:
21
"""
22
Initializes the service by collecting Wazuh-Indexer details and creating an Elasticsearch client.
23
"""
24
+ self.universal_service = UniversalService()
25
(
26
self.connector_url,
27
self.connector_username,
28
self.connector_password,
33
- ) = UniversalService().collect_wazuhindexer_details("Wazuh-Indexer")
29
+ ) = self.universal_service.collect_wazuhindexer_details("Wazuh-Indexer")
30
self.es = Elasticsearch(
31
[self.connector_url],
32
http_auth=(self.connector_username, self.connector_password),
@@ -43,67 +39,96 @@ class AlertsService:
39
def is_index_skipped(self, index_name: str) -> bool:
40
"""
41
Checks whether the given index name should be skipped.
46
-
47
- Args:
48
- index_name (str): The name of the index.
49
-
50
- Returns:
51
- bool: True if the index should be skipped, False otherwise.
42
"""
53
- for skipped in self.SKIP_INDEX_NAMES:
54
- if index_name.startswith(skipped):
55
- return True
56
- return False
43
+ return any(index_name.startswith(skipped) for skipped in self.SKIP_INDEX_NAMES)
44
58
- def collect_alerts(self) -> Dict[str, object]:
45
+ def is_valid_index(self, index_name: str) -> bool:
46
+ """
47
+ Checks if the index name starts with "wazuh_" and is not in the SKIP_INDEX_NAMES list.
48
"""
60
- Collects the alerts from the Wazuh-Indexer where the index name starts with "wazuh_"
61
- and is not in the SKIP_INDEX_NAMES list.
62
- Returns the 10 previous alerts based on the `timestamp_utc` field.
49
+ return index_name.startswith("wazuh_") and not self.is_index_skipped(index_name)
50
64
- Returns:
65
- Dict[str, object]: A dictionary containing success status and alerts or an error message.
51
+ def _collect_indices_and_validate(self) -> Dict[str, Any]:
52
"""
67
- if not all(
68
- [self.connector_url, self.connector_username, self.connector_password],
69
- ):
70
- return {
71
- "message": "Failed to collect Wazuh-Indexer details",
72
- "success": False,
73
- }
53
+ Collect indices and validate connector details.
54
+ """
55
+ if not all([self.connector_url, self.connector_username, self.connector_password]):
56
+ return self._error_response("Failed to collect Wazuh-Indexer details")
57
75
- indices_list = UniversalService().collect_indices()
58
+ indices_list = self.universal_service.collect_indices()
59
if not indices_list["success"]:
77
- return {"message": "Failed to collect indices", "success": False}
60
+ return self._error_response("Failed to collect indices")
61
79
- alerts_summary = []
80
- for index_name in indices_list["indices_list"]:
81
- if not index_name.startswith("wazuh_") or self.is_index_skipped(index_name):
82
- continue
62
+ valid_indices = [index for index in indices_list["indices_list"] if self.is_valid_index(index)]
63
+
64
+ return {"success": True, "indices": valid_indices}
65
+
66
+ def collect_alerts(self, size: int) -> Dict[str, object]:
67
+ """
68
+ Collects alerts from the Wazuh-Indexer.
69
+ """
70
+ indices_validation = self._collect_indices_and_validate()
71
+ if not indices_validation["success"]:
72
+ return indices_validation
73
84
- alerts = self._collect_alerts(index_name)
74
+ alerts_summary = []
75
+ for index_name in indices_validation["indices"]:
76
+ alerts = self._collect_alerts(index_name, size=size)
77
if alerts["success"] and len(alerts["alerts"]) > 0:
78
alerts_summary.append(
79
{
80
"index_name": index_name,
81
"total_alerts": len(alerts["alerts"]),
90
- "last_10_alerts": alerts["alerts"],
82
+ "alerts": alerts["alerts"],
83
},
84
)
85
86
return {
95
- "message": "Successfully collected alerts",
87
+ "message": f"Successfully collected top {size} alerts",
88
"success": True,
89
"alerts_summary": alerts_summary,
90
}
91
100
- def _collect_alerts(self, index_name: str) -> Dict[str, object]:
92
+ def collect_alerts_by_host(self) -> Dict[str, int]:
93
+ """
94
+ Collects the number of alerts per host.
95
+ """
96
+ indices_validation = self._collect_indices_and_validate()
97
+ if not indices_validation["success"]:
98
+ return indices_validation
99
+
100
+ alerts_by_host_dict = {}
101
+ for index_name in indices_validation["indices"]:
102
+ alerts = self._collect_alerts(index_name=index_name, size=1000)
103
+ if alerts["success"]:
104
+ for alert in alerts["alerts"]:
105
+ host = alert["_source"]["agent_name"]
106
+ alerts_by_host_dict[host] = alerts_by_host_dict.get(host, 0) + 1
107
+
108
+ alerts_by_host_list = [{"hostname": host, "number_of_alerts": count} for host, count in alerts_by_host_dict.items()]
109
+
110
+ return {
111
+ "message": "Successfully collected alerts by host",
112
+ "success": True,
113
+ "alerts_by_host": alerts_by_host_list,
114
+ }
115
+
116
+ @staticmethod
117
+ def _error_response(message: str) -> Dict[str, bool]:
118
+ """
119
+ Standardizes the error response format.
120
+ """
121
+ return {"message": message, "success": False}
122
+
123
+ def _collect_alerts(self, index_name: str, size: int = None) -> Dict[str, object]:
124
"""
102
- Elasticsearch query to get the 10 most recent alerts where the `rule_level` is 12 or higher or the
125
+ Elasticsearch query to get the most recent alerts where the `rule_level` is 12 or higher or the
126
`syslog_level` field is `ALERT` and return the results in descending order by the `timestamp_utc` field.
127
+ The number of alerts to return can be limited by the `size` parameter.
128
129
Args:
130
index_name (str): The name of the index to query.
131
+ size (int, optional): The maximum number of alerts to return. If None, all alerts are returned.
132
133
Returns:
134
Dict[str, object]: A dictionary containing success status and alerts or an error message.
@@ -111,7 +136,7 @@ class AlertsService:
136
logger.info(f"Collecting alerts from {index_name}")
137
query = self._build_query()
138
try:
114
- alerts = self.es.search(index=index_name, body=query, size=10)
139
+ alerts = self.es.search(index=index_name, body=query, size=size)
140
alerts_list = [alert for alert in alerts["hits"]["hits"]]
141
return {
142
"message": "Successfully collected alerts",
@@ -125,7 +150,7 @@ class AlertsService:
150
@staticmethod
151
def _build_query() -> Dict[str, object]:
152
"""
128
- Builds the Elasticsearch query to get the 10 most recent alerts where the `rule_level` is 12 or higher or
153
+ Builds the Elasticsearch query to get the most recent alerts where the `rule_level` is 12 or higher or
154
the `syslog_level` field is `ALERT`.
155
156
Returns:
backend/app/services/smtp/create_report.py
+9
-5
@@ -1,13 +1,17 @@
1
-# from app.services.WazuhIndexer.alerts import AlertsService
2
-# from loguru import logger
1
+from loguru import logger
2
from reportlab.lib.pagesizes import letter
3
from reportlab.pdfgen import canvas
4
5
+from app.services.WazuhIndexer.alerts import AlertsService
6
+
7
8
def create_pdf():
8
- # service = AlertsService()
9
- # alerts = service.collect_alerts()
10
- # logger.info(alerts)
9
+ service = AlertsService()
10
+ alerts = service.collect_alerts()
11
+ alerts_by_host_percentage = service.get_alerts_by_host_percentage(alerts["alerts_summary"])
12
+ logger.info(alerts_by_host_percentage)
13
+ alerts_by_host_per_index = service.get_alerts_by_host_percentage_by_index_name(alerts["alerts_summary"])
14
+ logger.info(alerts_by_host_per_index)
15
c = canvas.Canvas("report.pdf", pagesize=letter)
16
width, height = letter
17
c.setFont("Helvetica", 24)
backend/report.pdf
+2
-2
@@ -27,7 +27,7 @@ endobj
27
endobj
28
5 0 obj
29
<<
30
-/Author (anonymous) /CreationDate (D:20230714115450+06'00') /Creator (ReportLab PDF Library - www.reportlab.com) /Keywords () /ModDate (D:20230714115450+06'00') /Producer (ReportLab PDF Library - www.reportlab.com)
30
+/Author (anonymous) /CreationDate (D:20230714131032+06'00') /Creator (ReportLab PDF Library - www.reportlab.com) /Keywords () /ModDate (D:20230714131032+06'00') /Producer (ReportLab PDF Library - www.reportlab.com)
31
/Subject (unspecified) /Title (untitled) /Trapped /False
32
>>
33
endobj
@@ -56,7 +56,7 @@ xref
56
trailer
57
<<
58
/ID
59
-[<0f08e574214c02cb66997d758d4bd311><0f08e574214c02cb66997d758d4bd311>]
59
+[<9b1b145b1b22d3a9e95daf79e858348b><9b1b145b1b22d3a9e95daf79e858348b>]
60
% ReportLab generated PDF document -- digest (http://www.reportlab.com)
61
62
/Info 5 0 R