main
py 290 lines 11.6 KB
Raw
1 import json
2 from enum import Enum
3 from pathlib import Path
4 from typing import List
5 from uuid import uuid4
6
7 from fastapi import HTTPException
8 from loguru import logger
9
10 from app.connectors.graylog.schema.pipelines import CreatePipelineRule
11 from app.connectors.graylog.schema.pipelines import PipelineRulesResponse
12 from app.connectors.graylog.services.content_packs import insert_content_pack
13 from app.connectors.graylog.services.content_packs import install_content_pack
14 from app.connectors.graylog.services.pipelines import create_pipeline_rule
15 from app.connectors.graylog.services.pipelines import get_pipeline_rules
16 from app.stack_provisioning.graylog.schema.provision import AvailableContentPacks
17 from app.stack_provisioning.graylog.schema.provision import ProvisionContentPackRequest
18 from app.stack_provisioning.graylog.schema.provision import ProvisionGraylogResponse
19 from app.stack_provisioning.graylog.schema.provision import (
20 ProvisionNetworkContentPackRequest,
21 )
22 from app.stack_provisioning.graylog.schema.provision import ReplaceContentPackKeywords
23 from app.stack_provisioning.graylog.services.utils import does_content_pack_exist
24
25
26 def get_content_pack_path(file_name: str) -> Path:
27 """
28 Returns the path to the dashboard JSON file.
29
30 Parameters:
31 - dashboard_info (tuple): A tuple containing the folder name and file name of the dashboard.
32
33 Returns:
34 - Path: The path to the dashboard JSON file.
35 """
36 current_file = Path(__file__) # Path to the current file
37 base_dir = current_file.parent.parent # Move up two levels to the 'grafana' directory
38 return base_dir / "templates" / file_name
39
40
41 def load_content_pack_json(file_name: str) -> dict:
42 """
43 Load the JSON data of a dashboard from a file and replace the 'uid' value with the provided datasource UID.
44
45 Args:
46 dashboard_info (tuple): Information about the dashboard (e.g., file name, directory).
47 datasource_uid (str): The UID of the datasource to replace in the dashboard JSON.
48
49 Returns:
50 dict: The loaded dashboard data with the replaced 'uid' value.
51
52 Raises:
53 FileNotFoundError: If the dashboard JSON file is not found.
54 HTTPException: If there is an error decoding the JSON from the file.
55 """
56 logger.info(f"Loading content pack JSON file: {file_name}")
57 file_path = get_content_pack_path(file_name)
58 try:
59 with open(file_path, "r") as file:
60 content_pack_data = json.load(file)
61
62 return content_pack_data
63
64 except FileNotFoundError:
65 logger.error(f"Content pack JSON file not found at {file_path}")
66 raise HTTPException(status_code=404, detail="Content pack JSON file not found")
67
68
69 async def get_id_and_rev(data: dict) -> tuple:
70 return data.get("id"), data.get("rev")
71
72
73 # ! Only for testing purposes
74 async def write_content_pack_to_file(content_pack: dict) -> None:
75 """
76 Write the content pack to a file. Just for testing purposes.
77
78 Args:
79 content_pack (dict): The content pack to write to a file.
80 """
81 file_path = get_content_pack_path("wazuh_content_pack_testing.json")
82 with open(file_path, "w") as file:
83 json.dump(content_pack, file, indent=4)
84
85
86 async def retrieve_valid_content_packs(content_pack_type: str) -> list:
87 """
88 Returns a list of content pack template names based on the type.
89
90 Args:
91 content_pack_type (str): The type of content pack to retrieve.
92
93 Returns:
94 list: A list of content pack template names.
95 """
96 available_content_packs = [pack.name for pack in AvailableContentPacks]
97 logger.info(f"Available content packs: {available_content_packs}")
98 # Create a list of valid content pack names based on the content pack type
99 valid_content_pack_names = []
100 for content_pack in available_content_packs:
101 if content_pack_type in content_pack:
102 valid_content_pack_names.append(content_pack)
103 return valid_content_pack_names
104
105
106 def replace_keywords_in_json_complex(data, replacements):
107 """
108 Recursively replace specified keywords in JSON data, including within strings, with the provided values in the replacements dictionary.
109
110 Args:
111 data (dict or list): The JSON data in which replacements need to be made.
112 replacements (dict): A dictionary mapping keywords to their respective replacement values.
113
114 Returns:
115 dict or list: The modified JSON data with the keywords replaced.
116 """
117 if isinstance(data, dict):
118 return {key: replace_keywords_in_json_complex(value, replacements) for key, value in data.items()}
119 elif isinstance(data, list):
120 return [replace_keywords_in_json_complex(item, replacements) for item in data]
121 elif isinstance(data, str):
122 for key, value in replacements.items():
123 data = data.replace(key, str(value))
124 return data
125 else:
126 return data
127
128
129 def convert_port_value_to_int(data):
130 """
131 Recursively navigates through a JSON-like dictionary and converts the port value to an integer.
132
133 Args:
134 data (dict or list): The JSON data in which the port value needs to be converted.
135
136 Returns:
137 dict or list: The modified JSON data with the port value converted to integer.
138 """
139 if isinstance(data, dict):
140 for key, value in data.items():
141 if key == "port" and isinstance(value, dict) and "@value" in value and isinstance(value["@value"], str):
142 try:
143 # Convert the string to an integer
144 value["@value"] = int(value["@value"])
145 except ValueError:
146 # Handle the case where the string cannot be converted to an integer
147 pass
148 else:
149 # Recurse into the value
150 data[key] = convert_port_value_to_int(value)
151 elif isinstance(data, list):
152 # Process each item in the list
153 data = [convert_port_value_to_int(item) for item in data]
154 return data
155
156
157 async def provision_content_pack(content_pack_request: ProvisionContentPackRequest) -> ProvisionGraylogResponse:
158 """
159 Provision the Wazuh Content Pack in the Graylog instance
160 """
161 logger.info(
162 f"Provisioning {content_pack_request.content_pack_name.name} Content Pack with keywords {content_pack_request.keywords} ...",
163 )
164
165 content_pack = load_content_pack_json(f"{content_pack_request.content_pack_name.name}.json")
166 # ! Only for testing purposes
167 # await write_content_pack_to_file(content_pack)
168 # return ProvisionGraylogResponse(success=True, message=f"{content_pack_request.content_pack_name.name} Content Pack provisioned successfully")
169
170 logger.info(f"Inserting {content_pack_request.content_pack_name.name} Content Pack...")
171 await insert_content_pack(content_pack)
172 # ! Content Pack ID is found in the first `id` field and the revision is found in the first `rev` field
173 id, rev = await get_id_and_rev(content_pack)
174 logger.info(f"Id: {id}, Rev: {rev}")
175 await install_content_pack(content_pack_id=id, revision=rev)
176 return ProvisionGraylogResponse(
177 success=True,
178 message=f"{content_pack_request.content_pack_name.name} Content Pack provisioned successfully",
179 )
180
181
182 class PipelineRuleTitles(Enum):
183 ETW = "Set Syslog Level to ALERT for ETW Registry"
184
185
186 async def check_pipeline_rules() -> None:
187 """
188 Checks if the pipeline rules exist in Graylog. If they don't, create them.
189 """
190 pipeline_rules = await get_pipeline_rules()
191 non_existing_rules = await pipeline_rules_exists(pipeline_rules)
192 if non_existing_rules:
193 logger.info(f"Creating pipeline rules: {non_existing_rules}")
194 await create_pipeline_rules(non_existing_rules)
195
196
197 async def pipeline_rules_exists(pipeline_rules: PipelineRulesResponse) -> List[str]:
198 """
199 Checks if the pipeline rules exist in Graylog and returns a list of non-existing pipeline rules.
200 """
201 return [
202 rule_title.value
203 for rule_title in PipelineRuleTitles
204 if not any(rule.title == rule_title.value for rule in pipeline_rules.pipeline_rules)
205 ]
206
207
208 async def create_pipeline_rules(non_existing_rules: List[str]) -> None:
209 """
210 Creates the given pipeline rules.
211 """
212 rule_creators = {
213 "Set Syslog Level to ALERT for ETW Registry": create_etw_rule,
214 }
215
216 for rule_title in non_existing_rules:
217 logger.info(f"Creating pipeline rule {rule_title}.")
218 await rule_creators[rule_title](rule_title)
219
220
221 async def create_etw_rule(rule_title: str) -> None:
222 """
223 Creates the 'Set Syslog Level to ALERT for ETW Registry' pipeline rule.
224 """
225 rule_source = (
226 f'rule "{rule_title}"\n'
227 "when\n"
228 ' has_field("syscheck_path") &&\n'
229 ' starts_with(to_string($message.syscheck_path), "HKEY_LOCAL_MACHINE\\\\SYSTEM\\\\CurrentControlSet\\\\Control\\\\WMI\\\\Autologger\\\\EventLog")\n'
230 "then\n"
231 ' set_field("syslog_level", "ALERT");\n'
232 "end"
233 )
234 await create_pipeline_rule(
235 CreatePipelineRule(
236 title=rule_title,
237 description=rule_title,
238 source=rule_source,
239 ),
240 )
241
242
243 # ! NETWORK CONNECTOR CONTENT PACKS PROVISIONING ! #
244 async def filter_content_packs(content_packs, protocol_type):
245 if protocol_type == "TCP":
246 return [pack for pack in content_packs if "UDP" not in pack]
247 if protocol_type == "UDP":
248 return [pack for pack in content_packs if "TCP" not in pack]
249 return content_packs
250
251
252 async def process_content_pack(content_pack, content_pack_request):
253 content_pack_exists = await does_content_pack_exist(content_pack_request.keywords.customer_name)
254 if content_pack_exists is True:
255 return
256 content_pack = load_content_pack_json(f"{content_pack}.json")
257 replace_content_pack_keywords = ReplaceContentPackKeywords(
258 REPLACE_UUID_GLOBAL=str(uuid4()),
259 REPLACE_UUID_SPECIFIC=str(uuid4()),
260 customer_name=content_pack_request.keywords.customer_name,
261 customer_code=content_pack_request.keywords.customer_code,
262 SYSLOG_PORT=content_pack_request.keywords.syslog_port,
263 TLS_CERT_FILE=content_pack_request.keywords.tls_cert_file,
264 TLS_KEY_FILE=content_pack_request.keywords.tls_key_file,
265 )
266 if "PROCESSING_PIPELINE" not in content_pack:
267 content_pack = replace_keywords_in_json_complex(content_pack, replace_content_pack_keywords.model_dump())
268 content_pack = convert_port_value_to_int(content_pack)
269 await insert_and_install_content_pack(content_pack)
270
271
272 async def insert_and_install_content_pack(content_pack):
273 logger.info(f"Inserting {content_pack} Content Pack...")
274 content_pack_inserted = await insert_content_pack(content_pack)
275 id, rev = await get_id_and_rev(content_pack)
276 logger.info(f"Id: {id}, Rev: {rev}")
277 if content_pack_inserted is True:
278 await install_content_pack(content_pack_id=id, revision=rev)
279 else:
280 logger.info(f"Content pack {content_pack['name']} already inserted, skipping install...")
281
282
283 async def provision_content_pack_network_connector(content_pack_request: ProvisionNetworkContentPackRequest) -> ProvisionGraylogResponse:
284 logger.info(f"Provisioning {content_pack_request.content_pack_name} Content Pack with keywords {content_pack_request.keywords} ...")
285 content_packs = await retrieve_valid_content_packs(content_pack_request.content_pack_name)
286 content_packs = await filter_content_packs(content_packs, content_pack_request.keywords.protocol_type)
287 logger.info(f"Valid content packs: {content_packs}")
288 for content_pack in content_packs:
289 await process_content_pack(content_pack, content_pack_request)
290 return ProvisionGraylogResponse(success=True, message=f"{content_pack_request.content_pack_name} Content Pack provisioned successfully")