Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding Central component tests #5289

Closed
2 changes: 1 addition & 1 deletion deployability/modules/testing/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def parse_arguments():
parser = argparse.ArgumentParser(description="Wazuh testing tool")
parser.add_argument("--targets", action='append', default=[], required=True)
parser.add_argument("--tests", required=True)
parser.add_argument("--component", choices=['manager', 'agent'], required=True)
parser.add_argument("--component", choices=['manager', 'agent', 'central_components'], required=True)
parser.add_argument("--dependencies", action='append', default=[], required=False)
parser.add_argument("--cleanup", required=False, default=True)
parser.add_argument("--wazuh-version", required=True)
Expand Down
2 changes: 1 addition & 1 deletion deployability/modules/testing/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

class ExtraVars(BaseModel):
"""Extra vars for testing module."""
component: Literal['manager', 'agent']
component: Literal['manager', 'agent', 'central_components']
wazuh_version: str
wazuh_revision: str
wazuh_branch: str | None = None
Expand Down
177 changes: 177 additions & 0 deletions deployability/modules/testing/tests/helpers/central.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
# Copyright (C) 2015, Wazuh Inc.
# Created by Wazuh, Inc. <[email protected]>.
# This program is a free software; you can redistribute it and/or modify it under the terms of GPLv2

import requests
import socket

from .constants import CLUSTER_CONTROL, AGENT_CONTROL, WAZUH_CONF, WAZUH_ROOT
from .executor import Executor, WazuhAPI
from .generic import HostInformation, CheckFiles
from modules.testing.utils import logger
from .utils import Utils


class WazuhCentralComponents:

@staticmethod
def install_aio(inventory_path, wazuh_version) -> None:
"""
Installs Wazuh Central Components AIO in the host
rauldpm marked this conversation as resolved.
Show resolved Hide resolved

Args:
inventory_path (str): host's inventory path
wazuh_version (str): major.minor.patch

"""
wazuh_version = '.'.join(wazuh_version.split('.')[:2])
os_name = HostInformation.get_os_name_from_inventory(inventory_path)

if 'debian' in os_name:
commands = [
f"wget https://packages.wazuh.com/{wazuh_version}/wazuh-install.sh && sudo bash ./wazuh-install.sh -a --ignore-check"
]

else:
commands = [
f"curl -sO https://packages.wazuh.com/{wazuh_version}/wazuh-install.sh && sudo bash ./wazuh-install.sh -a --ignore-check"
]
rauldpm marked this conversation as resolved.
Show resolved Hide resolved

logger.info(f'Installing Wazuh AIO in {HostInformation.get_os_name_and_version_from_inventory(inventory_path)}')
Executor.execute_commands(inventory_path, commands)
rauldpm marked this conversation as resolved.
Show resolved Hide resolved


@staticmethod
def uninstall_aio(inventory_path) -> None:
"""
Uninstall Wazuh Central Components AIO in the host
rauldpm marked this conversation as resolved.
Show resolved Hide resolved

Args:
inventory_paths (str): hosts' inventory path
"""

commands = ['bash wazuh-install.sh --uninstall --ignore-check']

logger.info(f'Uninstalling Wazuh AIO in {HostInformation.get_os_name_and_version_from_inventory(inventory_path)}')
rauldpm marked this conversation as resolved.
Show resolved Hide resolved
Executor.execute_commands(inventory_path, commands)


@staticmethod
def _install_aio_callback(wazuh_params, host_params):
WazuhCentralComponents.install_aio(host_params, wazuh_params['wazuh_version'])


@staticmethod
def _uninstall_aio_callback(host_params):
WazuhCentralComponents.uninstall_aio(host_params)


@staticmethod
def perform_action_and_scan(host_params, action_callback) -> dict:
"""
Takes scans using filters, the callback action and compares the result

Args:
host_params (str): host parameters
callback (cb): callback (action)

Returns:
result (dict): comparison brief

"""
result = CheckFiles.perform_action_and_scan(host_params, action_callback)
os_name = HostInformation.get_os_name_from_inventory(host_params)
logger.info(f'Applying filters in checkfiles in {HostInformation.get_os_name_and_version_from_inventory(host_params)}')

if 'debian' in os_name:
filter_data = {
'/boot': {'added': [], 'removed': [], 'modified': ['grubenv']},
'/usr/bin': {
'added': [
'unattended-upgrade', 'gapplication', 'add-apt-repository', 'gpg-wks-server', 'pkexec', 'gpgsplit',
'watchgnupg', 'pinentry-curses', 'gpg-zip', 'gsettings', 'gpg-agent', 'gresource', 'gdbus',
'gpg-connect-agent', 'gpgconf', 'gpgparsemail', 'lspgpot', 'pkaction', 'pkttyagent', 'pkmon',
'dirmngr', 'kbxutil', 'migrate-pubring-from-classic-gpg', 'gpgcompose', 'pkcheck', 'gpgsm', 'gio',
'pkcon', 'gpgtar', 'dirmngr-client', 'gpg', 'filebeat', 'gawk', 'curl', 'update-mime-database',
'dh_installxmlcatalogs', 'appstreamcli', 'lspgpot', 'symcryptrun'
],
'removed': ['filebeat'],
'modified': []
},
'/root': {'added': ['trustdb.gpg', 'lesshst', 'ssh'], 'removed': ['filebeat'], 'modified': []},
'/usr/sbin': {
'added': [
'update-catalog', 'applygnupgdefaults', 'addgnupghome', 'install-sgmlcatalog', 'update-xmlcatalog'
],
'removed': [],
'modified': []
}
}
else:
filter_data = {
'/boot': {
'added': ['grub2', 'loader', 'vmlinuz', 'System.map', 'config-', 'initramfs'],
'removed': [],
'modified': ['grubenv']
},
'/usr/bin': {'added': ['filebeat'], 'removed': ['filebeat'], 'modified': []},
'/root': {'added': ['trustdb.gpg', 'lesshst'], 'removed': [], 'modified': ['.rnd']},
'/usr/sbin': {'added': [], 'removed': [], 'modified': []}
}

# Use of filters
for directory, changes in result.items():
if directory in filter_data:
for change, files in changes.items():
if change in filter_data[directory]:
result[directory][change] = [file for file in files if file.split('/')[-1] not in filter_data[directory][change]]

return result

@staticmethod
def perform_install_and_scan_for_aio(host_params, wazuh_params) -> None:
"""
Coordinates the action of install the Wazuh AIO and compares the checkfiles
rauldpm marked this conversation as resolved.
Show resolved Hide resolved

Args:
host_params (str): host parameters
wazuh_params (str): wazuh parameters

"""
action_callback = lambda: WazuhCentralComponents._install_aio_callback(wazuh_params, host_params)
result = WazuhCentralComponents.perform_action_and_scan(host_params, action_callback)
logger.info(f'Pre and post install checkfile comparison in {HostInformation.get_os_name_and_version_from_inventory(host_params)}: {result}')
WazuhCentralComponents.assert_results(result)


@staticmethod
def perform_uninstall_and_scan_for_aio(host_params) -> None:
"""
Coordinates the action of uninstall the Wazuh AIO and compares the checkfiles
rauldpm marked this conversation as resolved.
Show resolved Hide resolved

Args:
host_params (str): host parameters
wazuh_params (str): wazuh parameters

"""
action_callback = lambda: WazuhCentralComponents._uninstall_aio_callback(host_params)
result = WazuhCentralComponents.perform_action_and_scan(host_params, action_callback)
logger.info(f'Pre and post uninstall checkfile comparison in {HostInformation.get_os_name_and_version_from_inventory(host_params)}: {result}')
WazuhCentralComponents.assert_results(result)


@staticmethod
def assert_results(result) -> None:
"""
Gets the status of an agent given its name.

Args:
result (dict): result of comparison between pre and post action scan

"""
categories = ['/root', '/usr/bin', '/usr/sbin', '/boot']
actions = ['added', 'modified', 'removed']
# Testing the results
for category in categories:
for action in actions:
assert result[category][action] == [], logger.error(f'{result[category][action]} was found in: {category} {action}')
109 changes: 109 additions & 0 deletions deployability/modules/testing/tests/helpers/dashboard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# Copyright (C) 2015, Wazuh Inc.
# Created by Wazuh, Inc. <[email protected]>.
# This program is a free software; you can redistribute it and/or modify it under the terms of GPLv2

import requests
import socket
import json
import time

from .constants import CLUSTER_CONTROL, AGENT_CONTROL, WAZUH_CONF, WAZUH_ROOT
from .executor import Executor, WazuhAPI
from .generic import HostInformation, CheckFiles
from modules.testing.utils import logger
from .utils import Utils


class WazuhDashboard:

@staticmethod
def get_dashboard_version(inventory_path) -> str:
"""
Returns dashboard version
rauldpm marked this conversation as resolved.
Show resolved Hide resolved

Args:
inventory_path (str): host's inventory path

Returns:
- str: Version of the dashboard.
rauldpm marked this conversation as resolved.
Show resolved Hide resolved
"""

return Executor.execute_command(inventory_path,'cat /usr/share/wazuh-dashboard/VERSION').strip()


@staticmethod
def isDashboard_active(inventory_path) -> bool:
rauldpm marked this conversation as resolved.
Show resolved Hide resolved
"""
Returns True/False depending if the dashboard is active or not
rauldpm marked this conversation as resolved.
Show resolved Hide resolved

Args:
inventory_path (str): host's inventory path

Returns:
- bool: Status of the dashboard.
rauldpm marked this conversation as resolved.
Show resolved Hide resolved
"""

return '200' in Executor.execute_command(inventory_path, 'curl -Is -k https://localhost/app/login?nextUrl=%2F | head -n 1')


@staticmethod
def isDashboardKeystore_working(inventory_path) -> bool:
rauldpm marked this conversation as resolved.
Show resolved Hide resolved
"""
Returns True/False depending if the dashboard keystore is active or not
rauldpm marked this conversation as resolved.
Show resolved Hide resolved

Args:
inventory_path (str): host's inventory path

Returns:
- bool: Status of the dashboard keystore.
rauldpm marked this conversation as resolved.
Show resolved Hide resolved
"""

return 'No such file or directory' not in Executor.execute_command(inventory_path, '/usr/share/wazuh-dashboard/bin/opensearch-dashboards-keystore list --allow-root')


@staticmethod
def areDashboardNodes_working(wazuh_api: WazuhAPI) -> str:
rauldpm marked this conversation as resolved.
Show resolved Hide resolved
"""
Returns True/False depending the status of Dashboard nodes
rauldpm marked this conversation as resolved.
Show resolved Hide resolved

Returns:
- bool: True/False depending on the status.
"""
response = requests.get(f"{wazuh_api.api_url}/api/status", auth=(wazuh_api.username, wazuh_api.password), verify=False)

result = True
if response.status_code == 200:
for status in json.loads((response.text))['status']['statuses']:
if status['state'] == 'green' or status['state'] == 'yellow':
result = True
else:
result = False
return result

else:
logger.error(f'The dashboard API returned: {response.status_code}')
rauldpm marked this conversation as resolved.
Show resolved Hide resolved

@staticmethod
def isDashboard_port_opened(inventory_path, wait=10, cycles=50):
rauldpm marked this conversation as resolved.
Show resolved Hide resolved
"""
Check if dashboard port is open
rauldpm marked this conversation as resolved.
Show resolved Hide resolved

Args:
inventory_path (str): Dashboard inventory.
rauldpm marked this conversation as resolved.
Show resolved Hide resolved

Returns:
str: Os name.
rauldpm marked this conversation as resolved.
Show resolved Hide resolved
"""
wait_cycles = 0
while wait_cycles < cycles:
ports = Executor.execute_command(inventory_path, 'ss -t -a -n | grep ":443"').strip().split('\n')
for port in ports:
if 'ESTAB' in port or 'LISTEN' in port:
continue
else:
time.sleep(wait)
wait_cycles += 1
break
else:
return True
return False
30 changes: 20 additions & 10 deletions deployability/modules/testing/tests/helpers/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,26 +50,29 @@ def execute_commands(inventory_path, commands=[]) -> dict:


class WazuhAPI:
def __init__(self, inventory_path):
def __init__(self, inventory_path, component=None):
self.inventory_path = inventory_path
self.api_url = None
self.headers = None
self.component = component
self.username = None
self.password = None
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
self._authenticate()

def _extract_password(self, file_path, keyword):
if not 'true' in Executor.execute_command(self.inventory_path, f'test -f {file_path} && echo "true" || echo "false"'):
Executor.execute_command(self.inventory_path, 'tar -xvf wazuh-install-files.tar')
return Executor.execute_command(self.inventory_path, f"grep {keyword} {file_path} | head -n 1 | awk '{{print $NF}}'").replace("'", "").replace("\n", "")
rauldpm marked this conversation as resolved.
Show resolved Hide resolved

def _authenticate(self):
with open(self.inventory_path, 'r') as yaml_file:
inventory_data = yaml.safe_load(yaml_file)

user = 'wazuh'

#----Patch issue https://github.com/wazuh/wazuh-packages/issues/2883-------------
file_path = Executor.execute_command(self.inventory_path, 'pwd').replace("\n","") + '/wazuh-install-files/wazuh-passwords.txt'
if not 'true' in Executor.execute_command(self.inventory_path, f'test -f {file_path} && echo "true" || echo "false"'):
Executor.execute_command(self.inventory_path, 'tar -xvf wazuh-install-files.tar')
password = Executor.execute_command(self.inventory_path, "grep api_password wazuh-install-files/wazuh-passwords.txt | head -n 1 | awk '{print $NF}'").replace("'","").replace("\n","")
#--------------------------------------------------------------------------------

file_path = Executor.execute_command(self.inventory_path, 'pwd').replace("\n", "") + '/wazuh-install-files/wazuh-passwords.txt'
password = self._extract_password(file_path, 'api_password')

login_endpoint = 'security/user/authenticate'
host = inventory_data.get('ansible_host')
port = '55000'
Expand All @@ -80,8 +83,15 @@ def _authenticate(self):

token = json.loads(requests.post(login_url, headers=login_headers, verify=False).content.decode())['data']['token']

self.api_url = f'https://{host}:{port}'
self.headers = {
'Content-Type': 'application/json',
'Authorization': f'Bearer {token}'
}

self.api_url = f'https://{host}:{port}'

if self.component == 'dashboard' or self.component == 'indexer':
self.username = 'admin'
password = self._extract_password(file_path, 'indexer_password')
self.password = password
self.api_url = f'https://{host}' if self.component == 'dashboard' else f'https://127.0.0.1:9200'
Loading