#!/usr/bin/env python # # Authored by Jeremy Phillips # Copyright: Apache License 2.0 # # Wrapper around prowler script to parse results and forward to Wazuh # Prowler - https://github.com/toniblyx/prowler # # TODO: Add ability to disable different groups (EXTRA, etc... # TODO: Allow to disable individual checks # TODO: Remove all the commented out stuff # # Error Codes: # 1 - Unknown # 2 - SIGINT # 3 - Error output from execution of Prowler # 4 - Output row is invalid json # 5 - Wazuh must be running # 6 - Error sending to socket import argparse import json import os import re import signal import socket import subprocess import sys from datetime import datetime ################################################################################ # Constants ################################################################################ WAZUH_PATH = open("/etc/ossec-init.conf").readline().split('"')[1] DEBUG_LEVEL = 0 # Enable/disable debug mode PATH_TO_PROWLER = "{0}/integrations/prowler".format(WAZUH_PATH) # No trailing slash TEMPLATE_CHECK = """ {{ "integration": "prowler", "prowler": {0} }} """ TEMPLATE_MSG = "1:Wazuh-Prowler:{0}" TEMPLATE_ERROR = """{{ "aws_account_id": {aws_account_id}, "aws_profile": "{aws_profile}", "prowler_error": "{prowler_error}", "prowler_version": "{prowler_version}", "timestamp": "{timestamp}", "status": "Error" }} """ WAZUH_QUEUE = "{0}/queue/ossec/queue".format(WAZUH_PATH) FIELD_REMAP = { "Profile": "aws_profile", "Control": "control", "Account Number": "aws_account_id", "Level": "level", "Account Alias": "aws_account_alias", "Timestamp": "timestamp", "Region": "region", "Control ID": "control_id", "Status": "status", "Scored": "scored", "Message": "message", } CHECKS_FILES_TO_IGNORE = ["check_sample"] ################################################################################ # Functions ################################################################################ def _send_msg(msg): try: _json_msg = json.dumps(_reformat_msg(msg)) _debug("Sending Msg: {0}".format(_json_msg), 3) _socket = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) _socket.connect(WAZUH_QUEUE) _socket.send(TEMPLATE_MSG.format(_json_msg).encode()) _socket.close() except socket.error as e: if e.errno == 111: print("ERROR: Wazuh must be running.") sys.exit(5) else: print("ERROR: Error sending message to wazuh: {}".format(e)) sys.exit(6) except Exception as e: print("ERROR: Error sending message to wazuh: {}".format(e)) sys.exit(6) return def _handler(signal, frame): print("ERROR: SIGINT received.") sys.exit(12) def _debug(msg, msg_level): if DEBUG_LEVEL >= msg_level: print("DEBUG-{level}: {debug_msg}".format(level=msg_level, debug_msg=msg)) def _get_script_arguments(): _parser = argparse.ArgumentParser( usage="usage: %(prog)s [options]", description="Wazuh wodle for evaluating AWS security configuration", formatter_class=argparse.RawTextHelpFormatter, ) _parser.add_argument( "-c", "--aws_account_id", dest="aws_account_id", help="AWS Account ID for logs", required=False, ) _parser.add_argument( "-d", "--debug", action="store", dest="debug", default=0, help="Enable debug" ) _parser.add_argument( "-p", "--aws_profile", dest="aws_profile", help="The name of credential profile to use", default=None, ) _parser.add_argument( "-n", "--aws_account_alias", dest="aws_account_alias", help="AWS Account ID Alias", default="", ) _parser.add_argument( "-e", "--skip_on_error", action="store_false", dest="skip_on_error", help="If check output is invalid json, error out instead of skipping the check", default=True, ) return _parser.parse_args() def _run_prowler(prowler_args): _debug("Running prowler with args: {0}".format(prowler_args), 1) _prowler_command = "{prowler}/prowler {args}".format( prowler=PATH_TO_PROWLER, args=prowler_args ) _debug("Running command: {0}".format(_prowler_command), 2) _process = subprocess.Popen(_prowler_command, stdout=subprocess.PIPE, shell=True) _output, _error = _process.communicate() _debug("Raw prowler output: {0}".format(_output), 3) _debug("Raw prowler error: {0}".format(_error), 3) if _error is not None: _debug("PROWLER ERROR: {0}".format(_error), 1) exit(3) return _output def _get_prowler_version(options): _debug("+++ Get Prowler Version", 1) # Execute prowler, but only display the version and immediately exit return _run_prowler("-p {0} -V".format(options.aws_profile)).rstrip() def _get_prowler_results(options, prowler_check): _debug("+++ Get Prowler Results - {check}".format(check=prowler_check), 1) # Execute prowler with all checks # -b = disable banner # -p = credential profile # -M = output json return _run_prowler( "-b -c {check} -p {aws_profile} -M json".format( check=prowler_check, aws_profile=options.aws_profile ) ) def _get_prowler_checks(): _prowler_checks = [] for _directory_path, _directories, _files in os.walk( "{path}/checks".format(path=PATH_TO_PROWLER) ): _debug("Checking in : {}".format(_directory_path), 3) for _file in _files: if _file in CHECKS_FILES_TO_IGNORE: _debug("Ignoring check - {}".format(_directory_path, _file), 3) elif re.match("check\d+", _file): _prowler_checks.append(_file) elif re.match("check_extra(\d+)", _file): _prowler_checks.append(_file[6:]) else: _debug("Unknown check file type- {}".format(_directory_path, _file), 3) return _prowler_checks def _send_prowler_results(prowler_results, _prowler_version, options): _debug("+++ Send Prowler Results", 1) for _check_result in prowler_results.splitlines(): # Empty row if len(_check_result) < 1: continue # Something failed during prowler check elif _check_result[:17] == "An error occurred": _debug("ERROR MSG --- {0}".format(_check_result), 2) _temp_msg = TEMPLATE_ERROR.format( aws_account_id=options.aws_account_id, aws_profile=options.aws_profile, prowler_error=_check_result.replace('"', '"'), prowler_version=_prowler_version, timestamp=datetime.now().isoformat(), ) _error_msg = json.loads(TEMPLATE_CHECK.format(_temp_msg)) _send_msg(_error_msg) continue try: _debug("RESULT MSG --- {0}".format(_check_result), 2) _check_result = json.loads(TEMPLATE_CHECK.format(_check_result)) except: _debug( "INVALID JSON --- {0}".format(TEMPLATE_CHECK.format(_check_result)), 1 ) if not options.skip_on_error: exit(4) _check_result["prowler"]["prowler_version"] = _prowler_version _check_result["prowler"]["aws_account_alias"] = options.aws_account_alias _send_msg(_check_result) return True def _reformat_msg(msg): for field in FIELD_REMAP: if field in msg["prowler"]: msg["prowler"][FIELD_REMAP[field]] = msg["prowler"][field] del msg["prowler"][field] return msg # Main ############################################################################### def main(argv): _debug("+++ Begin script", 1) # Parse arguments _options = _get_script_arguments() if int(_options.debug) > 0: global DEBUG_LEVEL DEBUG_LEVEL = int(_options.debug) _debug("+++ Debug mode on - Level: {debug}".format(debug=_options.debug), 1) _prowler_version = _get_prowler_version(_options) _prowler_checks = _get_prowler_checks() for _check in _prowler_checks: _prowler_results = _get_prowler_results(_options, _check) _send_prowler_results(_prowler_results, _prowler_version, _options) _debug("+++ Finished script", 1) return if __name__ == "__main__": try: _debug("Args: {args}".format(args=str(sys.argv)), 2) signal.signal(signal.SIGINT, _handler) main(sys.argv[1:]) sys.exit(0) except Exception as e: print("Unknown error: {}".format(e)) if DEBUG_LEVEL > 0: raise sys.exit(1)