#TOTEST Cleared
"""
Set of function independant from any class that can be run remotely on VM
Contains 
    - command functions
    - packages checks
    - ...

"""
import csv
import logging
from variables import DEBUG_LEVEL,PATH_TO_RUN_SSH_FUNCTION,PATH_TO_RUN_SSH_SCRIPT,PATH_TO_BASH_SCRIPTS
from typing import Union
import importlib

import subprocess
# Use getattr() to dynamically access the logging level
log_level = getattr(logging, DEBUG_LEVEL, logging.INFO)  # Default to logging.INFO if not found

# Set up logging
logging.basicConfig(
    format='%(asctime)s - %(levelname)s - %(message)s',
    level=log_level,
    handlers=[logging.StreamHandler()]  # Logs to console by default
)

import os
import logging
import shutil
import ast
import pandas as pd
from datetime import datetime

def check_file_existence(files: list) -> int:
    """
    Check for the existence of files and create them if they do not exist.

    Args:
        files (list): List of file paths to check.

    Returns:
        int: 0 if all files exist, 1 otherwise.
    """
    logging.info("Running check_file_existence")
    number_of_files_not_found = 0

    for file in files:
        if not os.path.isfile(file):
            logging.warning(f"File not found: {file}. Creating the file.")
            open(file, 'a').close()  # Create the file
            number_of_files_not_found += 1
        else:
            logging.debug(f"File found: {file}")

    if number_of_files_not_found > 0:
        logging.debug(f"Total warnings: {number_of_files_not_found}")
        return 1
    else:
        logging.debug("All files found.")
        return 0

def check_folder_existence(folders: list) -> int:
    """
    Check for the existence of folders and create them if they do not exist.

    Args:
        folders (list): List of folder paths to check.

    Returns:
        int: 0 if all folders exist, 1 otherwise.
    """
    logging.info("Running check_folder_existence")
    number_of_folders_not_found = 0

    for folder in folders:
        if not os.path.isdir(folder):
            logging.warning(f"Folder not found: {folder}. Creating the folder.")
            os.makedirs(folder, exist_ok=True)
            number_of_folders_not_found += 1
        else:
            logging.debug(f"Folder found: {folder}")

    if number_of_folders_not_found > 0:
        logging.debug(f"Total warnings: {number_of_folders_not_found}")
        return 1
    else:
        logging.debug("All folders found.")
        return 0

def save_backup(file_path: str, backup_folder: str, max_files: int = 4):
    """
    Create a backup of the existing file with the date included in the pathname.
    Ensures that no more than max_files backups are kept.

    Args:
        file_path (str): Path to the file to back up.
        backup_folder (str): Path to the backup folder.
        max_files (int): Maximum number of backup files to keep. Default is 10.
    """
    logging.info("Running save_backup")

    try:
        # Ensure the backup folder exists
        check_folder_existence([backup_folder])

        # Create a backup of the existing file with the date included in the pathname
        if os.path.exists(file_path):
            backup_filename = f"{os.path.basename(file_path)}.{datetime.now().strftime('%Y%m%d%H%M%S')}"
            backup_path = os.path.join(backup_folder, backup_filename)
            shutil.copyfile(file_path, backup_path)
            logging.debug(f"Backup saved to {backup_path}")

            # Rotate backups to keep only the latest max_files
            backup_files = sorted(
                [f for f in os.listdir(backup_folder) if f.startswith(os.path.basename(file_path))],
                key=lambda x: os.path.getctime(os.path.join(backup_folder, x))
            )
            if len(backup_files) > max_files:
                for old_file in backup_files[:-max_files]:
                    old_file_path = os.path.join(backup_folder, old_file)
                    os.remove(old_file_path)
                    logging.debug(f"Removed old backup: {old_file_path}")
        else:
            logging.warning(f"File not found: {file_path}. No backup created.")
    except Exception as e:
        logging.error(f"Error creating backup: {e}")
        raise


def remove_file_if_exists(file_path: str) -> None:
    """
    Remove the file if it exists.

    Args:
        file_path (str): Path to the file to remove.
    """
    logging.info(f"Running remove_file_if_exists for {file_path}")

    try:
        if os.path.exists(file_path):
            os.remove(file_path)
            if os.path.exists(file_path):
                raise RuntimeError(f"File {file_path} was not successfully removed.")
            logging.debug(f"File removed: {file_path}")
        else:
            logging.debug(f"File not found: {file_path}")
    except Exception as e:
        logging.error(f"Error removing file: {e}")
        raise


def remove_duplicates(df: pd.DataFrame, subset: list) -> pd.DataFrame:
    """
    Remove duplicate rows based on the 'text' key within dictionaries in the specified columns.

    Args:
        df (pd.DataFrame): The DataFrame to process.
        subset (list): List of columns to check for duplicates.

    Returns:
        pd.DataFrame: DataFrame with duplicates removed.
    """
    def row_to_tuple(row, subset):
        """
        Convert a row to a tuple based on the 'text' key within dictionaries in the specified columns.

        Args:
            row (pd.Series): The row to process.
            subset (list): List of columns to check for duplicates.

        Returns:
            tuple: A tuple representing the row.
        """
        row_tuple = []
        for col in subset:
            if isinstance(row[col], list):
                for item in row[col]:
                    if isinstance(item, dict) and 'text' in item:
                        row_tuple.append((col, item['text']))
            elif isinstance(row[col], dict) and 'text' in row[col]:
                row_tuple.append((col, row[col]['text']))
            else:
                row_tuple.append((col, row[col]))
        return tuple(row_tuple)

    seen = set()
    unique_rows = []
    for _, row in df.iterrows():
        row_tuple = row_to_tuple(row, subset)
        if row_tuple not in seen:
            seen.add(row_tuple)
            unique_rows.append(row)

    return pd.DataFrame(unique_rows)


def convert_to_dict(value):
    """
    Recursively convert string representations of dictionaries back to dictionaries.

    Args:
        value: The value to convert.

    Returns:
        The converted value.
    """
    if isinstance(value, str) and value.startswith('{'):
        try:
            return ast.literal_eval(value)
        except (SyntaxError, ValueError):
            return value
    elif isinstance(value, dict):
        return {k: convert_to_dict(v) for k, v in value.items()}
    elif isinstance(value, list):
        return [convert_to_dict(v) for v in value]
    else:
        return value


def format_kwargs(kwargs: dict) -> dict:
    """
    Format kwargs to ensure each value is a dictionary with a 'text' key.

    Args:
        kwargs (dict): The keyword arguments to format.

    Returns:
        dict: The formatted keyword arguments.
    """
    formatted_kwargs = {}
    for key, value in kwargs.items():
        if isinstance(value, dict):
            formatted_kwargs[key] = [value]
        elif isinstance(value, list):
            formatted_kwargs[key] = [
                {'text': item} if not isinstance(item, dict) else (
                    item if 'text' in item else ValueError(f"Missing 'text' key in dictionary item: {item}")
                )
                for item in value
            ]

            
        else:
            formatted_kwargs[key] = [{'text': value}]
    return formatted_kwargs


def synchronize_with_VM(conf_path: str, remote_host: str, VM_conf_path: str):
    """
    Synchronize a configuration file with a remote VM.

    Args:
        conf_path (str): Local configuration file to synchronize.
        remote_host (str): Remote host address for synchronization.
        VM_conf_path (str): Path to the configuration file on the VM.

    Raises:
        AssertionError: If inputs are invalid.
        RuntimeError: If synchronization fails.
    """
    logging.info(f"Starting VM synchronization for file: {conf_path} to {remote_host}:{VM_conf_path}.")

    # Assertions for input validation
    assert isinstance(conf_path, str) and os.path.isfile(conf_path), f"Local configuration file {conf_path} does not exist."
    assert isinstance(remote_host, str) and remote_host.strip(), "remote_host must be a valid non-empty string."
    assert isinstance(VM_conf_path, str) and VM_conf_path.strip(), "VM_conf_path must be a valid non-empty string."

    try:
        conf_path_remote = os.path.abspath(conf_path)
        
        
        # Find the part of the path starting with '/vagrant'
        if "/vagrant" in conf_path_remote:
            conf_path_remote = conf_path_remote[conf_path_remote.index("/vagrant"):]


        command = [PATH_TO_RUN_SSH_FUNCTION, remote_host, 'synchronize_file', conf_path_remote, VM_conf_path]
        
        result = run_command(command)

        if result.returncode != 0:
            logging.error(f"Synchronization failed: {result.stderr}")
            raise RuntimeError(f"Synchronization failed with error: {result.stderr}")
        logging.debug(f"Synchronization successful: {result.stdout}")
    except Exception as e:
        logging.error(f"Error during synchronization with VM: {e}")
        raise


def is_consistent_value(value):
    """
    Check if the value is valid (not None and not NaN).

    Args:
        value: The value to check.

    Returns:
        bool: True if the value is valid, False otherwise.
    """
    if isinstance(value, list):
        return all(is_consistent_value(item) for item in value)
    elif pd.isna(value):
        return False
    elif value is None:
        return False
    return True

import psutil
import os

def kill_related_subprocesses(command):
    """
    Kill any subprocesses that match the given command.

    Args:
        command (list): The command to match against running processes.
    """
    for proc in psutil.process_iter(['pid', 'name', 'cmdline']):
        try:
            if command == proc.info['cmdline']:
                logging.debug(f"Killing related subprocess with PID {proc.info['pid']}")
                proc.kill()
        except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
            pass


import subprocess
import logging
import sys
def run_command(command: list[str]) -> subprocess.CompletedProcess:
    """
    Executes a shell command using subprocess, captures its output, and logs it.

    Args:
        command (list[str]): A list of strings representing the command to execute.
                             Each part of the command (program, arguments) should be a separate list item.

    Returns:
        None: Logs the stdout and stderr output from the command execution.

    NOTE:
        - # INFO Do not restart any system before doing a command , this could interfere ! (or add a delay)
        - Captures both stdout and stderr from the subprocess.
        - Uses `kill_related_subprocesses` to terminate any related processes before executing the command.
        - Ensures `shell=False` for better security unless explicitly required.

    Raises:
        Exception: Logs errors if the command fails and outputs stderr for debugging.
    """
    # Kill any related subprocesses to avoid conflicts
    kill_related_subprocesses(command)

    # Log the command being executed for debugging purposes
    logging.debug(f"Executing command: {' '.join(command)}")

    # Run the subprocess and capture all output
    result = subprocess.run(
        command,
        capture_output=True,  # Captures stdout and stderr
        text=True,  # Ensures output is returned as a string (not bytes)
        shell=False  # Use shell=False for security unless necessary
    )
    
    # Log the stdout from the command
    if result.stdout:
        logging.debug(f"Command stdout: {result.stdout}")
    
    # Log the stderr from the command, if any
    if result.stderr:
        logging.error(f"Command stderr: {result.stderr}")
        
    return result

def run_command_with_pipe(command1: list[str], command2: list[str]) -> subprocess.CompletedProcess:
    """
    Executes two shell commands connected by a pipe using subprocess.
    Captures the output of the first command and passes it to the second.

    Args:
        command1 (list[str]): The first command as a list of strings.
        command2 (list[str]): The second command as a list of strings.

    Returns:
        subprocess.CompletedProcess: The result of the second command.
    """

    # Strip whitespace from each argument in the commands
    command1 = [arg.strip() for arg in command1]
    command2 = [arg.strip() for arg in command2]

    # Execute the first command and pipe its output to the second command
    proc1 = subprocess.Popen(command1, stdout=subprocess.PIPE, text=True)
    proc2 = subprocess.Popen(command2, stdin=proc1.stdout, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
    proc1.stdout.close()  # Allow proc1 to receive a SIGPIPE if proc2 exits
    stdout, stderr = proc2.communicate()

    # Log the results for debugging
    logging.debug(f"Joining Command1: {' '.join(command1)} | Command2: {' '.join(command2)}")
    if stdout:
        logging.debug(f"Pipe command stdout: {stdout.strip()}")
    if stderr:
        logging.error(f"Pipe command stderr: {stderr.strip()}")

    return subprocess.CompletedProcess(args=command2, returncode=proc2.returncode, stdout=stdout, stderr=stderr)

def run_function_on_remote_host(remote_host: str, python_function: str, *args:list):
    """
    Run a Python or bash function on a remote host via SSH.

    This function constructs a command to execute a Python function on a remote host.
    It uses a Bash script to handle the SSH connection and execution of the Python function.

    Args:
        remote_host (str): The remote host where the Python function will be executed.
        python_function (str): The name of the Python function to be executed (#NOTE : by default should be in utils_test.py !).
        bash_function (str): The name of the Python function to be executed (#NOTE : by default should be in utils.sh !).
        *args: Additional arguments to be passed to the Python function , can be a string or a list or string.
        source_library (str, optional): The path to the source library where the Python function is located.
                                        Defaults to PATH_TO_UTILS_WAZUH.

    Returns:
    None
    """

    # Construct the command to be executed
    # The command includes the path to the Bash script, the remote host, the Python function,
    # the source library path, and the arguments string
           
    # Ensure the correct directory
    target_dir = 'src'
    if os.path.basename(os.getcwd()) != target_dir:
        os.chdir(target_dir)

    print("Current Directory:", os.getcwd())
    command = [
        PATH_TO_RUN_SSH_FUNCTION,  # Path to the Bash script
        remote_host,                                # Remote host
        python_function,                            # Python function to run
    ]
    command.extend(args)

    # Run the command using the run_command function
    # Assuming run_command is a function that takes a list of command arguments and executes them
    run_command(command)

    return


def run_bash_script_on_remote_host(remote_host: str, bash_script_name: str, *args: list):
    """
    Run a Bash script on a remote host via SSH.

    This function constructs a command to execute a Bash script on a remote host.
    It uses SSH to handle the connection and execution of the Bash script.

    Args:
        remote_host (str): The remote host where the Bash script will be executed.
        bash_script_name (str): The name of the Bash script to be executed.
        *args: Additional arguments to be passed to the Bash script.

    Returns:
        None
    """
        
    # Ensure the correct directory
    target_dir = 'src'
    if os.path.basename(os.getcwd()) != target_dir:
        os.chdir(target_dir)

    print("Current Directory:", os.getcwd())
    # Construct the command to be executed
    # The command includes the path to the Bash script, the remote host, the Bash script,
    # and the arguments string
    path_to_bash_script = os.path.join(PATH_TO_BASH_SCRIPTS,bash_script_name)
    command = [
        PATH_TO_RUN_SSH_SCRIPT,                  # Path to bash script runner
        remote_host,                             # Remote host
        path_to_bash_script,                     # Path to bash script
    ]

    # Add the Bash script content and arguments
    command.extend(args)

    # Run the command using subprocess
    run_command(command)

    return


def remove_duplicates_from_list(lst: list) -> list:
    """
    Remove duplicate dictionaries from a list.

    Args:
        lst (list): List of dictionaries.

    Returns:
        list: List with duplicate dictionaries removed.
    """
    def dict_to_tuple(d):
        """
        Convert a dictionary to a tuple for comparison.

        Args:
            d (dict): The dictionary to convert.

        Returns:
            tuple: A tuple representation of the dictionary.
        """
        return tuple(sorted((k, dict_to_tuple(v)) if isinstance(v, dict) else (k, v) for k, v in d.items()))

    seen = set()
    unique_lst = []
    for item in lst:
        if isinstance(item, dict):
            item_tuple = dict_to_tuple(item)
            if item_tuple not in seen:
                seen.add(item_tuple)
                unique_lst.append(item)
        else:
            unique_lst.append(item)
    return unique_lst


# HERE : Packages monitoring

def check_and_install_packages(packages:list[str]):
    """
    Ensure that the given list of packages is installed.

    Args:
        packages (list of str): List of package names to ensure installation.
    """
    try:
        for package in packages:
            logging.debug(f"Checking if package '{package}' is installed.")
            result = subprocess.run(["dpkg", "-s", package], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)

            if result.returncode != 0:
                logging.debug(f"Package '{package}' is not installed. Installing now.")
                run_command(["sudo", "apt", "install", "-y", package])
                logging.debug(f"Package '{package}' installed successfully.")
            else:
                logging.debug(f"Package '{package}' is already installed.")
    except Exception as e:
        logging.critical("Failed to check or install packages. Exiting.")
        raise

#HERE Basic check for ossec.conf : 
def check_in_tag(tag: str, text: str, ossec_path: str = "/var/ossec/etc/ossec.conf", attributes: dict = None):
    """
    Check for the presence of a specific text in the ossec.conf file within a specified tag and attributes.

    Args:
        tag (str): The tag to focus on.
        text (str): The text to search for within the tag.
        ossec_path (str): The path to the ossec.conf file. Default is "/var/ossec/etc/ossec.conf".
        attributes (dict): The attributes to check within the tag. Default is None.

    Returns:
        bool: True if the text is found, False otherwise.
    """
    logging.info(f"Checking for text '{text}' in tag '{tag}' with attributes {attributes} in {ossec_path}")

    if not os.path.isfile(ossec_path):
        logging.error(f"ossec.conf file not found at {ossec_path}.")
        return False

    with open(ossec_path, 'r') as file:
        lines = file.readlines()

    in_tag = False
    for line in lines:
        stripped_line = line.strip()
        if stripped_line.startswith(f"<{tag}"):
            in_tag = True
            if attributes:
                for key, value in attributes.items():
                    if f'{key}="{value}"' not in stripped_line:
                        in_tag = False
                        break
        elif stripped_line.startswith(f"</{tag}>"):
            in_tag = False

        if in_tag and text in stripped_line:
            logging.debug(f"Text '{text}' found in tag '{tag}' with attributes {attributes}.")
            return True

    logging.warning(f"Text '{text}' not found in tag '{tag}' with attributes {attributes}.")
    return False

# HERE : Restarts
import time
def restart(elements:Union[list[str],str]):
    """
    Restart one or multiple services to apply configuration changes.

    Args:
        elements (str or list of str): Name(s) of the service(s) to restart.
        
    INFO: Adjust time.sleep regarding the element to resstart, it can interfere with other commands !
    """
    if isinstance(elements, str):
        elements = [elements]  # Convert single element to a list

    for element in elements:
        RESTART_CMD = ["systemctl", "restart", element]
        try:
            subprocess.run(RESTART_CMD, check=True)
            time.sleep(2)
            logging.debug(f"{element} restarted successfully.")
        except subprocess.CalledProcessError as e:
            logging.error(f"Failed to restart {element}: {e}")
## HERE Relative import resolution ##

import os
import sys
import importlib
import logging

def load_classes_from_relative_dir(filename: str, class_folder_relative_path: str = None):
    """
    This function dynamically loads classes from Python files in the specified folder (class_folder_relative_path).
    It expects each Python file (excluding __init__.py) to contain a class named after the file (with the first letter capitalized).

    Args:
        current_dir (str): Current working directory from where doing the import (e.g., agents_elements dir where agent imports all classes).
        class_folder_relative_path (str): The relative path to the folder containing the Python files (e.g., 'common').
    NOTE: 
        Ignore current file
    Returns: 
        classes (list): A list of classes dynamically loaded from the Python files.
    """
    classes = []
    current_dir = os.path.dirname(filename)

    logging.debug(f"Current directory: {current_dir}")

    # Is the class inside the same folder of the other classes to load ?
    if class_folder_relative_path:
        class_folder_path = os.path.join(current_dir, class_folder_relative_path)
    else:
        class_folder_path = current_dir

    # Add the current directory to sys.path if not already present
    if current_dir not in sys.path:
        sys.path.append(current_dir)
        logging.debug(f"Added current directory to sys.path: {sys.path}")
    else:
        logging.debug(f"Current directory already in sys.path: {sys.path}")

    # Get the folder name dynamically
    folder_name = os.path.basename(class_folder_path)
    logging.debug(f"Folder name derived from path: {folder_name}")

    # Iterate through the files in the specified folder (class_folder_path)
    logging.debug(f"Listing files in folder: {class_folder_path}")
    for file in os.listdir(class_folder_path):
        logging.debug(f"Checking file: {file}")

        # Only process .py files (excluding __init__.py)
        if file.endswith(".py") and file not in  ["__init__.py",os.path.basename(filename),'__pycache__.py']:
            # Derive the class name (e.g., 'fim' -> 'Fim')
            class_name = file[:-3].capitalize()
            logging.debug(f"Class name derived from file: {class_name}")

            # Construct the module name dynamically using the folder name
            if class_folder_relative_path:
                module_name = f"{class_folder_relative_path.replace(os.sep, '.')}.{file[:-3]}"
            else :
                module_name = f"{file[:-3]}"

            logging.debug(f"Module name: {module_name}")

            try:
                # Dynamically import the module using importlib
                module = importlib.import_module(module_name)
                logging.debug(f"Successfully imported module: {module_name}")

                # Retrieve the class from the module
                cls = getattr(module, class_name, None)
                if cls:
                    logging.debug(f"Class {class_name} found in module {module_name}")
                    classes.append(cls)
                else:
                    logging.error(f"Class {class_name} not found in module {module_name}")
            except ModuleNotFoundError as e:
                # Handle the case where the module cannot be found
                logging.error(f"Module {module_name} not found: {e}")

    return classes

## HERE FIM ##

"""
Set of function to be run remotely on VM regarding fim managements.
Contains mainly tests.
No call of any class
"""

import os
import time

def simulate_file_change(monitored_path: str, test_file_name: str = "test.txt"):
    """
    Simulate file changes in the monitored directory & checks for alerts.

    Args:
        monitored_path (str): Path of the directory to monitor.
        test_file_name (str): Name of the test file to create and modify. Defaults to 'test.txt'.
    """
    # Remove whitespace from the directory argument
    monitored_path = monitored_path.strip().replace(" ", "")
    # Create the test directory if it doesn't exist
    logging.info(f"Simulating a file change in folder {monitored_path}.")

    os.makedirs(monitored_path, exist_ok=True)
    test_file_path = os.path.join(monitored_path, test_file_name)

    # If the test file already exists, remove it
    if os.path.exists(test_file_path):
        os.remove(test_file_path)
        logging.debug(f"Existing test file at {test_file_path} removed.")

    # Create the test file and write initial content
    with open(test_file_path, "w") as test_file:
        test_file.write(f"Initial content - FIM test for {monitored_path}")
    logging.debug(f"Test file created at {test_file_path}.")

    # Assertions to ensure the file exists and is created properly
    assert os.path.exists(test_file_path), f"Error: {test_file_path} was not created."
    assert os.path.getsize(test_file_path) > 0, f"Error: {test_file_path} is empty."

    # Simulate a file modification
    logging.debug("Modifying the file...")
    time.sleep(2)  # Wait for the manager to start monitoring

    # Modify the test file by appending content
    with open(test_file_path, "a") as test_file:
        test_file.write("\nModified content.")

    logging.debug(f"File modified at {test_file_path}.")

    # Assertion to verify the file was modified
    with open(test_file_path, "r") as test_file:
        content = test_file.read()
    assert "Modified content" in content, f"Error: 'Modified content' was not found in {test_file_path}."



# HERE audit - whodata


def check_audit_rule(directory: str, rule_name: str = 'wazuh_fim'):
    """
    Check if a specific audit rule is applied and apply it if not.
    # NOTE : To be run on a defender 
    Args:
        rule_name (str): The name of the rule to check.
        directory (str): The directory to monitor.
        permissions (str): The permissions to monitor.
        key (str): The key for the audit rule.
    """
    logging.info(f"Checking if '{rule_name}' rule is applied.")

    # Restarting...
    restart(['wazuh-agent', 'auditd'])
    time.sleep(2)
    # Running command
    command1 = ['auditctl', '-l']
    command2 = ['grep', rule_name]
    output = run_command_with_pipe(command1, command2)

    logging.debug(f"OUTPUT of the command : {output.stdout.strip()} \n")
    
    if len(output.stdout) == 0:
        logging.error(f"'{rule_name} not configured for audit ... , please configure the rule before adding directories")
        sys.exit(1)
    elif directory not in output.stdout.split():
        logging.warning(f"'{directory}' rule not found. Checking for {directory} directory in the configuration file.")
        if not check_in_tag(tag="directories", text=directory, attributes={"check_all": "yes", "whodata": "yes"}):
            logging.error(f"Directory isn't well configured in ossec.conf: {directory}")
            sys.exit(1)
                         
        else:
            logging.debug(f"Successfully configured '{directory}' directory for rule {rule_name} in ossec.conf")
            logging.error(f"Rule isn't well configured in audit: {directory}")
            sys.exit(1)
           
    else:
        logging.debug(f"'{directory}' configured for audit and rule {rule_name}")

def configure_audit():
    """
    Configure auditd to ensure it is not disabled by default and key rules are in place.
    NOTE :
       - To be run on a defender
    [Doc] : https://documentation.wazuh.com/current/user-manual/capabilities/file-integrity/advanced-settings.html
    """
    logging.debug("Running function configure_audit.")

    audit_packages = ['auditd', 'audispd-plugins']

    # Ensure auditd and its plugins are installed
    check_and_install_packages(audit_packages)

    # Check and remove '-a never,task' rule if present
    try:
        logging.debug("Checking for '-a never,task' rule.")

        rule_name = 'task'
        command1 = ['auditctl', '-l']
        command2 = ['grep', rule_name]
        output = run_command_with_pipe(command1, command2)

        if "-a never,task" in output.stdout.split():
            logging.debug("Detected '-a never,task' rule. Removing it.")
            with open("/etc/audit/rules.d/audit.rules", "r") as file:
                lines = file.readlines()

            with open("/etc/audit/rules.d/audit.rules", "w") as file:
                for line in lines:
                    if "-a never,task" in line:
                        continue  # Skip the line containing '-a never,task'
                    file.write(line)
            # Testing if removing was done
            output = run_command_with_pipe(command1, command2)
            if "-a never,task" not in output.stdout.split(): 
                logging.debug("Successfully removed '-a never,task' rule.")
            else : 
                logging.error("Removing '-a never,task' rule from {file} failed failed") 
                sys.exit(1)
        else : 
            # Ensure a rule for monitoring the directory is applied
            check_audit_rule("/etc")
    except Exception as e:
        logging.critical(f"An error occurred during configuration of audit: {e}")
        sys.exit(1)


def test_who_data():
    """
    Test the configuration to ensure that the alert is raised.
    NOTE : To ensure that audit , you need to modify a file via the user, not just log in .

    """
    logging.info("Running function test_who_data")

    configure_audit()
    try:
        # Check if the user 'smith' exists
        user_check = subprocess.run(["id", "smith"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
        if user_check.returncode != 0:
            logging.debug("User 'smith' does not exist. Creating user.")
            run_command(["useradd", "smith"])
        else:
            logging.debug("User 'smith' already exists.")

        # Set the password for the user 'smith' to 'wazuh'
        try:
            run_command(["echo", "smith:wazuh", "|", "chpasswd"])
            logging.debug("Password for user 'smith' set to 'wazuh'.")
        except Exception as e:
            logging.warning("Failed to set password for user 'smith'. Ensure you have the required permissions.")
            sys.exit(1)

        # Attempt to log in as 'smith'
        login_test = subprocess.run(["su", "-", "smith", "-c", "whoami"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
        if login_test.returncode == 0 and login_test.stdout.strip() == "smith":
            logging.debug("Successfully logged in as 'smith'.")
        else:
            logging.warning("Failed to log in as 'smith' with the provided password. Exiting.")
            sys.exit(1)

        
        # Define the IP to add
        ip_address = "192.168.32."
        manage_ip_in_hosts_allow(ip_address)
        # Now check for the alert on the manager.

    except Exception as e:
        logging.critical(f"An error occurred during the test: {e}")
        sys.exit(1)


def manage_ip_in_hosts_allow(ip_address: str):
    """
    Ensures the given IP address is rewritten in /etc/hosts.allow.
    Any existing occurrences of the IP address are removed, and the IP is added anew.

    Args:
        ip_address (str): The IP address to add or update.
    
    #NOTE:  to be run on a manager !

    """
    file_path = "/etc/hosts.allow"
    
    # Read the file content
    try:
        with open(file_path, "r") as file:
            content = file.readlines()
    except FileNotFoundError:
        logging.warning(f"{file_path} does not exist. Creating it.")
        content = []

    # Filter out any existing occurrences of the IP address
    content = [line for line in content if ip_address not in line]
    
    # Add the IP address anew
    content.append(f"{ip_address}\n")
    logging.debug(f"IP address '{ip_address}' has been rewritten in {file_path}.")
    
    # Write the updated content back to the file
    with open(file_path, "w") as file:
        file.writelines(content)


## HERE Debug ##

import inspect

def get_hierarchy(cls, visited_classes=None, seen_methods=None, indent=0, include_attributes=False):
    """
    Recursively builds the hierarchy of the provided class,
    displaying only unique methods and class attributes (optional).
    """
    if visited_classes is None:
        visited_classes = set()
    if seen_methods is None:
        seen_methods = set()

    # Avoid re-processing the same class
    if cls in visited_classes:
        return ""
    visited_classes.add(cls)

    # Exclude unwanted classes
    excluded_classes = {"Logger"}
    if cls.__name__ in excluded_classes:
        return ""

    # Initialize the hierarchy string
    hierarchy = "  " * indent + f"{cls.__name__} (Level {indent}):\n"

    # Get class attributes (excluding methods)
    if include_attributes:
        class_attributes = {
            name: value for name, value in cls.__dict__.items()
            if not inspect.isfunction(value) and not name.startswith("__")
        }

        # List class attributes
        if class_attributes:
            hierarchy += "  " * (indent + 1) + "Attributes:\n"
            for attr, value in sorted(class_attributes.items()):
                hierarchy += "  " * (indent + 2) + f"- {attr}: {value}\n"

    # Get methods defined in the current class only
    current_methods = {
        name for name, obj in cls.__dict__.items()
        if inspect.isfunction(obj) and name not in seen_methods
    }

    # Print current methods and mark them as seen
    if current_methods:
        hierarchy += "  " * (indent + 1) + "Methods:\n"
        for method in sorted(current_methods):
            hierarchy += "  " * (indent + 2) + f"- {method}\n"
            seen_methods.add(method)

    # Recurse for parent classes
    for base in cls.__bases__:
        hierarchy += get_hierarchy(base, visited_classes, seen_methods, indent + 1, include_attributes)

    return hierarchy


def save_hierarchy_to_file(cls, output_file="methods.txt", include_attributes=False):
    """
    Generates the hierarchy of a class and saves it to a file.
    """
    hierarchy = get_hierarchy(cls, include_attributes=include_attributes)
    with open(output_file, "w") as file:
        file.write(hierarchy)
    print(f"Hierarchy saved to '{output_file}'")
