forked from p34709852/monkey
* Added env' config
* Added exporters and aws exporter * changed report generation to be automatic on monkey death with support of on-demand report generation and mongo storage
This commit is contained in:
parent
c888ab7bc9
commit
148ee3f0f0
|
@ -5,6 +5,8 @@ import aws
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
AWS = 'aws'
|
||||||
|
STANDARD = 'standard'
|
||||||
|
|
||||||
ENV_DICT = {
|
ENV_DICT = {
|
||||||
'standard': standard.StandardEnvironment,
|
'standard': standard.StandardEnvironment,
|
||||||
|
|
|
@ -1,9 +1,293 @@
|
||||||
from exporter import Exporter
|
import logging
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
import boto3
|
||||||
|
|
||||||
|
from cc.resources.exporter import Exporter
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class AWSExporter(Exporter):
|
class AWSExporter(Exporter):
|
||||||
|
|
||||||
def __init__(self):
|
@staticmethod
|
||||||
Exporter.__init__(self)
|
def handle_report(report_json):
|
||||||
|
|
||||||
def handle_report(self, report_json):
|
findings_list = []
|
||||||
pass
|
issues_list = report_json['recommendations']['issues']
|
||||||
|
for machine in issues_list:
|
||||||
|
for issue in issues_list[machine]:
|
||||||
|
findings_list.append(AWSExporter._prepare_finding(issue))
|
||||||
|
|
||||||
|
if not AWSExporter._send_findings(findings_list):
|
||||||
|
logger.error('Exporting findings to aws failed')
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def merge_two_dicts(x, y):
|
||||||
|
z = x.copy() # start with x's keys and values
|
||||||
|
z.update(y) # modifies z with y's keys and values & returns None
|
||||||
|
return z
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _prepare_finding(issue):
|
||||||
|
findings_dict = {
|
||||||
|
'island_cross_segment': AWSExporter._handle_island_cross_segment_issue,
|
||||||
|
'ssh': AWSExporter._handle_ssh_issue,
|
||||||
|
'shellshock': AWSExporter._handle_shellshock_issue,
|
||||||
|
'tunnel': AWSExporter._handle_tunnel_issue,
|
||||||
|
'elastic': AWSExporter._handle_elastic_issue,
|
||||||
|
'smb_password': AWSExporter._handle_smb_password_issue,
|
||||||
|
'smb_pth': AWSExporter._handle_smb_pth_issue,
|
||||||
|
'sambacry': AWSExporter._handle_sambacry_issue,
|
||||||
|
'shared_passwords': AWSExporter._handle_shared_passwords_issue,
|
||||||
|
}
|
||||||
|
|
||||||
|
finding = {
|
||||||
|
"SchemaVersion": "2018-10-08",
|
||||||
|
"Id": uuid.uuid4().hex,
|
||||||
|
"ProductArn": "arn:aws:securityhub:us-west-2:324264561773:product/aws/guardduty",
|
||||||
|
"GeneratorId": issue['type'],
|
||||||
|
"AwsAccountId": "324264561773",
|
||||||
|
"Types": [
|
||||||
|
"Software and Configuration Checks/Vulnerabilities/CVE"
|
||||||
|
],
|
||||||
|
"CreatedAt": datetime.now().isoformat() + 'Z',
|
||||||
|
"UpdatedAt": datetime.now().isoformat() + 'Z',
|
||||||
|
}
|
||||||
|
return AWSExporter.merge_two_dicts(finding, findings_dict[issue['type']](issue))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _send_findings(findings_list):
|
||||||
|
|
||||||
|
securityhub = boto3.client('securityhub')
|
||||||
|
import_response = securityhub.batch_import_findings(Findings=findings_list)
|
||||||
|
print import_response
|
||||||
|
if import_response['ResponseMetadata']['HTTPStatusCode'] == 200:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _handle_tunnel_issue(issue):
|
||||||
|
finding =\
|
||||||
|
{
|
||||||
|
"Severity": {
|
||||||
|
"Product": 5,
|
||||||
|
"Normalized": 100
|
||||||
|
},
|
||||||
|
"Resources": [{
|
||||||
|
"Type": "IpAddress",
|
||||||
|
"Id": issue['dest']
|
||||||
|
}],
|
||||||
|
"RecordState": "ACTIVE",
|
||||||
|
}
|
||||||
|
|
||||||
|
finding["Title"] = "Weak segmentation - Machines were able to communicate over unused ports."
|
||||||
|
finding["Description"] = "Use micro-segmentation policies to disable communication other than the required."
|
||||||
|
finding["Remediation"] = {
|
||||||
|
"Recommendation": {
|
||||||
|
"Text": "Machines are not locked down at port level. Network tunnel was set up from {0} to {1}"
|
||||||
|
.format(issue['machine'], issue['dest'])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return finding
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _handle_sambacry_issue(issue):
|
||||||
|
finding = \
|
||||||
|
{
|
||||||
|
"Severity": {
|
||||||
|
"Product": 10,
|
||||||
|
"Normalized": 100
|
||||||
|
},
|
||||||
|
"Resources": [{
|
||||||
|
"Type": "IpAddress",
|
||||||
|
"Id": str(issue['ip_address'])
|
||||||
|
}],
|
||||||
|
"RecordState": "ACTIVE",
|
||||||
|
}
|
||||||
|
|
||||||
|
finding["Title"] = "Samba servers are vulnerable to 'SambaCry'"
|
||||||
|
finding["Description"] = "Change {0} password to a complex one-use password that is not shared with other computers on the network. Update your Samba server to 4.4.14 and up, 4.5.10 and up, or 4.6.4 and up."\
|
||||||
|
.format(issue['username'])
|
||||||
|
finding["Remediation"] = {
|
||||||
|
"Recommendation": {
|
||||||
|
"Text": "The machine {0} ({1}) is vulnerable to a SambaCry attack. The Monkey authenticated over the SMB protocol with user {2} and its password, and used the SambaCry vulnerability.".format(issue['machine'], issue['ip_address'], issue['username'])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return finding
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _handle_smb_pth_issue(issue):
|
||||||
|
finding = \
|
||||||
|
{
|
||||||
|
"Severity": {
|
||||||
|
"Product": 5,
|
||||||
|
"Normalized": 100
|
||||||
|
},
|
||||||
|
"Resources": [{
|
||||||
|
"Type": "IpAddress",
|
||||||
|
"Id": issue['ip_address']
|
||||||
|
}],
|
||||||
|
"RecordState": "ACTIVE",
|
||||||
|
}
|
||||||
|
|
||||||
|
finding["Title"] = "Machines are accessible using passwords supplied by the user during the Monkey's configuration."
|
||||||
|
finding["Description"] = "Change {0}'s password to a complex one-use password that is not shared with other computers on the network.".format(issue['username'])
|
||||||
|
finding["Remediation"] = {
|
||||||
|
"Recommendation": {
|
||||||
|
"Text": "The machine {0}({1}) is vulnerable to a SMB attack. The Monkey used a pass-the-hash attack over SMB protocol with user {2}.".format(issue['machine'], issue['ip_address'], issue['username'])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return finding
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _handle_ssh_issue(issue):
|
||||||
|
finding = \
|
||||||
|
{
|
||||||
|
"Severity": {
|
||||||
|
"Product": 1,
|
||||||
|
"Normalized": 100
|
||||||
|
},
|
||||||
|
"Resources": [{
|
||||||
|
"Type": "IpAddress",
|
||||||
|
"Id": issue['ip_address']
|
||||||
|
}],
|
||||||
|
"RecordState": "ACTIVE",
|
||||||
|
}
|
||||||
|
|
||||||
|
finding["Title"] = "Machines are accessible using SSH passwords supplied by the user during the Monkey's configuration."
|
||||||
|
finding["Description"] = "Change {0}'s password to a complex one-use password that is not shared with other computers on the network.".format(issue['username'])
|
||||||
|
finding["Remediation"] = {
|
||||||
|
"Recommendation": {
|
||||||
|
"Text": "The machine {0} ({1}) is vulnerable to a SSH attack. The Monkey authenticated over the SSH protocol with user {2} and its password.".format(issue['machine'], issue['ip_address'], issue['username'])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return finding
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _handle_elastic_issue(issue):
|
||||||
|
finding = \
|
||||||
|
{
|
||||||
|
"Severity": {
|
||||||
|
"Product": 10,
|
||||||
|
"Normalized": 100
|
||||||
|
},
|
||||||
|
"Resources": [{
|
||||||
|
"Type": "IpAddress",
|
||||||
|
"Id": issue['ip_address']
|
||||||
|
}],
|
||||||
|
"RecordState": "ACTIVE",
|
||||||
|
}
|
||||||
|
|
||||||
|
finding["Title"] = "Elasticsearch servers are vulnerable to CVE-2015-1427"
|
||||||
|
finding["Description"] = "Update your Elastic Search server to version 1.4.3 and up."
|
||||||
|
finding["Remediation"] = {
|
||||||
|
"Recommendation": {
|
||||||
|
"Text": "The machine {0}({1}) is vulnerable to an Elastic Groovy attack. The attack was made possible because the Elastic Search server was not patched against CVE-2015-1427.".format(issue['machine'], issue['ip_address'])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return finding
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _handle_island_cross_segment_issue(issue):
|
||||||
|
finding = \
|
||||||
|
{
|
||||||
|
"Severity": {
|
||||||
|
"Product": 1,
|
||||||
|
"Normalized": 100
|
||||||
|
},
|
||||||
|
"Resources": [{
|
||||||
|
"Type": "IpAddress",
|
||||||
|
"Id": issue['networks'][0][:-2]
|
||||||
|
}],
|
||||||
|
"RecordState": "ACTIVE",
|
||||||
|
}
|
||||||
|
|
||||||
|
finding["Title"] = "Weak segmentation - Machines from different segments are able to communicate."
|
||||||
|
finding["Description"] = "egment your network and make sure there is no communication between machines from different segments."
|
||||||
|
finding["Remediation"] = {
|
||||||
|
"Recommendation": {
|
||||||
|
"Text": "The network can probably be segmented. A monkey instance on \
|
||||||
|
{0} in the networks {1} \
|
||||||
|
could directly access the Monkey Island server in the networks {2}.".format(issue['machine'],
|
||||||
|
issue['networks'],
|
||||||
|
issue['server_networks'])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return finding
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _handle_shared_passwords_issue(issue):
|
||||||
|
finding = \
|
||||||
|
{
|
||||||
|
"Severity": {
|
||||||
|
"Product": 1,
|
||||||
|
"Normalized": 100
|
||||||
|
},
|
||||||
|
"Resources": [{
|
||||||
|
"Type": "IpAddress",
|
||||||
|
"Id": '10.0.0.1'
|
||||||
|
}],
|
||||||
|
"RecordState": "ACTIVE",
|
||||||
|
}
|
||||||
|
|
||||||
|
finding["Title"] = "Multiple users have the same password"
|
||||||
|
finding["Description"] = "Some users are sharing passwords, this should be fixed by changing passwords."
|
||||||
|
finding["Remediation"] = {
|
||||||
|
"Recommendation": {
|
||||||
|
"Text": "These users are sharing access password: {0}.".format(issue['shared_with'])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return finding
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _handle_shellshock_issue(issue):
|
||||||
|
finding = \
|
||||||
|
{
|
||||||
|
"Severity": {
|
||||||
|
"Product": 10,
|
||||||
|
"Normalized": 100
|
||||||
|
},
|
||||||
|
"Resources": [{
|
||||||
|
"Type": "IpAddress",
|
||||||
|
"Id": issue['ip_address']
|
||||||
|
}],
|
||||||
|
"RecordState": "ACTIVE",
|
||||||
|
}
|
||||||
|
|
||||||
|
finding["Title"] = "Machines are vulnerable to 'Shellshock'"
|
||||||
|
finding["Description"] = "Update your Bash to a ShellShock-patched version."
|
||||||
|
finding["Remediation"] = {
|
||||||
|
"Recommendation": {
|
||||||
|
"Text": "The machine {0} ({1}) is vulnerable to a ShellShock attack. The attack was made possible because the HTTP server running on TCP port {2} was vulnerable to a shell injection attack on the paths: {3}.".format(issue['machine'], issue['ip_address'], issue['port'], issue['paths'])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return finding
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _handle_smb_password_issue(issue):
|
||||||
|
finding = \
|
||||||
|
{
|
||||||
|
"Severity": {
|
||||||
|
"Product": 1,
|
||||||
|
"Normalized": 100
|
||||||
|
},
|
||||||
|
"Resources": [{
|
||||||
|
"Type": "IpAddress",
|
||||||
|
"Id": issue['ip_address']
|
||||||
|
}],
|
||||||
|
"RecordState": "ACTIVE",
|
||||||
|
}
|
||||||
|
|
||||||
|
finding["Title"] = "Machines are accessible using passwords supplied by the user during the Monkey's configuration."
|
||||||
|
finding["Description"] = "Change {0}'s password to a complex one-use password that is not shared with other computers on the network."
|
||||||
|
finding["Remediation"] = {
|
||||||
|
"Recommendation": {
|
||||||
|
"Text": "The machine {0} ({1}) is vulnerable to a SMB attack. The Monkey authenticated over the SMB protocol with user {2} and its password.".format(issue['machine'], issue['ip_address'], issue['username'])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return finding
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
|
|
||||||
|
|
||||||
class Exporter:
|
class Exporter:
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def handle_report(self, report_json):
|
@staticmethod
|
||||||
|
def handle_report(report_json):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
|
@ -65,5 +65,7 @@ class Root(flask_restful.Resource):
|
||||||
if not infection_done:
|
if not infection_done:
|
||||||
report_done = False
|
report_done = False
|
||||||
else:
|
else:
|
||||||
|
if is_any_exists:
|
||||||
|
ReportService.get_report()
|
||||||
report_done = ReportService.is_report_generated()
|
report_done = ReportService.is_report_generated()
|
||||||
return dict(run_server=True, run_monkey=is_any_exists, infection_done=infection_done, report_done=report_done)
|
return dict(run_server=True, run_monkey=is_any_exists, infection_done=infection_done, report_done=report_done)
|
||||||
|
|
|
@ -294,6 +294,10 @@ class NodeService:
|
||||||
def is_monkey_finished_running():
|
def is_monkey_finished_running():
|
||||||
return NodeService.is_any_monkey_exists() and not NodeService.is_any_monkey_alive()
|
return NodeService.is_any_monkey_exists() and not NodeService.is_any_monkey_alive()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_latest_modified_monkey():
|
||||||
|
return mongo.db.monkey.find({}).sort('modifytime', -1).limit(1)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def add_credentials_to_monkey(monkey_id, creds):
|
def add_credentials_to_monkey(monkey_id, creds):
|
||||||
mongo.db.monkey.update(
|
mongo.db.monkey.update(
|
||||||
|
|
|
@ -8,6 +8,8 @@ from enum import Enum
|
||||||
from six import text_type
|
from six import text_type
|
||||||
|
|
||||||
from cc.database import mongo
|
from cc.database import mongo
|
||||||
|
from cc.environment.environment import load_env_from_file, AWS
|
||||||
|
from cc.resources.aws_exporter import AWSExporter
|
||||||
from cc.services.config import ConfigService
|
from cc.services.config import ConfigService
|
||||||
from cc.services.edge import EdgeService
|
from cc.services.edge import EdgeService
|
||||||
from cc.services.node import NodeService
|
from cc.services.node import NodeService
|
||||||
|
@ -123,9 +125,9 @@ class ReportService:
|
||||||
'label': node['label'],
|
'label': node['label'],
|
||||||
'ip_addresses': node['ip_addresses'],
|
'ip_addresses': node['ip_addresses'],
|
||||||
'accessible_from_nodes':
|
'accessible_from_nodes':
|
||||||
(x['hostname'] for x in
|
list((x['hostname'] for x in
|
||||||
(NodeService.get_displayed_node_by_id(edge['from'], True)
|
(NodeService.get_displayed_node_by_id(edge['from'], True)
|
||||||
for edge in EdgeService.get_displayed_edges_by_to(node['id'], True))),
|
for edge in EdgeService.get_displayed_edges_by_to(node['id'], True)))),
|
||||||
'services': node['services']
|
'services': node['services']
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -659,26 +661,19 @@ class ReportService:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def is_report_generated():
|
def is_report_generated():
|
||||||
generated_report = mongo.db.report.find_one({'name': 'generated_report'})
|
generated_report = mongo.db.report.find_one({})
|
||||||
if generated_report is None:
|
if generated_report is None:
|
||||||
return False
|
return False
|
||||||
return generated_report['value']
|
return True
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def set_report_generated():
|
def generate_report():
|
||||||
mongo.db.report.update(
|
|
||||||
{'name': 'generated_report'},
|
|
||||||
{'$set': {'value': True}},
|
|
||||||
upsert=True)
|
|
||||||
logger.info("Report marked as generated.")
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_report():
|
|
||||||
domain_issues = ReportService.get_domain_issues()
|
domain_issues = ReportService.get_domain_issues()
|
||||||
issues = ReportService.get_issues()
|
issues = ReportService.get_issues()
|
||||||
config_users = ReportService.get_config_users()
|
config_users = ReportService.get_config_users()
|
||||||
config_passwords = ReportService.get_config_passwords()
|
config_passwords = ReportService.get_config_passwords()
|
||||||
cross_segment_issues = ReportService.get_cross_segment_issues()
|
cross_segment_issues = ReportService.get_cross_segment_issues()
|
||||||
|
monkey_latest_modify_time = list(NodeService.get_latest_modified_monkey())[0]['modifytime']
|
||||||
|
|
||||||
report = \
|
report = \
|
||||||
{
|
{
|
||||||
|
@ -710,17 +705,50 @@ class ReportService:
|
||||||
{
|
{
|
||||||
'issues': issues,
|
'issues': issues,
|
||||||
'domain_issues': domain_issues
|
'domain_issues': domain_issues
|
||||||
|
},
|
||||||
|
'meta':
|
||||||
|
{
|
||||||
|
'latest_monkey_modifytime': monkey_latest_modify_time
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ReportService.export_to_exporters(report)
|
||||||
finished_run = NodeService.is_monkey_finished_running()
|
mongo.db.report.drop()
|
||||||
if finished_run:
|
mongo.db.report.insert_one(report)
|
||||||
ReportService.set_report_generated()
|
|
||||||
|
|
||||||
return report
|
return report
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_latest_report_exists():
|
||||||
|
latest_report_doc = mongo.db.report.find_one({}, {'meta.latest_monkey_modifytime': 1})
|
||||||
|
|
||||||
|
if latest_report_doc:
|
||||||
|
report_latest_modifytime = latest_report_doc['meta']['latest_monkey_modifytime']
|
||||||
|
latest_monkey_modifytime = NodeService.get_latest_modified_monkey()[0]['modifytime']
|
||||||
|
return report_latest_modifytime == latest_monkey_modifytime
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_report():
|
||||||
|
if ReportService.is_latest_report_exists():
|
||||||
|
return mongo.db.report.find_one()
|
||||||
|
return ReportService.generate_report()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def did_exploit_type_succeed(exploit_type):
|
def did_exploit_type_succeed(exploit_type):
|
||||||
return mongo.db.edge.count(
|
return mongo.db.edge.count(
|
||||||
{'exploits': {'$elemMatch': {'exploiter': exploit_type, 'result': True}}},
|
{'exploits': {'$elemMatch': {'exploiter': exploit_type, 'result': True}}},
|
||||||
limit=1) > 0
|
limit=1) > 0
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_active_exporters():
|
||||||
|
# This function should be in another module in charge of building a list of active exporters
|
||||||
|
exporters_list = []
|
||||||
|
if load_env_from_file() == AWS:
|
||||||
|
exporters_list.append(AWSExporter)
|
||||||
|
return exporters_list
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def export_to_exporters(report):
|
||||||
|
for exporter in ReportService.get_active_exporters():
|
||||||
|
exporter.handle_report(report)
|
||||||
|
|
|
@ -14,3 +14,4 @@ netifaces
|
||||||
ipaddress
|
ipaddress
|
||||||
enum34
|
enum34
|
||||||
PyCrypto
|
PyCrypto
|
||||||
|
boto3
|
Loading…
Reference in New Issue