async def send_api_request(self): result = (await self.protocol.send_request(self.command, self.data)).decode() if result.startswith('Error'): raise exception.WazuhException(3009, result) elif result.startswith('WazuhException'): _, code, message = result.split(' ', 2) raise exception.WazuhException(int(code), message) elif result == 'There are no connected worker nodes': request_result = '{}' else: if self.command == b'dapi' or self.command == b'dapi_forward' or self.command == b'send_file' or \ result == 'Sent request to master node': try: timeout = None if self.wait_for_complete \ else self.cluster_items['intervals']['communication']['timeout_api_request'] await asyncio.wait_for( self.protocol.response_available.wait(), timeout=timeout) request_result = self.protocol.response.decode() except asyncio.TimeoutError: raise exception.WazuhException(3020) else: request_result = result return request_result
async def start(self): """ Connects to the server """ # Get a reference to the event loop as we plan to use # low-level APIs. asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) loop = asyncio.get_running_loop() on_con_lost = loop.create_future() try: self.transport, self.protocol = await loop.create_unix_connection( protocol_factory=lambda: LocalClientHandler( loop=loop, on_con_lost=on_con_lost, name=self.name, logger=self.logger, fernet_key='', manager=self, cluster_items=self.cluster_items), path='{}/queue/cluster/c-internal.sock'.format( common.ossec_path)) except ConnectionRefusedError: raise exception.WazuhException(3012) except MemoryError: raise exception.WazuhException(1119) except Exception as e: raise exception.WazuhException(3009, str(e))
def get_api_response(self, in_command, future): result = future.result() if result.startswith(b'Error'): result = json.dumps( exception.WazuhException(3000, result.decode()).to_dict()).encode() elif result.startswith(b'WazuhException'): _, code, message = result.decode().split(' ', 2) result = json.dumps( exception.WazuhException(int(code), message).to_dict()).encode() asyncio.create_task( self.send_request(command=b'dapi_res' if in_command == b'dapi' else b'control_res', data=result))
def process_request(self, command: bytes, data: bytes): """ Defines requests available in the local server :param command: Received command :param data: Received payload :return: A response """ if command == b'dapi': self.server.dapi.add_request(self.name.encode() + b' ' + data) return b'ok', b'Added request to API requests queue' elif command == b'dapi_forward': node_name, request = data.split(b' ', 1) node_name = node_name.decode() if node_name == 'fw_all_nodes': if len(self.server.node.clients) > 0: for node_name, node in self.server.node.clients.items(): asyncio.create_task( node.send_request( b'dapi', self.name.encode() + b' ' + request)) return b'ok', b'Request forwarded to all worker nodes' else: return b'ok', b'There are no connected worker nodes' elif node_name in self.server.node.clients: asyncio.create_task( self.server.node.clients[node_name].send_request( b'dapi', self.name.encode() + b' ' + request)) return b'ok', b'Request forwarded to worker node' else: raise exception.WazuhException(3022, node_name) else: return super().process_request(command, data)
def get_api_response(self, in_command, future): """ Forwards response sent by the master to the local client. Callback of the send_request_to_master method. :param in_command: command originally sent to the master :param future: Request response :return: Nothing """ result = future.result() if result.startswith(b'Error'): result = json.dumps( exception.WazuhException(3000, result.decode()).to_dict()).encode() elif result.startswith(b'WazuhException'): _, code, message = result.decode().split(' ', 2) result = json.dumps( exception.WazuhException(int(code), message).to_dict()).encode() asyncio.create_task( self.send_request(command=b'dapi_res' if in_command == b'dapi' else b'control_res', data=result))
async def distribute_function(self) -> str: """ Distributes an API call :return: Dictionary with API response """ try: request_type = rq.functions[self.input_json['function']]['type'] is_dapi_enabled = self.cluster_items['distributed_api']['enabled'] is_cluster_disabled = self.node == local_client and cluster.check_cluster_status( ) if 'wait_for_complete' not in self.input_json['arguments']: self.input_json['arguments']['wait_for_complete'] = False # if it is a cluster API request and the cluster is not enabled, raise an exception if is_cluster_disabled and 'cluster' in self.input_json['function'] and \ self.input_json['function'] != '/cluster/status' and \ self.input_json['function'] != '/cluster/config' and \ self.input_json['function'] != '/cluster/node': raise exception.WazuhException(3013) # First case: execute the request local. # If the distributed api is not enabled # If the cluster is disabled or the request type is local_any # if the request was made in the master node and the request type is local_master # if the request came forwarded from the master node and its type is distributed_master if not is_dapi_enabled or is_cluster_disabled or request_type == 'local_any' or \ (request_type == 'local_master' and self.node_info['type'] == 'master') or \ (request_type == 'distributed_master' and self.input_json['from_cluster']): return await self.execute_local_request() # Second case: forward the request # Only the master node will forward a request, and it will only be forwarded if its type is distributed_ # master elif request_type == 'distributed_master' and self.node_info[ 'type'] == 'master': return await self.forward_request() # Last case: execute the request remotely. # A request will only be executed remotely if it was made in a worker node and its type isn't local_any else: return await self.execute_remote_request() except exception.WazuhException as e: if self.debug: raise return self.print_json(data=e.message, error=e.code) except Exception as e: if self.debug: raise return self.print_json(data=str(e), error=1000)
async def execute_local_request(self) -> str: """ Executes an API request locally. :return: a JSON response. """ def run_local(args): self.logger.debug("Starting to execute request locally") data = rq.functions[self.input_json['function']]['function']( **args) self.logger.debug("Finished executing request locally") return data try: before = time.time() self.check_wazuh_status(basic_services=rq.functions[ self.input_json['function']].get('basic_services', None)) timeout = None if self.input_json['arguments']['wait_for_complete'] \ else self.cluster_items['intervals']['communication']['timeout_api_exe'] local_args = copy.deepcopy(self.input_json['arguments']) del local_args[ 'wait_for_complete'] # local requests don't use this parameter if rq.functions[self.input_json['function']]['is_async']: task = run_local(local_args) else: loop = asyncio.get_running_loop() task = loop.run_in_executor( None, functools.partial(run_local, local_args)) try: data = await asyncio.wait_for(task, timeout=timeout) except asyncio.TimeoutError: raise exception.WazuhException(3021) after = time.time() self.logger.debug( "Time calculating request result: {}s".format(after - before)) return self.print_json(data=data, error=0) except exception.WazuhException as e: if self.debug: raise return self.print_json(data=e.message, error=e.code) except Exception as e: self.logger.error( "Error executing API request locally: {}".format(e)) if self.debug: raise return self.print_json(data=str(e), error=1000)
def check_wazuh_status(self): """ There are some services that are required for wazuh to correctly process API requests. If any of those services is not running, the API must raise an exception indicating that: * It's not ready yet to process requests if services are restarting * There's an error in any of those services that must be adressed before using the API if any service is in failed status. * Wazuh must be started before using the API is the services are stopped. The basic services wazuh needs to be running are: wazuh-modulesd, ossec-remoted, ossec-analysisd, ossec-execd and wazuh-db """ if self.input_json['function'] == '/manager/status' or self.input_json['function'] == '/cluster/:node_id/status': return basic_services = ('wazuh-modulesd', 'ossec-remoted', 'ossec-analysisd', 'ossec-execd', 'wazuh-db') status = operator.itemgetter(*basic_services)(manager.status()) for status_name, exc_code in [('failed', 1019), ('restarting', 1017), ('stopped', 1018)]: if status_name in status: raise exception.WazuhException(exc_code, status)
def check_wazuh_status(self, basic_services=None): """ There are some services that are required for wazuh to correctly process API requests. If any of those services is not running, the API must raise an exception indicating that: * It's not ready yet to process requests if services are restarting * There's an error in any of those services that must be adressed before using the API if any service is in failed status. * Wazuh must be started before using the API is the services are stopped. The basic services wazuh needs to be running are: wazuh-modulesd, ossec-remoted, ossec-analysisd, ossec-execd and wazuh-db """ if self.input_json['function'] == '/manager/status' or self.input_json[ 'function'] == '/cluster/:node_id/status': return if not basic_services: basic_services = ('wazuh-modulesd', 'ossec-analysisd', 'ossec-execd', 'wazuh-db') if common.install_type != "local": basic_services += ('ossec-remoted', ) status = manager.status() not_ready_daemons = { k: status[k] for k in basic_services if status[k] in ('failed', 'restarting', 'stopped') } if not_ready_daemons: extra_info = { 'node_name': self.node_info.get('node', 'UNKNOWN NODE'), 'not_ready_daemons': ', '.join([ f'{key}->{value}' for key, value in not_ready_daemons.items() ]) } raise exception.WazuhException(1017, extra_message=extra_info)
def process_request(self, command: bytes, data: bytes): if command == b'dapi': self.server.dapi.add_request(self.name.encode() + b' ' + data) return b'ok', b'Added request to API requests queue' elif command == b'dapi_forward': node_name, request = data.split(b' ', 1) node_name = node_name.decode() if node_name == 'fw_all_nodes': for node_name, node in self.server.node.clients.items(): asyncio.create_task( node.send_request(b'dapi', self.name.encode() + b' ' + request)) return b'ok', b'Request forwarded to all worker nodes' elif node_name in self.server.node.clients: asyncio.create_task( self.server.node.clients[node_name].send_request( b'dapi', self.name.encode() + b' ' + request)) return b'ok', b'Request forwarded to worker node' else: raise exception.WazuhException(3022, node_name) else: return super().process_request(command, data)
def get_connected_nodes(self, filter_node=None, offset=0, limit=common.database_limit, sort=None, search=None, select=None, filter_type='all') -> Dict: """ Return all connected nodes, including the master node :return: A dictionary containing data from each node """ def return_node(node_info): return (filter_node is None or node_info['name'] in filter_node) and (filter_type == 'all' or node_info['type'] == filter_type) default_fields = self.to_dict()['info'].keys() if select is None: select = {'fields': default_fields} else: if not set(select['fields']).issubset(default_fields): raise exception.WazuhException( 1724, "Allowed fields: {}. Fields: {}".format( ', '.join(default_fields), ', '.join(set(select['fields']) - default_fields))) if filter_type != 'all' and filter_type not in {'worker', 'master'}: raise exception.WazuhException( 1728, "Valid types are 'worker' and 'master'.") if filter_node is not None: filter_node = set(filter_node) if isinstance( filter_node, list) else {filter_node} if not filter_node.issubset( set( itertools.chain(self.clients.keys(), [self.configuration['node_name']]))): raise exception.WazuhException(1730) res = [ val.to_dict()['info'] for val in itertools.chain([self], self.clients.values()) if return_node(val.to_dict()['info']) ] if sort is not None: res = utils.sort_array(array=res, sort_by=sort['fields'], order=sort['order'], allowed_sort_fields=default_fields) if search is not None: res = utils.search_array(array=res, text=search['value'], negation=search['negation']) return { 'totalItems': len(res), 'items': utils.cut_array([{k: v[k] for k in select['fields']} for v in res], offset, limit) }