diff --git a/src/wazuh_qa_framework/generic_modules/tools/file_regex_monitor.py b/src/wazuh_qa_framework/generic_modules/tools/file_regex_monitor.py index 4d66beb..fd77966 100644 --- a/src/wazuh_qa_framework/generic_modules/tools/file_regex_monitor.py +++ b/src/wazuh_qa_framework/generic_modules/tools/file_regex_monitor.py @@ -2,7 +2,7 @@ Module to build a tool that allow us to monitor a file content and check if the content matches with a specified callback. -We can configure this tools to check from the beggining of file or just check new lines from monitoring time. If the +We can configure this tools to check from the beginning of file or just check new lines from monitoring time. If the callback is not matched, a TimeoutError exception will be raised. The monitoring will start as soon as the object is created. We don't need to do anymore. @@ -13,12 +13,104 @@ """ import os +import re import time from wazuh_qa_framework.generic_modules.exceptions.exceptions import ValidationError, TimeoutError from wazuh_qa_framework.generic_modules.file.file import get_file_encoding +class MonitoringObject: + """Class to monitor a file and check if the content matches with the specified callback. + + Args: + description (str): String that describes the whole metadata stack. + pattern (str): Regex that is combined with the prefix to match with logs. + prefix (str): Prefix string that comes before the pattern used to match logs. + monitored_file (str): File path to monitor. + callback (function): Callback function that will be evaluated for each log line. + timeout (int): Max time to monitor and trigger the callback. + + Attributes: + description (str): String that describes the whole metadata stack. + pattern (str): Regex that is combined with the prefix to match with logs. + prefix (str): Prefix string that comes before the pattern used to match logs. + complete_pattern (str): Prefix and pattern join. + regex (re): Regex created with the prefix and pattern. + monitored_file (str): File path to monitor. + callback (function): Callback function that will be evaluated for each log line. + timeout (int): Max time to monitor and trigger the callback. + """ + def __init__(self, description=None, pattern='.*', prefix='.*', timeout=1, monitored_file=None, callback=None): + self.validate_args(description, pattern, prefix, timeout, monitored_file, callback) + + self.pattern = pattern + self.prefix = prefix + self.complete_pattern = pattern if prefix is None else fr'{prefix}{pattern}' + self.regex = re.compile(self.complete_pattern) + self.timeout = timeout + self.monitored_file = monitored_file + self.callback = callback if callback else self.get_default_callback() + self.description = description if description else self.get_default_description() + + def __str__(self): + """String representation when using str function.""" + return self.description + + def validate_args(self, description, pattern, prefix, timeout, monitored_file, callback): + """Validate the given monitoring args. + + Args: + description (str): String that describes the whole metadata stack. + pattern (str): Regex that is combined with the prefix to match with logs. + prefix (str): Prefix string that comes before the pattern used to match logs. + monitored_file (str): File path to monitor. + callback (function): Callback function that will be evaluated for each log line. + timeout (int): Max time to monitor and trigger the callback. + """ + # Validate if the specified file can be monitored. + # Check if monitored file is given + if not monitored_file: + raise ValidationError('The monitored_file arg is required. Pass an existing file to be monitored.') + + # Check that the monitored file exists + if not os.path.exists(monitored_file): + raise ValidationError(f"File {monitored_file} does not exist") + + # Check that the monitored file is a file + if not os.path.isfile(monitored_file): + raise ValidationError(f"{monitored_file} is not a file") + + # Check that the program can read the content of the file + if not os.access(monitored_file, os.R_OK): + raise ValidationError(f"{monitored_file} is not readable") + + # Validate if timeout is a positive integer + if timeout < 0: + raise ValidationError('The timeout can\'t be a negative value') + + # Validate that callback is a callable function if given + if callback and not callable(callback): + raise ValidationError(f"The given callback {callback} is not a callable function.") + + def get_default_callback(self): + """Get the default callback lambda function. + + The default callback checks if the given line matches with the monitored regex. + """ + return lambda line: self.regex.match(line.decode() if isinstance(line, bytes) else line) is not None + + def get_default_description(self): + """Get the default monitoring description. + + The default description uses the fields that represent the pattern search: + - prefix + - pattern + - monitored_file + """ + return f"MonitoringObject-{self.prefix}{self.pattern}-{self.monitored_file}" + + class FileRegexMonitor: """Class to monitor a file and check if the content matches with the specified callback. @@ -40,11 +132,8 @@ class FileRegexMonitor: callback_result (*): It will store the result returned by the callback call if it is not None. """ - def __init__(self, monitored_file, callback, timeout=10, accumulations=1, only_new_events=False, - error_message=None): - self.monitored_file = monitored_file - self.callback = callback - self.timeout = timeout + def __init__(self, monitoring, accumulations=1, only_new_events=False, error_message=None): + self.monitoring = monitoring self.accumulations = accumulations self.only_new_events = only_new_events self.error_message = error_message @@ -56,26 +145,26 @@ def __init__(self, monitored_file, callback, timeout=10, accumulations=1, only_n def __validate_parameters(self): """Validate if the specified file can be monitored.""" # Check that the monitored file exists - if not os.path.exists(self.monitored_file): - raise ValidationError(f"File {self.monitored_file} does not exist") + if not os.path.exists(self.monitoring.monitored_file): + raise ValidationError(f"File {self.monitoring.monitored_file} does not exist") # Check that the monitored file is a file - if not os.path.isfile(self.monitored_file): - raise ValidationError(f"{self.monitored_file} is not a file") + if not os.path.isfile(self.monitoring.monitored_file): + raise ValidationError(f"{self.monitoring.monitored_file} is not a file") # Check that the program can read the content of the file - if not os.access(self.monitored_file, os.R_OK): - raise ValidationError(f"{self.monitored_file} is not readable") + if not os.access(self.monitoring.monitored_file, os.R_OK): + raise ValidationError(f"{self.monitoring.monitored_file} is not readable") def __start(self): """Start the file regex monitoring""" matches = 0 - encoding = get_file_encoding(self.monitored_file) + encoding = get_file_encoding(self.monitoring.monitored_file) # Check if current file content lines triggers the callback (only when new events has False value) if not self.only_new_events: - with open(self.monitored_file, encoding=encoding) as _file: + with open(self.monitoring.monitored_file, encoding=encoding) as _file: for line in _file: - callback_result = self.callback(line) + callback_result = self.monitoring.callback(line) self.callback_result = callback_result if callback_result is not None else self.callback_result matches = matches + 1 if callback_result else matches if matches >= self.accumulations: @@ -85,7 +174,7 @@ def __start(self): start_time = time.time() # Start the file regex monitoring from the last line - with open(self.monitored_file, encoding=encoding) as _file: + with open(self.monitoring.monitored_file, encoding=encoding) as _file: # Go to the end of the file _file.seek(0, 2) while True: @@ -97,7 +186,7 @@ def __start(self): time.sleep(0.1) # If we have a new line, check if it matches with the callback else: - callback_result = self.callback(line) + callback_result = self.monitoring.callback(line) self.callback_result = callback_result if callback_result is not None else self.callback_result matches = matches + 1 if callback_result else matches # If it has triggered the callback the expected times, break and leave the loop @@ -108,6 +197,6 @@ def __start(self): elapsed_time = time.time() - start_time # Raise timeout error if we have passed the timeout - if elapsed_time > self.timeout: - raise TimeoutError(f"Events from {self.monitored_file} did not match with the callback" if - self.error_message is None else self.error_message) + if elapsed_time > self.monitoring.timeout: + raise TimeoutError(f"Events from {self.monitoring.monitored_file} did not match with the callback" + + f" from {self.monitoring}" if self.error_message is None else self.error_message) diff --git a/src/wazuh_qa_framework/meta_testing/utils.py b/src/wazuh_qa_framework/meta_testing/utils.py index b976dc4..5c23d03 100644 --- a/src/wazuh_qa_framework/meta_testing/utils.py +++ b/src/wazuh_qa_framework/meta_testing/utils.py @@ -16,6 +16,7 @@ import sys import logging +CUSTOM_PATTERN = 'wazuh-modulesd:aws-s3: INFO: Executing Service Analysis' CUSTOM_REGEX = r'.*wazuh-modulesd:aws-s3: INFO: Executing Service Analysis' DEFAULT_LOG_MESSAGE = '2023/02/14 09:49:47 wazuh-modulesd:aws-s3: INFO: Executing Service Analysis' FREE_API_URL = 'https://jsonplaceholder.typicode.com' diff --git a/tests/generic_modules/test_tools/test_file_regex_monitor/test_accumulations.py b/tests/generic_modules/test_tools/test_file_regex_monitor/test_accumulations.py index a7084f3..3025563 100644 --- a/tests/generic_modules/test_tools/test_file_regex_monitor/test_accumulations.py +++ b/tests/generic_modules/test_tools/test_file_regex_monitor/test_accumulations.py @@ -10,8 +10,8 @@ import time import pytest -from wazuh_qa_framework.meta_testing.utils import custom_callback, append_log, DEFAULT_LOG_MESSAGE -from wazuh_qa_framework.generic_modules.tools.file_regex_monitor import FileRegexMonitor +from wazuh_qa_framework.meta_testing.utils import custom_callback, append_log, CUSTOM_PATTERN, DEFAULT_LOG_MESSAGE +from wazuh_qa_framework.generic_modules.tools.file_regex_monitor import MonitoringObject, FileRegexMonitor from wazuh_qa_framework.generic_modules.exceptions.exceptions import TimeoutError from wazuh_qa_framework.generic_modules.threading.thread import Thread @@ -42,8 +42,49 @@ def test_accumulations_case_1(create_destroy_sample_file): time.sleep(0.25) # Start the file regex monitoring - file_regex_monitor_parameters = {'monitored_file': log_file, 'callback': custom_callback, 'timeout': 5, - 'only_new_events': False, 'accumulations': 2} + monitoring = MonitoringObject(pattern=CUSTOM_PATTERN, timeout=5, monitored_file=log_file) + file_regex_monitor_parameters = {'monitoring': monitoring, 'only_new_events': False, 'accumulations': 2} + file_regex_monitor_process = Thread(target=FileRegexMonitor, parameters=file_regex_monitor_parameters) + file_regex_monitor_process.start() + + # Waiting time for log to be written + time.sleep(0.25) + + # Write the event + append_log(log_file, DEFAULT_LOG_MESSAGE) + + # Check that the callback has been triggered and no exception has been raised + file_regex_monitor_process.join() + + +def test_accumulations_custom_callback_case_1(create_destroy_sample_file): + """Check the FileRegexMonitor behavior when we set the "accumulations" parameter. + + case: Pre-logged event, log another event while monitoring and expect 2 matches. + + test_phases: + - setup: + - Create an empty file. + - test: + - Log a line that triggers the monitoring callback. + - Start file monitoring. + - Log a line that triggers the monitoring callback. + - Check that no TimeoutError exception has been raised. + - teardown: + - Remove the create file in the setup phase. + + parameters: + - create_destroy_sample_file (fixture): Create an empty file and remove it after finishing. + """ + log_file = create_destroy_sample_file + + # Write the event + append_log(log_file, DEFAULT_LOG_MESSAGE) + time.sleep(0.25) + + # Start the file regex monitoring + monitoring = MonitoringObject(callback=custom_callback, timeout=5, monitored_file=log_file) + file_regex_monitor_parameters = {'monitoring': monitoring, 'only_new_events': False, 'accumulations': 2} file_regex_monitor_process = Thread(target=FileRegexMonitor, parameters=file_regex_monitor_parameters) file_regex_monitor_process.start() @@ -78,8 +119,8 @@ def test_accumulations_case_2(create_destroy_sample_file): log_file = create_destroy_sample_file # Start the file regex monitoring - file_regex_monitor_parameters = {'monitored_file': log_file, 'callback': custom_callback, 'timeout': 5, - 'only_new_events': True, 'accumulations': 2} + monitoring = MonitoringObject(pattern=CUSTOM_PATTERN, timeout=5, monitored_file=log_file) + file_regex_monitor_parameters = {'monitoring': monitoring, 'only_new_events': False, 'accumulations': 2} file_regex_monitor_process = Thread(target=FileRegexMonitor, parameters=file_regex_monitor_parameters) file_regex_monitor_process.start() @@ -116,8 +157,8 @@ def test_accumulations_case_3(create_destroy_sample_file): log_file = create_destroy_sample_file # Start the file regex monitoring - file_regex_monitor_parameters = {'monitored_file': log_file, 'callback': custom_callback, 'timeout': 1, - 'only_new_events': False, 'accumulations': 2} + monitoring = MonitoringObject(pattern=CUSTOM_PATTERN, timeout=1, monitored_file=log_file) + file_regex_monitor_parameters = {'monitoring': monitoring, 'only_new_events': False, 'accumulations': 2} file_regex_monitor_process = Thread(target=FileRegexMonitor, parameters=file_regex_monitor_parameters) file_regex_monitor_process.start() diff --git a/tests/generic_modules/test_tools/test_file_regex_monitor/test_callback.py b/tests/generic_modules/test_tools/test_file_regex_monitor/test_callback.py index 34209df..e622a35 100644 --- a/tests/generic_modules/test_tools/test_file_regex_monitor/test_callback.py +++ b/tests/generic_modules/test_tools/test_file_regex_monitor/test_callback.py @@ -11,7 +11,7 @@ import pytest from wazuh_qa_framework.meta_testing.utils import custom_callback, append_log, DEFAULT_LOG_MESSAGE -from wazuh_qa_framework.generic_modules.tools.file_regex_monitor import FileRegexMonitor +from wazuh_qa_framework.generic_modules.tools.file_regex_monitor import MonitoringObject, FileRegexMonitor from wazuh_qa_framework.generic_modules.exceptions.exceptions import TimeoutError from wazuh_qa_framework.generic_modules.threading.thread import Thread @@ -37,7 +37,8 @@ def test_callback_case_1(create_destroy_sample_file): log_file = create_destroy_sample_file # Start the file regex monitoring - file_regex_monitor_parameters = {'monitored_file': log_file, 'callback': custom_callback, 'timeout': 1} + monitoring = MonitoringObject(callback=custom_callback, timeout=1, monitored_file=log_file) + file_regex_monitor_parameters = {'monitoring': monitoring} file_regex_monitor_process = Thread(target=FileRegexMonitor, parameters=file_regex_monitor_parameters) file_regex_monitor_process.start() @@ -72,7 +73,8 @@ def test_callback_case_2(create_destroy_sample_file): log_file = create_destroy_sample_file # Start the file regex monitoring - file_regex_monitor_parameters = {'monitored_file': log_file, 'callback': custom_callback, 'timeout': 1} + monitoring = MonitoringObject(callback=custom_callback, timeout=1, monitored_file=log_file) + file_regex_monitor_parameters = {'monitoring': monitoring} file_regex_monitor_process = Thread(target=FileRegexMonitor, parameters=file_regex_monitor_parameters) file_regex_monitor_process.start() @@ -111,7 +113,8 @@ def test_callback_case_3(create_destroy_sample_file): log_file = create_destroy_sample_file # Start the file regex monitoring - file_regex_monitor_parameters = {'monitored_file': log_file, 'callback': custom_callback, 'timeout': 1} + monitoring = MonitoringObject(callback=custom_callback, timeout=1, monitored_file=log_file) + file_regex_monitor_parameters = {'monitoring': monitoring} file_regex_monitor_process = Thread(target=FileRegexMonitor, parameters=file_regex_monitor_parameters) file_regex_monitor_process.start() diff --git a/tests/generic_modules/test_tools/test_file_regex_monitor/test_callback_result.py b/tests/generic_modules/test_tools/test_file_regex_monitor/test_callback_result.py index d7d4e60..441681a 100644 --- a/tests/generic_modules/test_tools/test_file_regex_monitor/test_callback_result.py +++ b/tests/generic_modules/test_tools/test_file_regex_monitor/test_callback_result.py @@ -12,7 +12,7 @@ import re from wazuh_qa_framework.meta_testing.utils import DEFAULT_LOG_MESSAGE, append_log -from wazuh_qa_framework.generic_modules.tools.file_regex_monitor import FileRegexMonitor +from wazuh_qa_framework.generic_modules.tools.file_regex_monitor import MonitoringObject, FileRegexMonitor def custom_callback(line): @@ -55,7 +55,8 @@ def test_get_callback_group_values(create_destroy_sample_file): append_log(log_file, DEFAULT_LOG_MESSAGE) # Start the file regex monitoring - file_regex_monitor_process = FileRegexMonitor(monitored_file=log_file, callback=custom_callback, timeout=1) + monitoring = MonitoringObject(callback=custom_callback, timeout=1, monitored_file=log_file) + file_regex_monitor_process = FileRegexMonitor(monitoring=monitoring) # Check that callback results values are the expected ones. assert file_regex_monitor_process.callback_result == ('2023/02/14 09:49:47', 'aws-s3', 'INFO') diff --git a/tests/generic_modules/test_tools/test_file_regex_monitor/test_file_encoding.py b/tests/generic_modules/test_tools/test_file_regex_monitor/test_file_encoding.py index 8570115..f3a037f 100644 --- a/tests/generic_modules/test_tools/test_file_regex_monitor/test_file_encoding.py +++ b/tests/generic_modules/test_tools/test_file_regex_monitor/test_file_encoding.py @@ -12,7 +12,7 @@ import sys import pytest -from wazuh_qa_framework.generic_modules.tools.file_regex_monitor import FileRegexMonitor +from wazuh_qa_framework.generic_modules.tools.file_regex_monitor import MonitoringObject, FileRegexMonitor from wazuh_qa_framework.generic_modules.threading.thread import Thread from wazuh_qa_framework.meta_testing.utils import append_log from wazuh_qa_framework.meta_testing.configuration import get_test_cases_data @@ -73,8 +73,8 @@ def test_pre_encoding(case_parameters, create_destroy_sample_file): append_log(log_file, f"{case_parameters['pre_text']}\n", encoding=case_parameters['encoding']) # Start the file regex monitoring - file_regex_monitor_parameters = {'monitored_file': log_file, 'callback': custom_callback, 'timeout': 1, - 'only_new_events': False} + monitoring = MonitoringObject(callback=custom_callback, timeout=1, monitored_file=log_file) + file_regex_monitor_parameters = {'monitoring': monitoring, 'only_new_events': False} file_regex_monitor_process = Thread(target=FileRegexMonitor, parameters=file_regex_monitor_parameters) file_regex_monitor_process.start() @@ -108,8 +108,8 @@ def test_post_encoding(case_parameters, create_destroy_sample_file): log_file = create_destroy_sample_file # Start the file regex monitoring - file_regex_monitor_parameters = {'monitored_file': log_file, 'callback': custom_callback, 'timeout': 1, - 'only_new_events': False} + monitoring = MonitoringObject(callback=custom_callback, timeout=1, monitored_file=log_file) + file_regex_monitor_parameters = {'monitoring': monitoring, 'only_new_events': False} file_regex_monitor_process = Thread(target=FileRegexMonitor, parameters=file_regex_monitor_parameters) file_regex_monitor_process.start() @@ -146,8 +146,8 @@ def test_new_encoding(create_destroy_sample_file): append_log(log_file, 'ÿð¤¢é') # Start the file regex monitoring - file_regex_monitor_parameters = {'monitored_file': log_file, 'callback': custom_callback, 'timeout': 1, - 'only_new_events': False} + monitoring = MonitoringObject(callback=custom_callback, timeout=1, monitored_file=log_file) + file_regex_monitor_parameters = {'monitoring': monitoring, 'only_new_events': False} file_regex_monitor_process = Thread(target=FileRegexMonitor, parameters=file_regex_monitor_parameters) file_regex_monitor_process.start() diff --git a/tests/generic_modules/test_tools/test_file_regex_monitor/test_only_new_events.py b/tests/generic_modules/test_tools/test_file_regex_monitor/test_only_new_events.py index f4c34a0..b43b186 100644 --- a/tests/generic_modules/test_tools/test_file_regex_monitor/test_only_new_events.py +++ b/tests/generic_modules/test_tools/test_file_regex_monitor/test_only_new_events.py @@ -17,7 +17,7 @@ import pytest from wazuh_qa_framework.meta_testing.utils import custom_callback, append_log, DEFAULT_LOG_MESSAGE -from wazuh_qa_framework.generic_modules.tools.file_regex_monitor import FileRegexMonitor +from wazuh_qa_framework.generic_modules.tools.file_regex_monitor import MonitoringObject, FileRegexMonitor from wazuh_qa_framework.generic_modules.exceptions.exceptions import TimeoutError from wazuh_qa_framework.generic_modules.threading.thread import Thread @@ -49,8 +49,8 @@ def test_only_new_events_case_1(only_new_events, expected_exception, create_dest log_file = create_destroy_sample_file # Start the file regex monitoring - file_regex_monitor_parameters = {'monitored_file': log_file, 'callback': custom_callback, 'timeout': 5, - 'only_new_events': only_new_events} + monitoring = MonitoringObject(callback=custom_callback, timeout=5, monitored_file=log_file) + file_regex_monitor_parameters = {'monitoring': monitoring, 'only_new_events': only_new_events} file_regex_monitor_process = Thread(target=FileRegexMonitor, parameters=file_regex_monitor_parameters) file_regex_monitor_process.start() @@ -98,8 +98,8 @@ def test_only_new_events_case_2(only_new_events, expected_exception, create_dest time.sleep(0.25) # Start the file regex monitoring - file_regex_monitor_parameters = {'monitored_file': log_file, 'callback': custom_callback, 'timeout': 1, - 'only_new_events': only_new_events} + monitoring = MonitoringObject(callback=custom_callback, timeout=1, monitored_file=log_file) + file_regex_monitor_parameters = {'monitoring': monitoring, 'only_new_events': only_new_events} file_regex_monitor_process = Thread(target=FileRegexMonitor, parameters=file_regex_monitor_parameters) file_regex_monitor_process.start() @@ -136,8 +136,8 @@ def test_only_new_events_case_3(only_new_events, expected_exception, create_dest log_file = create_destroy_sample_file # Start the file regex monitoring - file_regex_monitor_parameters = {'monitored_file': log_file, 'callback': custom_callback, 'timeout': 1, - 'only_new_events': only_new_events} + monitoring = MonitoringObject(callback=custom_callback, timeout=1, monitored_file=log_file) + file_regex_monitor_parameters = {'monitoring': monitoring, 'only_new_events': only_new_events} file_regex_monitor_process = Thread(target=FileRegexMonitor, parameters=file_regex_monitor_parameters) file_regex_monitor_process.start()