mirror of
https://github.com/ghndrx/prowler.git
synced 2026-02-10 14:55:00 +00:00
New folder structure phase 1
This commit is contained in:
115
contrib/wazuh/README.md
Normal file
115
contrib/wazuh/README.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# Prowler integration with Wazuh (DRAFT)
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Description](#description)
|
||||
- [Features](#features)
|
||||
- [Requirements](#requirements)
|
||||
- [Integration steps](#integration-steps)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Thanks](#thanks)
|
||||
- [License](#license)
|
||||
|
||||
## Description
|
||||
|
||||
Prowler integration with WAZUH using a python wrapper. Due to the wrapper limitations, this integration can be considered as a proof of concept at this time.
|
||||
|
||||
## Features
|
||||
|
||||
Wazuh, using a wodle, runs Prowler every certain time and stores alerts (failed checks) using JSON output which Wazuh processes and sends to Elastic Search to be queried from Kibana.
|
||||
|
||||
## Requirements
|
||||
|
||||
1. Latest AWS-CLI client (`pip install awscli`). If you have it already installed, make sure you are using the latest version, upgrade it: `pip install awscli --upgrade`.
|
||||
2. Also `jq` is needed (`pip install jq`).
|
||||
|
||||
Remember, you must have AWS-CLI credentials already configured in the same instance running Wazuh (run `aws configure` if needed). In this DRAFT I'm using `/root/.aws/credentials` file with [default] as AWS-CLI profile and access keys but you can use assume role configuration as well. For the moment instance profile is not supported in this wrapper.
|
||||
|
||||
It may work in previous versions of Wazuh, but this document and integration was tested on Wazuh 3.7.1. So to have a Wazuh running installation is obviously required.
|
||||
|
||||
## Integration steps
|
||||
|
||||
Add Prowler to Wazuh's integrations:
|
||||
```
|
||||
cd /var/ossec/integrations/
|
||||
git clone https://github.com/toniblyx/prowler
|
||||
```
|
||||
Copy `prowler-wrapper.py` to integrations folder:
|
||||
|
||||
```
|
||||
cp /var/ossec/integrations/prowler/integrations/prowler-wrapper.py /var/ossec/integrations/prowler-wrapper.py
|
||||
```
|
||||
Then make sure it is executable:
|
||||
```
|
||||
chmod +x /var/ossec/integrations/prowler-wrapper.py
|
||||
```
|
||||
Run Prowler wrapper manually to make sure it works fine, use `--debug 1` or `--debug 2`):
|
||||
```
|
||||
/var/ossec/integrations/prowler-wrapper.py --aws_profile default --aws_account_alias default --debug 2
|
||||
```
|
||||
|
||||
Copy rules file to its location:
|
||||
|
||||
```
|
||||
cp /var/ossec/integrations/prowler/integrations/prowler_rules.xml /var/ossec/etc/rules/prowler_rules.xml
|
||||
```
|
||||
|
||||
Edit `/var/ossec/etc/ossec.conf` and add the following wodle configuration. Remember that here `timeout 21600 seconds` is 6 hours, just to allow Prowler runs completely in case of a large account. The interval recommended is 1d:
|
||||
```xml
|
||||
<wodle name="command">
|
||||
<disabled>no</disabled>
|
||||
<tag>aws-prowler: account1</tag>
|
||||
<command>/var/ossec/integrations/prowler-wrapper.py --aws_profile default --aws_account_alias default</command>
|
||||
<interval>1d</interval>
|
||||
<ignore_output>no</ignore_output>
|
||||
<run_on_start>no</run_on_start>
|
||||
<timeout>21600</timeout>
|
||||
</wodle>
|
||||
```
|
||||
To check multiple AWS accounts, add a wodle per account.
|
||||
|
||||
Now restart `wazuh-manager` and look at `/var/ossec/logs/alerts/alerts.json`, eventually you should see FAIL checks detected by Prowler, then you will find them using Kibana. Some Kibana search examples are:
|
||||
```
|
||||
data.integration:"prowler" and data.prowler.status:"Fail"
|
||||
data.integration:"prowler" AND rule.level >= 5
|
||||
data.integration:"prowler" AND rule.level : 7 or 9
|
||||
```
|
||||
|
||||
Adjust the level range to what alerts you want to include, as alerts, Elastic Search only gets fail messages (7 and 9).
|
||||
|
||||
1 - pass
|
||||
3 - info
|
||||
5 - error
|
||||
7 - fail: not scored
|
||||
9 - fail: scored
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
To make sure rules are working fine, run `/var/ossec/bin/ossec-logtest` and copy/paste this sample JSON:
|
||||
|
||||
```json
|
||||
{"prowler":{"Timestamp":"2018-11-29T03:15:50Z","Region":"us-east-1","Profile":"default","Account Number”:”1234567890”,”Control":"[check34] Ensure a log metric filter and alarm exist for IAM policy changes (Scored)","Message":"No CloudWatch group found for CloudTrail events","Status":"Fail","Scored":"Scored","Level":"Level 1","Control ID":"3.4"}, "integration": "prowler"}
|
||||
```
|
||||
You must see 3 phases goin on.
|
||||
|
||||
To check if there is any error you can enable the debug mode of `modulesd` setting the `wazuh_modules.debug=0` variable to 2 in `/var/ossec/etc/internal_options.conf` file. Restart wazun-manager and errors should appear in the `/var/ossec/logs/ossec.log` file.
|
||||
|
||||
## Thanks
|
||||
|
||||
To Jeremy Phillips <jeremy@uranusbytes.com>, who wrote the initial rules file and wrapper and helped me to understand how it works and debug it.
|
||||
|
||||
To [Marta Gomez](https://github.com/mgmacias95) and the [Wazuh](https://www.wazuh.com) team for their support to debug this integration and make it work properly. Their job on Wazuh and willingness to help is invaluable.
|
||||
|
||||
## License
|
||||
|
||||
All CIS based checks in the checks folder are licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Public License.
|
||||
The link to the license terms can be found at
|
||||
<https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode>
|
||||
Any other piece of code is licensed as Apache License 2.0 as specified in each file. You may obtain a copy of the License at
|
||||
<http://www.apache.org/licenses/LICENSE-2.0>
|
||||
|
||||
NOTE: If you are interested in using Prowler for commercial purposes remember that due to the CC4.0 license “The distributors or partners that are interested and using Prowler would need to enroll as CIS SecureSuite Members to incorporate this product, which includes references to CIS resources, in their offering.". Information about CIS pricing for vendors here: <https://www.cisecurity.org/cis-securesuite/pricing-and-categories/product-vendor/>
|
||||
|
||||
**I'm not related anyhow with CIS organization, I just write and maintain Prowler to help companies over the world to make their cloud infrastructure more secure.**
|
||||
|
||||
If you want to contact me visit <https://blyx.com/contact>
|
||||
242
contrib/wazuh/prowler-wrapper.py
Normal file
242
contrib/wazuh/prowler-wrapper.py
Normal file
@@ -0,0 +1,242 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Authored by Jeremy Phillips <jeremy@uranusbytes.com>
|
||||
# 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 signal
|
||||
import sys
|
||||
import socket
|
||||
import argparse
|
||||
import subprocess
|
||||
import json
|
||||
from datetime import datetime
|
||||
import os
|
||||
import re
|
||||
|
||||
################################################################################
|
||||
# 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)
|
||||
45
contrib/wazuh/prowler_rules.xml
Normal file
45
contrib/wazuh/prowler_rules.xml
Normal file
@@ -0,0 +1,45 @@
|
||||
<!--
|
||||
Rules for parsing Prowler output
|
||||
Authored by Jeremy Phillips <jeremy@uranusbytes.com>
|
||||
Copyright: Apache License 2.0
|
||||
ID: 110000-110009
|
||||
Prowler - https://github.com/toniblyx/prowler
|
||||
-->
|
||||
|
||||
<group name="local,amazon,prowler,">
|
||||
<!-- Filter 1: Only prowler events -->
|
||||
<rule id="110001" level="0">
|
||||
<field name="integration">prowler</field>
|
||||
<description>Prowler Check Result: $(prowler.status) - Control $(prowler.control_id)</description>
|
||||
</rule>
|
||||
<!-- Check Result: Pass -->
|
||||
<rule id="110002" level="1">
|
||||
<if_sid>110001</if_sid>
|
||||
<field name="prowler.status">Pass</field>
|
||||
<description>Prowler Check Result: $(prowler.status) - Control $(prowler.control_id)</description>
|
||||
</rule>
|
||||
<!-- Check Result: Info -->
|
||||
<rule id="110003" level="3">
|
||||
<if_sid>110001</if_sid>
|
||||
<field name="prowler.status">Info</field>
|
||||
<description>Prowler Check Result: $(prowler.status) - Control $(prowler.control_id)</description>
|
||||
</rule>
|
||||
<!-- Check Result: Error -->
|
||||
<rule id="110004" level="5">
|
||||
<if_sid>110001</if_sid>
|
||||
<field name="prowler.status">Error</field>
|
||||
<description>Prowler Check Result: $(prowler.status) - Control $(prowler.control_id)</description>
|
||||
</rule>
|
||||
<!-- Check Result: Fail, Scored -->
|
||||
<rule id="110005" level="9">
|
||||
<if_sid>110001</if_sid>
|
||||
<field name="prowler.status">Fail</field>
|
||||
<description>Prowler Check Result: $(prowler.status) - Control $(prowler.control_id)</description>
|
||||
</rule>
|
||||
<!-- Check Result: Fail, Not Scored -->
|
||||
<rule id="110006" level="7">
|
||||
<if_sid>110005</if_sid>
|
||||
<field name="prowler.scored">Not Scored</field>
|
||||
<description>Prowler Check Result: $(prowler.status) - Control $(prowler.control_id)</description>
|
||||
</rule>
|
||||
</group>
|
||||
Reference in New Issue
Block a user