@cryptotaxi247 / CoPilot / commits / 8ea0e244

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