Skip to content

Commit

Permalink
Merge pull request #58 from wazuh/18-add-monitoring-object
Browse files Browse the repository at this point in the history
Add a monitoring class to the regex monitoring tool
  • Loading branch information
davidjiglesias authored Sep 6, 2023
2 parents f09f1cc + 2276a2a commit 600fadc
Show file tree
Hide file tree
Showing 7 changed files with 183 additions and 48 deletions.
129 changes: 109 additions & 20 deletions src/wazuh_qa_framework/generic_modules/tools/file_regex_monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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
Expand All @@ -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)
1 change: 1 addition & 0 deletions src/wazuh_qa_framework/meta_testing/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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()

Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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')
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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()

Expand Down
Loading

0 comments on commit 600fadc

Please sign in to comment.