* 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:
maor.rayzin 2018-11-25 12:39:47 +02:00
parent c888ab7bc9
commit 148ee3f0f0
7 changed files with 345 additions and 24 deletions

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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(

View File

@ -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)

View File

@ -14,3 +14,4 @@ netifaces
ipaddress ipaddress
enum34 enum34
PyCrypto PyCrypto
boto3