Skip to content

Commit

Permalink
Merge pull request wazuh#24878 from wazuh/feature/24616-aca-initial-s…
Browse files Browse the repository at this point in the history
…etup

Agent comms API initial setup
  • Loading branch information
fdalmaup authored Aug 5, 2024
2 parents a0a5a6a + f97fbe8 commit 9815fed
Show file tree
Hide file tree
Showing 22 changed files with 427 additions and 26 deletions.
8 changes: 7 additions & 1 deletion api/api/alogging.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,8 @@ def set_logging(log_filepath, log_level='INFO', foreground_mode=False) -> dict:
},
"loggers": {
"wazuh-api": {"handlers": hdls, "level": log_level, "propagate": False},
"start-stop-api": {"handlers": hdls, "level": 'INFO', "propagate": False}
"start-stop-api": {"handlers": hdls, "level": 'INFO', "propagate": False},
"wazuh-comms-api": {"handlers": hdls, "level": log_level, "propagate": False}
}
}

Expand All @@ -209,6 +210,11 @@ def set_logging(log_filepath, log_level='INFO', foreground_mode=False) -> dict:
log_config_dict['loggers']['uvicorn.error'] = {"handlers": hdls, "level": 'WARNING', "propagate": False}
log_config_dict['loggers']['uvicorn.access'] = {'level': 'WARNING'}

# Configure the gunicorn loggers. They will be created by the gunicorn process.
log_config_dict['loggers']['gunicorn'] = {"handlers": hdls, "level": log_level, "propagate": False}
log_config_dict['loggers']['gunicorn.error'] = {"handlers": hdls, "level": log_level, "propagate": False}
log_config_dict['loggers']['gunicorn.access'] = {'level': log_level}

return log_config_dict


Expand Down
4 changes: 3 additions & 1 deletion api/api/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
SECURITY_PATH = os.path.join(CONFIG_PATH, 'security')
SECURITY_CONFIG_PATH = os.path.join(SECURITY_PATH, 'security.yaml')
RELATIVE_SECURITY_PATH = os.path.relpath(SECURITY_PATH, common.WAZUH_PATH)
API_LOG_PATH = os.path.join(common.WAZUH_PATH, 'logs', 'api')
BASE_LOG_PATH = os.path.join(common.WAZUH_PATH, 'logs')
API_LOG_PATH = os.path.join(BASE_LOG_PATH, 'api')
COMMS_API_LOG_PATH = os.path.join(BASE_LOG_PATH, 'comms_api')
API_SSL_PATH = os.path.join(CONFIG_PATH, 'ssl')
INSTALLATION_UID_PATH = os.path.join(SECURITY_PATH, 'installation_uid')
INSTALLATION_UID_KEY = 'installation_uid'
Expand Down
22 changes: 3 additions & 19 deletions api/scripts/wazuh_apid.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,22 +49,6 @@ def spawn_authentication_pool():
signal.signal(signal.SIGINT, signal.SIG_IGN)


def assign_wazuh_ownership(filepath: str):
"""Create a file if it doesn't exist and assign ownership.
Parameters
----------
filepath : str
File to assign ownership.
"""
if not os.path.isfile(filepath):
f = open(filepath, "w")
f.close()
if os.stat(filepath).st_gid != common.wazuh_gid() or \
os.stat(filepath).st_uid != common.wazuh_uid():
os.chown(filepath, common.wazuh_uid(), common.wazuh_gid())


def configure_ssl(params):
"""Configure https files and permission, and set the uvicorn dictionary configuration keys.
Expand Down Expand Up @@ -106,8 +90,8 @@ def configure_ssl(params):
logger.warning(SSL_DEPRECATED_MESSAGE.format(ssl_protocol=config_ssl_protocol))

# Check and assign ownership to wazuh user for server.key and server.crt files
assign_wazuh_ownership(api_conf['https']['key'])
assign_wazuh_ownership(api_conf['https']['cert'])
utils.assign_wazuh_ownership(api_conf['https']['key'])
utils.assign_wazuh_ownership(api_conf['https']['cert'])

params['ssl_version'] = ssl.PROTOCOL_TLS_SERVER

Expand Down Expand Up @@ -386,7 +370,7 @@ def error(self, msg, *args, **kws):
# set permission on log files
for handler in uvicorn_params['log_config']['handlers'].values():
if 'filename' in handler:
assign_wazuh_ownership(handler['filename'])
utils.assign_wazuh_ownership(handler['filename'])
os.chmod(handler['filename'], 0o660)

# Configure and create the wazuh-api logger
Expand Down
15 changes: 15 additions & 0 deletions apis/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Python
*.pyc
*.egg-info
*.egg

# Tests
.env/configurations/tmp/*

# Framework
dist/
build/

# Projects
.idea
.code
31 changes: 31 additions & 0 deletions apis/comms_api/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Makefile for Wazuh APIs
# Copyright (C) 2015, Wazuh Inc.
# May 3, 2017
#
# Syntax: make [ all | install | service ]

WAZUH_GROUP = wazuh
INSTALLDIR ?= /var/ossec

MV_FILE = mv -f
INSTALL_DIR = install -o root -g ${WAZUH_GROUP} -m 0750 -d
INSTALL_EXEC = install -o root -g ${WAZUH_GROUP} -m 0750
INSTALL_FILE = install -o root -g ${WAZUH_GROUP} -m 0640


.PHONY: all install

all: install

install:
# Copy files and create folders
$(INSTALL_DIR) $(INSTALLDIR)

$(INSTALL_DIR) $(INSTALLDIR)/apis/scripts
$(INSTALL_FILE) scripts/wazuh_comms_apid.py ${INSTALLDIR}/apis/scripts

# Install scripts/%.py on $(INSTALLDIR)/bin/%
$(foreach script,$(wildcard scripts/*.py),$(INSTALL_EXEC) ../wrappers/generic_wrapper.sh $(patsubst scripts/%.py,$(INSTALLDIR)/bin/%,$(script));)

$(MV_FILE) $(INSTALLDIR)/bin/wazuh_comms_apid $(INSTALLDIR)/bin/wazuh-comms-apid

3 changes: 3 additions & 0 deletions apis/comms_api/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Agent communications API

The Agent communications API is an open source RESTful API that allows for interaction with the Wazuh agents.
Empty file added apis/comms_api/__init__.py
Empty file.
10 changes: 10 additions & 0 deletions apis/comms_api/routers/router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from fastapi import APIRouter, status
from fastapi.responses import Response


router = APIRouter(prefix='/api/v1')


@router.get("/")
async def home():
return Response(status_code=status.HTTP_200_OK)
212 changes: 212 additions & 0 deletions apis/comms_api/scripts/wazuh_comms_apid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import logging
import logging.config
import os
import signal
import ssl
from argparse import ArgumentParser, Namespace
from functools import partial
from sys import exit
from typing import Any, Callable, Dict

from fastapi import FastAPI
from gunicorn.app.base import BaseApplication

from api.alogging import set_logging
from api.configuration import generate_private_key, generate_self_signed_certificate
from api.constants import COMMS_API_LOG_PATH
from routers import router
from wazuh.core import common, pyDaemonModule, utils
from wazuh.core.exception import WazuhCommsAPIError

MAIN_PROCESS = 'wazuh-comms-apid'

app = FastAPI()
app.include_router(router.router)


def setup_logging(foreground_mode: bool) -> dict:
"""Sets up the logging module and returns the configuration used.
Parameters
----------
foreground_mode : bool
Whether to execute the script in foreground mode or not.
Returns
-------
dict
Logging configuration dictionary.
"""
log_config_dict = set_logging(log_filepath=COMMS_API_LOG_PATH,
log_level='INFO',
foreground_mode=foreground_mode)

for handler in log_config_dict['handlers'].values():
if 'filename' in handler:
utils.assign_wazuh_ownership(handler['filename'])
os.chmod(handler['filename'], 0o660)

logging.config.dictConfig(log_config_dict)

return log_config_dict

def configure_ssl(keyfile: str, certfile: str) -> None:
"""Generate SSL key file and self-siged certificate if they do not exist.
Raises
------
ssl.SSLError
Invalid private key.
IOError
File permissions or path error.
"""
try:
if not os.path.exists(keyfile) or not os.path.exists(certfile):
private_key = generate_private_key(keyfile)
logger.info(f"Generated private key file in {certfile}")

generate_self_signed_certificate(private_key, certfile)
logger.info(f"Generated certificate file in {certfile}")
except ssl.SSLError as exc:
raise WazuhCommsAPIError(2700, extra_message=str(exc))
except IOError as exc:
if exc.errno == 22:
raise WazuhCommsAPIError(2701, extra_message=str(exc))
elif exc.errno == 13:
raise WazuhCommsAPIError(2702, extra_message=str(exc))
else:
raise WazuhCommsAPIError(2703, extra_message=str(exc))

def ssl_context(conf, default_ssl_context_factory) -> ssl.SSLContext:
"""Returns the default SSL context with a custom minimum version.
Returns
-------
ssl.SSLContext
Server SSL context.
"""
context = default_ssl_context_factory()
context.minimum_version = ssl.TLSVersion.MINIMUM_SUPPORTED
return context

def get_gunicorn_options(pid: int, foreground_mode: bool, log_config_dict: dict) -> dict:
"""Get the gunicorn app configuration options.
Parameters
----------
pid : int
Main process ID.
foreground_mode : bool
Whether to execute the script in foreground mode or not.
log_config_dict : dict
Logging configuration dictionary.
Returns
-------
dict
Gunicorn configuration options.
"""
# TODO: get values from the configuration
keyfile = '/var/ossec/api/configuration/ssl/server.key'
certfile = '/var/ossec/api/configuration/ssl/server.crt'
configure_ssl(keyfile, certfile)

pidfile = os.path.join(common.WAZUH_PATH, common.OS_PIDFILE_PATH, f'{MAIN_PROCESS}-{pid}.pid')

return {
'proc_name': MAIN_PROCESS,
'pidfile': pidfile,
'daemon': not foreground_mode,
'bind': f'{args.host}:{args.port}',
'workers': 4,
'worker_class': 'uvicorn.workers.UvicornWorker',
'preload_app': True,
'keyfile': keyfile,
'certfile': certfile,
'ca_certs': '/etc/ssl/certs/ca-certificates.crt',
'ssl_context': ssl_context,
'ciphers': '',
'logconfig_dict': log_config_dict,
'user': os.getuid()
}

def get_script_arguments() -> Namespace:
"""Get script arguments.
Returns
-------
argparse.Namespace
Arguments passed to the script.
"""
parser = ArgumentParser()
parser.add_argument('--host', type=str, default='0.0.0.0', help='API host.')
parser.add_argument('-p', '--port', type=int, default=27000, help='API port.')
parser.add_argument('-f', action='store_true', dest='foreground', help='Run API in foreground mode.')
parser.add_argument('-r', action='store_true', dest='root', help='Run as root')
parser.add_argument('-t', action='store_true', dest='test_config', help='Test configuration')

return parser.parse_args()


class StandaloneApplication(BaseApplication):
def __init__(self, app: Callable, options: Dict[str, Any] = None):
self.options = options or {}
self.app = app
super().__init__()

def load_config(self):
config = {
key: value
for key, value in self.options.items()
if key in self.cfg.settings and value is not None
}
for key, value in config.items():
self.cfg.set(key.lower(), value)

def load(self):
return self.app


if __name__ == '__main__':
args = get_script_arguments()

# The bash script that starts all services first executes them using the `-t` flag to check the configuration.
# We don't have a configuration yet, but it will be added in the future, so we just exit successfully for now.
#
# TODO: check configuration
if args.test_config:
exit(0)

utils.clean_pid_files(MAIN_PROCESS)

log_config_dict = setup_logging(args.foreground)
logger = logging.getLogger('wazuh-comms-api')

if args.foreground:
logger.info('Starting API in foreground')
else:
pyDaemonModule.pyDaemon()

if not args.root:
# Drop privileges to wazuh
os.setgid(common.wazuh_gid())
os.setuid(common.wazuh_uid())
else:
logger.info('Starting API as root')

pid = os.getpid()
signal.signal(signal.SIGTERM, partial(pyDaemonModule.exit_handler, process_name=MAIN_PROCESS, logger=logger))
signal.signal(signal.SIGINT, signal.SIG_IGN)

try:
options = get_gunicorn_options(pid, args.foreground, log_config_dict)
StandaloneApplication(app, options).run()
except WazuhCommsAPIError as e:
logger.error(f'Error when trying to start the Wazuh Agent comms API. {e}')
exit(1)
except Exception as e:
logger.error(f'Internal error when trying to start the Wazuh Agent comms API. {e}')
exit(1)
finally:
pyDaemonModule.delete_child_pids(MAIN_PROCESS, pid, logger)
pyDaemonModule.delete_pid(MAIN_PROCESS, pid)
33 changes: 33 additions & 0 deletions apis/comms_api/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/usr/bin/env python

# Copyright (C) 2015, Wazuh Inc.
# Created by Wazuh, Inc. <info@wazuh.com>.
# This program is a free software; you can redistribute it and/or modify it under the terms of GPLv2

from setuptools import setup, find_namespace_packages

# To install the library, run the following
#
# python setup.py install
#
# prerequisite: setuptools
# http://pypi.python.org/pypi/setuptools

setup(
name='comms_api',
version='5.0.0',
description='Agent communications API',
author_email='hello@wazuh.com',
author='Wazuh',
url='https://github.com/wazuh',
keywords=['Agent communications API', 'Agent comms API'],
install_requires=[],
packages=find_namespace_packages(exclude=['*.test', '*.test.*', 'test.*', 'test']),
package_data={},
include_package_data=True,
zip_safe=False,
license='GPLv2',
long_description="""
The Agent communications API is an open source RESTful API that allows for interaction with the Wazuh agents.
"""
)
Loading

0 comments on commit 9815fed

Please sign in to comment.