def get_sca_checks(policy_id=None, agent_list=None, q="", offset=0, limit=common.database_limit, sort=None, search=None, select=None, filters=None): """ Get a list of checks analyzed for a policy :param policy_id: policy id to get the checks from :param agent_list: agent id to get the policies from :param q: Defines query to filter in DB. :param offset: First item to return. :param limit: Maximum number of items to return. :param sort: Sorts the items. Format: {"fields":["field1","field2"],"order":"asc|desc"}. :param search: Looks for items with the specified string. Format: {"fields": ["field1","field2"]} :param select: Select fields to return. Format: {"fields":["field1","field2"]}. :param filters: Define field filters required by the user. Format: {"field1":"value1", "field2":["value2","value3"]} :return: AffectedItemsWazuhResult """ result = AffectedItemsWazuhResult( all_msg='All selected sca/policy information was returned', some_msg='Some sca/policy information was not returned', none_msg='No sca/policy information was returned') if len(agent_list) != 0: if agent_list[0] in get_agents_info(): fields_translation = { **fields_translation_sca_check, **fields_translation_sca_check_compliance, **fields_translation_sca_check_rule } full_select = ( list(fields_translation_sca_check.keys()) + list(fields_translation_sca_check_compliance.keys()) + list(fields_translation_sca_check_rule.keys())) db_query = WazuhDBQuerySCA(agent_id=agent_list[0], offset=offset, limit=limit, sort=sort, search=search, select=full_select, count=True, get_data=True, query=f"policy_id={policy_id}" if q == "" else f"policy_id={policy_id};{q}", filters=filters, default_query=default_query_sca_check, default_sort_field='policy_id', fields=fields_translation, count_field='id') result_dict = db_query.run() if 'items' in result_dict: checks = result_dict['items'] else: raise WazuhInternalError(2007) groups = groupby(checks, key=itemgetter('id')) select_fields = full_select if select is None else select select_fields = set([ field if field != 'compliance' else 'compliance' for field in select_fields if field in fields_translation_sca_check ]) # Rearrange check and compliance fields for _, group in groups: group_list = list(group) check_dict = { k: v for k, v in group_list[0].items() if k in select_fields } for extra_field, field_translations in [ ('compliance', fields_translation_sca_check_compliance), ('rules', fields_translation_sca_check_rule) ]: if (select is None or extra_field in select) \ and set(field_translations.keys()) & group_list[0].keys(): check_dict[extra_field] = [ dict(zip(field_translations.values(), x)) for x in set(( map(itemgetter( *field_translations.keys()), group_list))) ] result.affected_items.append(check_dict) result.total_affected_items = result_dict['totalItems'] else: result.add_failed_item(id_=agent_list[0], error=WazuhResourceNotFound(1701)) result.total_affected_items = 0 return result
(WazuhPermissionError, 4000, ['remediation', 'code'], 403, ProblemException), (WazuhResourceNotFound, 1710, ['remediation', 'code'], 404, ProblemException), (WazuhInternalError, 1000, ['remediation', 'code'], 500, ProblemException) ]) def test_create_problem(exception_type, code, extra_fields, returned_code, returned_exception): """Check that _create_problem returns exception with expected data""" with pytest.raises(returned_exception) as exc_info: util._create_problem(exception_type(code)) if returned_exception == ProblemException: assert exc_info.value.status == returned_code if extra_fields: assert all(x in exc_info.value.ext.keys() for x in extra_fields) assert None not in exc_info.value.ext.values() @pytest.mark.parametrize('obj, code', [ ((WazuhError(6001), ['value0', 'value1']), 429), ((WazuhInternalError(1000), ['value0', 'value1']), None), ((WazuhPermissionError(4000), ['value0', 'value1']), None), ((WazuhResourceNotFound(1710), ['value0', 'value1']), None) ]) @patch('api.util._create_problem') def test_raise_if_exc(mock_create_problem, obj, code): """Check that raise_if_exc calls _create_problem when an exception is given""" result = util.raise_if_exc(obj) if isinstance(obj, Exception): mock_create_problem.assert_called_once_with(obj, code) else: assert result == obj
def remove_bulk_agents(agent_ids_list: KeysView, logger): """ Removes files created by agents in worker nodes. This function doesn't remove agents from client.keys since the client.keys file is overwritten by the master node. :param agent_ids_list: List of agents ids to remove. :param logger: Logger to use :return: None. """ def remove_agent_file_type(agent_files: List[str]): """ Removes files if they exist :param agent_files: Path regexes of the files to remove :return: None """ for filetype in agent_files: filetype_glob = filetype.format(ossec_path=common.ossec_path, id='*', name='*', ip='*') filetype_agent = { filetype.format(ossec_path=common.ossec_path, id=a['id'], name=a['name'], ip=a['ip']) for a in agent_info } for agent_file in set( glob.iglob(filetype_glob)) & filetype_agent: logger.debug2("Removing {}".format(agent_file)) if os.path.isdir(agent_file): shutil.rmtree(agent_file) else: os.remove(agent_file) if not agent_ids_list: return # the function doesn't make sense if there is no agents to remove logger.info("Removing files from {} agents".format( len(agent_ids_list))) logger.debug("Agents to remove: {}".format(', '.join(agent_ids_list))) # the agents must be removed in groups of 997: 999 is the limit of SQL variables per query. Limit and offset are # always included in the SQL query, so that leaves 997 variables as limit. for agents_ids_sublist in itertools.zip_longest(*itertools.repeat( iter(agent_ids_list), 997), fillvalue='0'): agents_ids_sublist = list( filter(lambda x: x != '0', agents_ids_sublist)) # Get info from DB agent_info = Agent.get_agents_overview(q=",".join( ["id={}".format(i) for i in agents_ids_sublist]), select=['ip', 'id', 'name'], limit=None)['items'] logger.debug2("Removing files from agents {}".format( ', '.join(agents_ids_sublist))) files_to_remove = [ '{ossec_path}/queue/agent-info/{name}-{ip}', '{ossec_path}/queue/rootcheck/({name}) {ip}->rootcheck', '{ossec_path}/queue/diff/{name}', '{ossec_path}/queue/agent-groups/{id}', '{ossec_path}/queue/rids/{id}', '{ossec_path}/var/db/agents/{name}-{id}.db' ] remove_agent_file_type(files_to_remove) logger.debug2("Removing agent group assigments from database") # remove agent from groups db_global = glob.glob(common.database_path_global) if not db_global: raise WazuhInternalError(1600) conn = Connection(db_global[0]) agent_ids_db = { 'id_agent{}'.format(i): int(i) for i in agents_ids_sublist } conn.execute( 'delete from belongs where {}'.format(' or '.join([ 'id_agent = :{}'.format(i) for i in agent_ids_db.keys() ])), agent_ids_db) conn.commit() # Tell wazuhbd to delete agent database wdb_conn = WazuhDBConnection() wdb_conn.delete_agents_db(agents_ids_sublist) logger.info("Agent files removed")
def last_scan(agent_list): """Get the last scan of an agent. Parameters ---------- agent_list : str Agent ID. Returns ------- result : AffectedItemsWazuhResult Confirmation/Error message. """ my_agent = Agent(agent_list[0]) result = AffectedItemsWazuhResult( all_msg='Last syscheck scan of the agent was returned', none_msg='No last scan information was returned') # If agent status is never_connected, a KeyError happens try: agent_version = my_agent.get_basic_information( select=['version'])['version'] except KeyError: # If the agent is never_connected, it won't have either version (key error) or last scan information. result.affected_items.append({'start': None, 'end': None}) result.total_affected_items += 1 return result if WazuhVersion(agent_version) < WazuhVersion('Wazuh v3.7.0'): db_agent = glob('{0}/{1}-*.db'.format(common.database_path_agents, agent_list[0])) if not db_agent: raise WazuhInternalError(1600, extra_message=agent_list[0]) else: db_agent = db_agent[0] conn = Connection(db_agent) data = {} # end time query = "SELECT max(date_last) FROM pm_event WHERE log = 'Ending rootcheck scan.'" conn.execute(query) for t in conn: data['end'] = t['max(date_last)'] if t[ 'max(date_last)'] is not None else "ND" # start time query = "SELECT max(date_last) FROM pm_event WHERE log = 'Starting rootcheck scan.'" conn.execute(query) for t in conn: data['start'] = t['max(date_last)'] if t[ 'max(date_last)'] is not None else "ND" result.affected_items.append(data) else: with WazuhDBQuerySyscheck(agent_id=agent_list[0], query='module=fim', offset=0, sort=None, search=None, limit=common.database_limit, select={'end', 'start'}, fields={ 'end': 'end_scan', 'start': 'start_scan', 'module': 'module' }, table='scan_info', default_sort_field='start_scan') as db_query: fim_scan_info = db_query.run()['items'][0] end = None if not fim_scan_info['end'] else fim_scan_info['end'] start = None if not fim_scan_info['start'] else fim_scan_info['start'] # If start is None or the scan is running, end will be None. result.affected_items.append({ 'start': start, 'end': None if start is None else None if end is None or end < start else end }) result.total_affected_items = len(result.affected_items) return result
def upload_group_configuration(group_id, file_content): """ Updates group configuration :param group_id: Group to update :param file_content: File content of the new configuration in a string. :return: Confirmation message. """ if not os_path.exists(os_path.join(common.shared_path, group_id)): raise WazuhResourceNotFound(1710, group_id) # path of temporary files for parsing xml input tmp_file_path = os_path.join( common.ossec_path, "tmp", f"api_tmp_file_{time.time()}_{random.randint(0, 1000)}.xml") # create temporary file for parsing xml input and validate XML format try: with open(tmp_file_path, 'w') as tmp_file: custom_entities = { '_custom_open_tag_': '\\<', '_custom_close_tag_': '\\>', '_custom_amp_lt_': '<', '_custom_amp_gt_': '>' } # Replace every custom entity for character, replacement in custom_entities.items(): file_content = re.sub(replacement.replace('\\', '\\\\'), character, file_content) # Beautify xml file using a minidom.Document xml = parseString(f'<root>\n{file_content}\n</root>') # Remove first line (XML specification: <? xmlversion="1.0" ?>), <root> and </root> tags, and empty lines pretty_xml = '\n'.join( filter(lambda x: x.strip(), xml.toprettyxml(indent=' ').split('\n')[2:-2])) + '\n' # Revert xml.dom replacements and remove any whitespaces and '\n' between '\' and '<' if present # github.com/python/cpython/blob/8e0418688906206fe59bd26344320c0fc026849e/Lib/xml/dom/minidom.py#L305 pretty_xml = re.sub( r'(?:(?<=\\) +)', '', pretty_xml.replace("&", "&").replace("<", "<").replace( """, "\"", ).replace(">", ">").replace("\\\n", "\\")) # Restore the replaced custom entities for replacement, character in custom_entities.items(): pretty_xml = re.sub(replacement, character.replace('\\', '\\\\'), pretty_xml) tmp_file.write(pretty_xml) except Exception as e: raise WazuhError(1113, str(e)) try: # check Wazuh xml format try: subprocess.check_output([ os_path.join(common.ossec_path, "bin", "verify-agent-conf"), '-f', tmp_file_path ], stderr=subprocess.STDOUT) except subprocess.CalledProcessError as e: # extract error message from output. # Example of raw output # 2019/01/08 14:51:09 verify-agent-conf: ERROR: (1230): # Invalid element in the configuration: 'agent_conf'.\n2019/01/08 14:51:09 verify-agent-conf: ERROR: (1207): # Syscheck remote configuration in '/var/ossec/tmp/api_tmp_file_2019-01-08-01-1546959069.xml' is corrupted. # \n\n # Example of desired output: # Invalid element in the configuration: 'agent_conf'. # Syscheck remote configuration in '/var/ossec/tmp/api_tmp_file_2019-01-08-01-1546959069.xml' is corrupted. output_regex = re.findall( pattern= r"\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2} verify-agent-conf: ERROR: " r"\(\d+\): ([\w \/ \_ \- \. ' :]+)", string=e.output.decode()) if output_regex: raise WazuhError(1114, ' '.join(output_regex)) else: raise WazuhError(1115, e.output.decode()) except Exception as e: raise WazuhInternalError(1743, str(e)) # move temporary file to group folder try: new_conf_path = os_path.join(common.shared_path, group_id, "agent.conf") safe_move(tmp_file_path, new_conf_path, permissions=0o660) except Exception as e: raise WazuhInternalError(1016, extra_message=str(e)) return 'Agent configuration was successfully updated' except Exception as e: # remove created temporary file if os.path.exists(tmp_file_path): remove(tmp_file_path) raise e
relative_dirname=relative_dirname, parents=parents) assert isinstance(result, AffectedItemsWazuhResult) # Build result names set from response for filter validation result_names = {d['name'] for d in result.affected_items} assert result_names == expected_names # Assert failed items length matches expected result assert result.total_failed_items == expected_total_failed finally: os.rename(wrong_decoder_tmp_path, wrong_decoder_original_path) @pytest.mark.parametrize('conf, exception', [(decoder_ossec_conf, None), ({ 'ruleset': None }, WazuhInternalError(1500))]) def test_get_decoders_files(conf, exception): with patch('wazuh.core.configuration.get_ossec_conf', return_value=conf): try: # UUT call result = decoder.get_decoders_files() assert isinstance(result, AffectedItemsWazuhResult) # Assert result is a list with at least one dict element with the appropriate fields assert isinstance(result.affected_items, list) assert len(result.affected_items) != 0 for item in result.affected_items: assert {'filename', 'relative_dirname', 'status'}.issubset(set(item)) assert result.total_affected_items == len(result.affected_items) except WazuhInternalError as e: # If the UUT call returns an exception we check it has the appropriate error code
'mycallable': get_node}, {'foo': 'bar', 'foo2': 3, 'mycallable': get_agents_summary_status}, {'foo': 'bar', 'foo2': 3, 'mycallable': status}, {'foo': 'bar', 'foo2': 3, 'exception': WazuhError(1500, extra_message="test message", extra_remediation="test remediation")}, {'foo': 'bar', 'foo2': 3, 'exception': WazuhInternalError(1000, extra_message="test message", extra_remediation="test remediation")}, {'foo': 'bar', 'foo2': 3, 'result': WazuhResult({'field1': 'value1', 'field2': 3}, str_priority=['KO', 'OK'])}, {'foo': 'bar', 'foo2': 3, 'result': affected} ] @pytest.mark.parametrize('obj', objects_to_encode) @patch('wazuh.common.wazuh_path', new=os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data')) def test_encoder_decoder(obj): # Encoding first object encoded = json.dumps(obj, cls=WazuhJSONEncoder)
def test_check_status(status, expected_result): try: # UUT call result = decoder.check_status(status) assert result == expected_result except WazuhError as e: # If the UUT call returns an exception we check it has the appropriate error code assert e.code == expected_result.code @pytest.mark.parametrize( "filename, relative_dirname, status, permissions, exception", [ ('test1_decoders.xml', 'decoders', "all", 777, None), ('test2_decoders.xml', 'decoders', "enabled", 777, None), ('wrong_decoders.xml', 'decoders', "all", 777, WazuhInternalError(1501)), ('non_existing.xml', 'decoders', "disabled", 777, WazuhError(1502)), ('test1_decoders.xml', 'decoders', "all", 000, WazuhError(1502)), ]) @patch('wazuh.core.common.ossec_path', new=test_data_path) def test_load_decoders_from_file(filename, relative_dirname, status, permissions, exception): full_file_path = os.path.join(test_data_path, relative_dirname, filename) try: old_permissions = stat.S_IMODE(os.lstat(full_file_path).st_mode) except FileNotFoundError: old_permissions = None try: # Set file permissions if the file exists os.path.exists(full_file_path) and os.chmod(full_file_path, permissions)
def send_msg_to_agent(self, msg: str = '', agent_id: str = '', msg_type: str = '') -> str: """Send message to agent. Active-response Agents: /var/ossec/queue/alerts/ar - Existing command: - (msg_to_agent) [] NNS 001 restart-ossec0 arg1 arg2 arg3 - (msg_to_agent) [] ANN (null) restart-ossec0 arg1 arg2 arg3 - Custom command: - (msg_to_agent) [] NNS 001 !test.sh arg1 arg2 arg3 - (msg_to_agent) [] ANN (null) !test.sh arg1 arg2 arg3 Agents with version >= 4.2.0: - Existing and custom commands: - (msg_to_agent) [] NNS 001 {JSON message} Manager: /var/ossec/queue/alerts/execq - Existing or custom command: - {JSON message} Parameters ---------- msg : str Message to be sent to the agent. agent_id : str ID of the agent we want to send the message to. msg_type : str Message type. Raises ------ WazuhInternalError(1012) If the message was invalid to queue. WazuhError(1014) If there was an error communicating with socket. Returns ------- str Message confirming the message has been sent. """ # Variables to check if msg is a non active-response message or a restart message msg_is_no_ar = msg in [WazuhQueue.HC_SK_RESTART, WazuhQueue.HC_FORCE_RECONNECT] msg_is_restart = msg in [WazuhQueue.RESTART_AGENTS, WazuhQueue.RESTART_AGENTS_JSON] # Create flag and string used to specify the agent ID if agent_id: flag = 'NNS' if not msg_is_no_ar else 'N!S' str_agent_id = agent_id else: flag = 'ANN' if not msg_is_no_ar else 'A!N' str_agent_id = '(null)' # AR if msg_type == WazuhQueue.AR_TYPE: socket_msg = create_wazuh_queue_socket_msg(flag, str_agent_id, msg) if agent_id != '000' else msg # Return message ret_msg = "Command sent." # NO-AR: Restart syscheck and reconnect # Restart agents else: # If msg is not a non active-response command and not a restart command, raises WazuhInternalError if not msg_is_no_ar and not msg_is_restart: raise WazuhInternalError(1012, msg) socket_msg = create_wazuh_queue_socket_msg(flag, str_agent_id, msg, is_restart=msg_is_restart) # Return message if msg == WazuhQueue.HC_SK_RESTART: ret_msg = "Restarting Syscheck on agent" if agent_id else "Restarting Syscheck on all agents" elif msg == WazuhQueue.HC_FORCE_RECONNECT: ret_msg = "Reconnecting agent" if agent_id else "Reconnecting all agents" else: # msg == WazuhQueue.RESTART_AGENTS or msg == WazuhQueue.RESTART_AGENTS_JSON ret_msg = "Restarting agent" if agent_id else "Restarting all agents" try: # Send message self._send(socket_msg.encode()) except: raise WazuhError(1014, extra_message=f": WazuhQueue socket with path {self.path}") return ret_msg
def _connect(self): try: self.s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) self.s.connect(self.path) except Exception as e: raise WazuhInternalError(1013, extra_message=str(e))
("wrong", WazuhError(1202)) ]) def test_check_status(status, expected_result): try: # UUT call result = decoder.check_status(status) assert result == expected_result except WazuhError as e: # If the UUT call returns an exception we check it has the appropriate error code assert e.code == expected_result.code @pytest.mark.parametrize("filename, relative_dirname, status, permissions, exception", [ ('test1_decoders.xml', 'decoders', "all", 777, None), ('test2_decoders.xml', 'decoders', "enabled", 777, None), ('wrong_decoders.xml', 'decoders', "all", 777, WazuhInternalError(1501)), ('non_existing.xml', 'decoders', "disabled", 777, WazuhError(1502)), ('test1_decoders.xml', 'decoders', "all", 000, WazuhError(1502)), ]) @patch('wazuh.core.common.wazuh_path', new=test_data_path) def test_load_decoders_from_file(filename, relative_dirname, status, permissions, exception): full_file_path = os.path.join(test_data_path, relative_dirname, filename) try: old_permissions = stat.S_IMODE(os.lstat(full_file_path).st_mode) except FileNotFoundError: old_permissions = None try: # Set file permissions if the file exists os.path.exists(full_file_path) and os.chmod(full_file_path, permissions) # UUT call result = decoder.load_decoders_from_file(filename, relative_dirname, status)
def get_decoders_files(status=None, relative_dirname=None, filename=None, offset=0, limit=common.database_limit, sort_by=None, sort_ascending=True, search_text=None, complementary_search=False, search_in_fields=None): """Gets a list of the available decoder files. :param status: Filters by status: enabled, disabled, all. :param relative_dirname: Filters by relative dirname. :param filename: List of filenames to filter by. :param offset: First item to return. :param limit: Maximum number of items to return. :param sort_by: Fields to sort the items by :param sort_ascending: Sort in ascending (true) or descending (false) order :param search_text: Text to search :param complementary_search: Find items without the text to search :param search_in_fields: Fields to search in :return: AffectedItemsWazuhResult """ result = AffectedItemsWazuhResult( none_msg='No decoder files were returned', some_msg='Some decoder files were not returned', all_msg='All decoder files were returned') status = check_status(status) ruleset_conf = configuration.get_ossec_conf(section='ruleset')['ruleset'] if not ruleset_conf: raise WazuhInternalError(1500) decoders_files = list() tags = ['decoder_include', 'decoder_exclude', 'decoder_dir'] if isinstance(filename, list): for f in filename: decoders_files.extend( format_rule_decoder_file( ruleset_conf, { 'status': status, 'relative_dirname': relative_dirname, 'filename': f }, tags)) else: decoders_files = format_rule_decoder_file( ruleset_conf, { 'status': status, 'relative_dirname': relative_dirname, 'filename': filename }, tags) data = process_array(decoders_files, search_text=search_text, search_in_fields=search_in_fields, complementary_search=complementary_search, sort_by=sort_by, sort_ascending=sort_ascending, offset=offset, limit=limit) result.affected_items = data['items'] result.total_affected_items = data['totalItems'] return result
def send_msg_to_agent(self, msg: Union[str, dict] = '', agent_id: str = '', msg_type: str = '') -> str: """Send message to agent. Active-response Agents: /var/ossec/queue/alerts/ar - Existing command: - (msg_to_agent) [] NNS 001 restart-ossec0 arg1 arg2 arg3 - (msg_to_agent) [] ANN (null) restart-ossec0 arg1 arg2 arg3 - Custom command: - (msg_to_agent) [] NNS 001 !test.sh arg1 arg2 arg3 - (msg_to_agent) [] ANN (null) !test.sh arg1 arg2 arg3 Agents with version >= 4.2.0: - Existing and custom commands: - (msg_to_agent) [] NNS 001 {JSON message} Manager: /var/ossec/queue/alerts/execq - Existing or custom command: - {JSON message} Parameters ---------- msg : str Message to be sent to the agent. agent_id : str ID of the agent we want to send the message to. msg_type : str Message type. Raises ------ WazuhError(1652) If it was unable to run the command. WazuhInternalError(1012) If the message was invalid to queue. WazuhError(1601) If it was unable to run the syscheck scan on the agent because it is a non active agent. WazuhError(1702) If it was unable to restart the agent. Returns ------- str Message confirming the message has been sent. """ # Build message ALL_AGENTS_C = 'A' NONE_C = 'N' SPECIFIC_AGENT_C = 'S' NO_AR_C = '!' if agent_id: str_all_agents = NONE_C str_agent = SPECIFIC_AGENT_C str_agent_id = agent_id else: str_all_agents = ALL_AGENTS_C str_agent = NONE_C str_agent_id = "(null)" # AR if msg_type == WazuhQueue.AR_TYPE: if agent_id != "000": # Example restart 'msg': restart-ossec0 - null (from_the_server) (no_rule_id) socket_msg = "{0} {1}{2}{3} {4} {5}".format("(msg_to_agent) []", str_all_agents, NONE_C, str_agent, str_agent_id, msg) elif agent_id == "000": socket_msg = msg # Send message try: self._send(socket_msg.encode()) except Exception: raise WazuhError(1652) return "Command sent." # Legacy: Restart syscheck, restart agents else: if msg == WazuhQueue.HC_SK_RESTART: socket_msg = "{0} {1}{2}{3} {4} {5}".format("(msg_to_agent) []", str_all_agents, NO_AR_C, str_agent, str_agent_id, WazuhQueue.HC_SK_RESTART) elif msg == WazuhQueue.RESTART_AGENTS or msg == WazuhQueue.RESTART_AGENTS_JSON: socket_msg = "{0} {1}{2}{3} {4} {5} - {6} (from_the_server) (no_rule_id)".format("(msg_to_agent) []", str_all_agents, NONE_C, str_agent, str_agent_id, msg, "null") else: raise WazuhInternalError(1012, msg) # Send message try: self._send(socket_msg.encode()) except: if msg == WazuhQueue.HC_SK_RESTART: if agent_id: raise WazuhError(1601, "on agent") else: raise WazuhError(1601, "on all agents") elif msg == WazuhQueue.RESTART_AGENTS: raise WazuhError(1702) # Return message if msg == WazuhQueue.HC_SK_RESTART: return "Restarting Syscheck on agent" if agent_id else "Restarting Syscheck on all agents" elif msg == WazuhQueue.RESTART_AGENTS: return "Restarting agent" if agent_id else "Restarting all agents"
def totals(date): """ Returns the totals file. :param date: date object with the date value of the stats :return: Array of dictionaries. Each dictionary represents an hour. """ stat_filename = "" try: stat_filename = os.path.join( common.stats_path, "totals", str(date.year), MONTHS[date.month - 1], f"ossec-totals-{date.strftime('%d')}.log") stats = open(stat_filename, 'r') except IOError: raise WazuhError(1308, extra_message=stat_filename) result = AffectedItemsWazuhResult( all_msg='Statistical information for each node was successfully read', some_msg='Could not read statistical information for some nodes', none_msg='Could not read statistical information for any node') alerts = [] for line in stats: data = line.split('-') if len(data) == 4: sigid = int(data[1]) level = int(data[2]) times = int(data[3]) alert = {'sigid': sigid, 'level': level, 'times': times} alerts.append(alert) else: data = line.split('--') if len(data) != 5: if len(data) in (0, 1): continue else: result.add_failed_item( id_=node_id if cluster_enabled else 'manager', error=WazuhInternalError(1309)) return result hour = int(data[0]) total_alerts = int(data[1]) events = int(data[2]) syscheck = int(data[3]) firewall = int(data[4]) result.affected_items.append({ 'hour': hour, 'alerts': alerts, 'totalAlerts': total_alerts, 'events': events, 'syscheck': syscheck, 'firewall': firewall }) alerts = [] result.total_affected_items = len(result.affected_items) return result
def validation(): """Check if Wazuh configuration is OK. :return: AffectedItemsWazuhResult. """ result = AffectedItemsWazuhResult(**_validation_default_result_kwargs) lock_file = open(execq_lockfile, 'a+') fcntl.lockf(lock_file, fcntl.LOCK_EX) try: # Sockets path api_socket_relative_path = join('queue', 'alerts', 'execa') api_socket_path = join(common.ossec_path, api_socket_relative_path) execq_socket_path = common.EXECQ # Message for checking Wazuh configuration execq_msg = 'check-manager-configuration ' # Remove api_socket if exists try: remove(api_socket_path) except OSError as e: if exists(api_socket_path): extra_msg = f'Socket: WAZUH_PATH/{api_socket_relative_path}. Error: {e.strerror}' raise WazuhInternalError(1014, extra_message=extra_msg) # up API socket try: api_socket = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) api_socket.bind(api_socket_path) # Timeout api_socket.settimeout(5) except OSError as e: extra_msg = f'Socket: WAZUH_PATH/{api_socket_relative_path}. Error: {e.strerror}' raise WazuhInternalError(1013, extra_message=extra_msg) # Connect to execq socket if exists(execq_socket_path): try: execq_socket = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) execq_socket.connect(execq_socket_path) except OSError as e: extra_msg = f'Socket: WAZUH_PATH/queue/alerts/execq. Error {e.strerror}' raise WazuhInternalError(1013, extra_message=extra_msg) else: raise WazuhInternalError(1901) # Send msg to execq socket try: execq_socket.send(execq_msg.encode()) execq_socket.close() except socket.error as e: raise WazuhInternalError(1014, extra_message=str(e)) finally: execq_socket.close() # If api_socket receives a message, configuration is OK try: buffer = bytearray() # Receive data datagram = api_socket.recv(4096) buffer.extend(datagram) except socket.timeout as e: raise WazuhInternalError(1014, extra_message=str(e)) finally: api_socket.close() # Remove api_socket if exists(api_socket_path): remove(api_socket_path) try: response = parse_execd_output(buffer.decode('utf-8').rstrip('\0')) except (KeyError, json.decoder.JSONDecodeError) as e: raise WazuhInternalError(1904, extra_message=str(e)) result.affected_items.append({'name': node_id, **response}) result.total_affected_items += 1 except WazuhError as e: result.add_failed_item(id_=node_id, error=e) finally: fcntl.lockf(lock_file, fcntl.LOCK_UN) lock_file.close() return result
def get_group_files(group_list=None, offset=0, limit=None, search_text=None, search_in_fields=None, complementary_search=False, sort_by=None, sort_ascending=True, hash_algorithm='md5'): """Gets the group files. :param group_list: List of Group names. :param offset: First item to return. :param limit: Maximum number of items to return. :param sort_by: Fields to sort the items by. :param sort_ascending: Sort in ascending (true) or descending (false) order. :param search_text: Text to search. :param complementary_search: Find items without the text to search. :param search_in_fields: Fields to search in. :param hash_algorithm: hash algorithm used to get mergedsum and configsum. :return: WazuhResult. """ # We access unique group_id from list, this may change if and when we decide to add option to get files for # a list of groups group_id = group_list[0] group_path = common.shared_path result = AffectedItemsWazuhResult( all_msg='All selected groups files were returned', some_msg='Some groups files were not returned', none_msg='No groups files were returned') if group_id: if not Agent.group_exists(group_id): result.add_failed_item(id_=group_id, error=WazuhResourceNotFound(1710)) return result group_path = path.join(common.shared_path, group_id) if not path.exists(group_path): result.add_failed_item(id_=group_path, error=WazuhError(1006)) return result try: data = [] for entry in listdir(group_path): item = dict() item['filename'] = entry item['hash'] = get_hash(path.join(group_path, entry), hash_algorithm) data.append(item) # ar.conf ar_path = path.join(common.shared_path, 'ar.conf') data.append({ 'filename': "ar.conf", 'hash': get_hash(ar_path, hash_algorithm) }) data = process_array(data, search_text=search_text, search_in_fields=search_in_fields, complementary_search=complementary_search, sort_by=sort_by, sort_ascending=sort_ascending, offset=offset, limit=limit) result.affected_items = data['items'] result.total_affected_items = data['totalItems'] except WazuhError as e: result.add_failed_item(id_=group_path, error=e) raise e except Exception as e: raise WazuhInternalError(1727, extra_message=str(e)) return result
from os.path import isfile from distutils.version import LooseVersion import sqlite3 import sys import time # Python 2/3 compatibility if sys.version_info[0] == 3: unicode = str # Check SQL compatibility: >= 3.7.0.0 if LooseVersion(sqlite3.sqlite_version) < LooseVersion('3.7.0.0'): msg = str(sqlite3.sqlite_version) msg += "\nTry to export the internal SQLite library:" msg += "\nexport LD_LIBRARY_PATH=$LD_LIBRARY_PATH:{0}/lib".format( common.wazuh_path) raise WazuhInternalError(2001, extra_message=msg) class Connection: """ Represents a connection against a database """ def __init__(self, db_path=common.database_path_global, busy_sleep=0.001, max_attempts=50): """ Constructor """ self.db_path = db_path
def execute(self, query, count=False, delete=False, update=False): """ Sends a sql query to wdb socket """ def send_request_to_wdb(query_lower, step, off, response): try: request = query_lower.replace(':limit', 'limit {}'.format(step)).replace(':offset', 'offset {}'.format(off)) response.extend(self._send(request)) except ValueError: # if the step is already 1, it can't be divided if step == 1: raise WazuhInternalError(2007) send_request_to_wdb(query_lower, step // 2, off, response) # Add step // 2 remaining when the step is odd to avoid losing information send_request_to_wdb(query_lower, step // 2 + step % 2, step // 2 + off, response) query_lower = self.__query_lower(query) self.__query_input_validation(query_lower) # only for delete queries if delete: regex = re.compile(r"\w+ \d+? (sql delete from ([a-z0-9,_ ]+)|\w+ delete$)") if regex.match(query_lower) is None: raise WazuhError(2004, "Delete query is wrong") return self._send(query_lower) # only for update queries if update: # regex = re.compile(r"\w+ \d+? sql update ([a-z0-9,*_ ]+) set value = '([a-z0-9,*_ ]+)' where key (=|like)?" regex = re.compile(r"\w+ \d+? sql update ([\w\d,*_ ]+) set value = '([\w\d,*_ ]+)' where key (=|like)?" r" '([a-z0-9,*_%\- ]+)'") if regex.match(query_lower) is None: raise WazuhError(2004, "Update query is wrong") return self._send(query_lower) # Remove text inside 'where' clause to prevent finding reserved words (offset/count) query_without_where = re.sub(r'where \([^()]*\)', 'where ()', query_lower) # if the query has already a parameter limit / offset, divide using it offset = 0 if re.search(r'offset \d+', query_without_where): offset = int(re.compile(r".* offset (\d+)").match(query_lower).group(1)) # Replace offset with a wildcard query_lower = ' :offset'.join(query_lower.rsplit((' offset {}'.format(offset)), 1)) if not re.search(r'.?select count\([\w \*]+\)( as [^,]+)? from', query_without_where): lim = 0 if re.search(r'limit \d+', query_without_where): lim = int(re.compile(r".* limit (\d+)").match(query_lower).group(1)) # Replace limit with a wildcard query_lower = ' :limit'.join(query_lower.rsplit((' limit {}'.format(lim)), 1)) regex = re.compile(r"\w+(?: \d*|)? sql select ([A-Z a-z0-9,*_` \.\-%\(\):\']+?) from") select = regex.match(query_lower).group(1) gb_regex = re.compile(r"(group by [^\s]+)") countq = query_lower.replace(select, "count(*)", 1).replace(":limit", "").replace(":offset", "") try: group_by = gb_regex.search(query_lower) if group_by: countq = countq.replace(group_by.group(1), '') except IndexError: pass try: total = list(self._send(countq)[0].values())[0] except IndexError: total = 0 limit = lim if lim != 0 else total response = [] step = limit if limit < self.request_slice and limit > 0 else self.request_slice if ':limit' not in query_lower: query_lower += ' :limit' if ':offset' not in query_lower: query_lower += ' :offset' try: for off in range(offset, limit + offset, step): send_request_to_wdb(query_lower, step, off, response) except ValueError as e: raise WazuhError(2006, str(e)) except WazuhError as e: raise e except Exception as e: raise WazuhInternalError(2007, str(e)) if count: return response, total else: return response else: return list(self._send(query_lower)[0].values())[0]
def execute(self, query, count=False, delete=False, update=False): """ Sends a sql query to wdb socket """ def send_request_to_wdb(query_lower, step, off, response): try: request = "{} limit {} offset {}".format( query_lower, step, off) response.extend(self._send(request)) except ValueError: # if the step is already 1, it can't be divided if step == 1: raise WazuhInternalError(2007) send_request_to_wdb(query_lower, step // 2, off, response) send_request_to_wdb(query_lower, step // 2, step // 2 + off, response) query_lower = self.__query_lower(query) self.__query_input_validation(query_lower) # only for delete queries if delete: regex = re.compile(r"\w+ \d+? sql delete from ([a-z0-9,_ ]+)") if regex.match(query_lower) is None: raise WazuhError(2004, "Delete query is wrong") return self._send(query_lower) # only for update queries if update: # regex = re.compile(r"\w+ \d+? sql update ([a-z0-9,*_ ]+) set value = '([a-z0-9,*_ ]+)' where key (=|like)?" regex = re.compile( r"\w+ \d+? sql update ([\w\d,*_ ]+) set value = '([\w\d,*_ ]+)' where key (=|like)?" r" '([a-z0-9,*_%\- ]+)'") if regex.match(query_lower) is None: raise WazuhError(2004, "Update query is wrong") return self._send(query_lower) # Remove text inside 'where' clause to prevent finding reserved words (offset/count) query_without_where = re.sub(r'where \(.*\)', 'where ()', query_lower) # if the query has already a parameter limit / offset, divide using it offset = 0 if 'offset' in query_without_where: offset = int( re.compile(r".* offset (\d+)").match(query_lower).group(1)) query_lower = query_lower.replace(" offset {}".format(offset), "") if not re.search(r'.?count\(.*\).?', query_without_where): lim = 0 if 'limit' in query_lower: lim = int( re.compile(r".* limit (\d+)").match(query_lower).group(1)) query_lower = query_lower.replace(" limit {}".format(lim), "") regex = re.compile( r"\w+(?: \d*|)? sql select ([A-Z a-z0-9,*_` \.\-%\(\):\']+) from" ) select = regex.match(query_lower).group(1) countq = query_lower.replace(select, "count(*)", 1) try: total = list(self._send(countq)[0].values())[0] except IndexError: total = 0 limit = lim if lim != 0 else total response = [] step = limit if limit < self.request_slice and limit > 0 else self.request_slice try: for off in range(offset, limit + offset, step): send_request_to_wdb(query_lower, step, off, response) except ValueError as e: raise WazuhError(2006, str(e)) except Exception as e: raise WazuhInternalError(2007, str(e)) if count: return response, total else: return response else: return list(self._send(query_lower)[0].values())[0]
def get_active_configuration(agent_id, component, configuration): """ Reads agent loaded configuration in memory """ if not component or not configuration: raise WazuhError(1307) components = { "agent", "agentless", "analysis", "auth", "com", "csyslog", "integrator", "logcollector", "mail", "monitor", "request", "syscheck", "wmodules" } # checks if the component is correct if component not in components: raise WazuhError(1101, f'Valid components: {", ".join(components)}') sockets_path = os_path.join(common.ossec_path, "queue", "ossec") if agent_id == '000': dest_socket = os_path.join(sockets_path, component) command = f"getconfig {configuration}" else: dest_socket = os_path.join(sockets_path, "request") command = f"{str(agent_id).zfill(3)} {component} getconfig {configuration}" # Socket connection try: s = OssecSocket(dest_socket) except Exception: raise WazuhInternalError(1121) # Send message s.send(command.encode()) # Receive response try: # Receive data length rec_msg_ok, rec_msg = s.receive().decode().split(" ", 1) except ValueError: raise WazuhInternalError(1118, extra_message="Data could not be received") s.close() if rec_msg_ok.startswith('ok'): msg = json.loads(rec_msg) # Include password if auth->use_password enabled and authd.pass file exists if msg.get('auth', {}).get('use_password') == 'yes': try: with open(os_path.join(common.ossec_path, "etc", "authd.pass"), 'r') as f: msg['authd.pass'] = f.read().rstrip() except IOError: pass return msg else: raise WazuhError(1117 if "No such file or directory" in rec_msg or "Cannot send request" in rec_msg else 1116, extra_message='{0}:{1}'.format( component, configuration))
Wazuh is a python package to manage OSSEC. """ __version__ = '4.3.0' msg = "\n\nPython 2.7 or newer not found." msg += "\nUpdate it or set the path to a valid version. Example:" msg += "\n export PATH=$PATH:/opt/rh/python27/root/usr/bin" msg += "\n export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/opt/rh/python27/root/usr/lib64" try: from sys import version_info as python_version if python_version.major < 2 or (python_version.major == 2 and python_version.minor < 7): raise WazuhInternalError(999, msg) except Exception as e: raise WazuhInternalError(999, msg) class Wazuh: """ Basic class to set up OSSEC directories """ def __init__(self): """ Initialize basic information and directories. :return: """ self.version = common.wazuh_version
def get_sca_checks(policy_id=None, agent_list=None, q="", offset=0, limit=common.database_limit, sort=None, search=None, select=None, filters=None): """ Get a list of checks analyzed for a policy Parameters ---------- policy_id : str Policy id to get the checks from. agent_list : list Agent id to get the policies from q : str Defines query to filter in DB. offset : int First item to return. limit : int Maximum number of items to return. sort : str Sorts the items. Format: {"fields":["field1","field2"],"order":"asc|desc"}. search : str Looks for items with the specified string. Format: {"fields": ["field1","field2"]} select : str Select fields to return. Format: {"fields":["field1","field2"]}. filters : str Define field filters required by the user. Format: {"field1":"value1", "field2":["value2","value3"]} Returns ------- AffectedItemsWazuhResult """ result = AffectedItemsWazuhResult( all_msg='All selected sca/policy information was returned', some_msg='Some sca/policy information was not returned', none_msg='No sca/policy information was returned') if len(agent_list) != 0: sca_checks = list() if agent_list[0] in get_agents_info(): fields_translation = { **fields_translation_sca_check, **fields_translation_sca_check_compliance, **fields_translation_sca_check_rule } full_select = ( list(fields_translation_sca_check.keys()) + list(fields_translation_sca_check_compliance.keys()) + list(fields_translation_sca_check_rule.keys())) # Workaround for too long sca_checks results until the chunk algorithm is implemented (1/2) db_query = WazuhDBQuerySCA(agent_id=agent_list[0], offset=0, limit=None, sort=None, filters=filters, search=None, select=full_select, count=True, get_data=True, query=f"policy_id={policy_id}", default_query=default_query_sca_check, default_sort_field='policy_id', fields=fields_translation, count_field='id') result_dict = db_query.run() if 'items' in result_dict: checks = result_dict['items'] else: raise WazuhInternalError(2007) groups = groupby(checks, key=itemgetter('id')) select_fields = full_select if select is None else select select_fields = set([ field if field != 'compliance' else 'compliance' for field in select_fields if field in fields_translation_sca_check ]) # Rearrange check and compliance fields for _, group in groups: group_list = list(group) check_dict = { k: v for k, v in group_list[0].items() if k in select_fields } for extra_field, field_translations in [ ('compliance', fields_translation_sca_check_compliance), ('rules', fields_translation_sca_check_rule) ]: if (select is None or extra_field in select) \ and set(field_translations.keys()) & group_list[0].keys(): check_dict[extra_field] = [ dict(zip(field_translations.values(), x)) for x in set(( map(itemgetter( *field_translations.keys()), group_list))) ] sca_checks.append(check_dict) else: result.add_failed_item(id_=agent_list[0], error=WazuhResourceNotFound(1701)) result.total_affected_items = 0 # Workaround for too long sca_checks results until the chunk algorithm is implemented (2/2) data = process_array( sca_checks, search_text=search['value'] if search else None, complementary_search=search['negation'] if search else False, sort_by=sort['fields'] if sort else ['policy_id'], sort_ascending=False if sort and sort['order'] == 'desc' else True, offset=offset, limit=limit, q=q) result.affected_items = data['items'] result.total_affected_items = data['totalItems'] return result
def send_msg_to_agent(self, msg, agent_id=None, msg_type=None): # Active-response # Agents: /var/ossec/queue/alerts/ar # - Existing command: # - (msg_to_agent) [] NNS 001 restart-ossec0 arg1 arg2 arg3 # - (msg_to_agent) [] ANN (null) restart-ossec0 arg1 arg2 arg3 # - Custom command: # - (msg_to_agent) [] NNS 001 !test.sh arg1 arg2 arg3 # - (msg_to_agent) [] ANN (null) !test.sh arg1 arg2 arg3 # Manager: /var/ossec/queue/alerts/execq # - Existing command: # - restart-ossec0 arg1 arg2 arg3 # - Custom command: # - !test.sh Hello World # Build message ALL_AGENTS_C = 'A' NONE_C = 'N' SPECIFIC_AGENT_C = 'S' NO_AR_C = '!' if agent_id: str_all_agents = NONE_C str_agent = SPECIFIC_AGENT_C str_agent_id = agent_id else: str_all_agents = ALL_AGENTS_C str_agent = NONE_C str_agent_id = "(null)" # AR if msg_type == OssecQueue.AR_TYPE: if agent_id != "000": # Example restart 'msg': restart-ossec0 - null (from_the_server) (no_rule_id) socket_msg = "{0} {1}{2}{3} {4} {5}".format( "(msg_to_agent) []", str_all_agents, NONE_C, str_agent, str_agent_id, msg) elif agent_id == "000": socket_msg = msg # Send message try: self._send(socket_msg.encode()) except Exception: raise WazuhError(1652) return "Command sent." # Legacy: Restart syscheck, restart agents else: if msg == OssecQueue.HC_SK_RESTART: socket_msg = "{0} {1}{2}{3} {4} {5}".format( "(msg_to_agent) []", str_all_agents, NO_AR_C, str_agent, str_agent_id, OssecQueue.HC_SK_RESTART) elif msg == OssecQueue.RESTART_AGENTS: socket_msg = "{0} {1}{2}{3} {4} {5} - {6} (from_the_server) (no_rule_id)".format( "(msg_to_agent) []", str_all_agents, NONE_C, str_agent, str_agent_id, OssecQueue.RESTART_AGENTS, "null") else: raise WazuhInternalError(1012, msg) # Send message try: self._send(socket_msg.encode()) except: if msg == OssecQueue.HC_SK_RESTART: if agent_id: raise WazuhError(1601, "on agent") else: raise WazuhError(1601, "on all agents") elif msg == OssecQueue.RESTART_AGENTS: raise WazuhError(1702) # Return message if msg == OssecQueue.HC_SK_RESTART: return "Restarting Syscheck on agent" if agent_id else "Restarting Syscheck on all agents" elif msg == OssecQueue.RESTART_AGENTS: return "Restarting agent" if agent_id else "Restarting all agents"