* 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__)
|
||||
|
||||
AWS = 'aws'
|
||||
STANDARD = 'standard'
|
||||
|
||||
ENV_DICT = {
|
||||
'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):
|
||||
|
||||
def __init__(self):
|
||||
Exporter.__init__(self)
|
||||
@staticmethod
|
||||
def handle_report(report_json):
|
||||
|
||||
def handle_report(self, report_json):
|
||||
pass
|
||||
findings_list = []
|
||||
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:
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def handle_report(self, report_json):
|
||||
@staticmethod
|
||||
def handle_report(report_json):
|
||||
raise NotImplementedError
|
||||
|
|
|
@ -65,5 +65,7 @@ class Root(flask_restful.Resource):
|
|||
if not infection_done:
|
||||
report_done = False
|
||||
else:
|
||||
if is_any_exists:
|
||||
ReportService.get_report()
|
||||
report_done = ReportService.is_report_generated()
|
||||
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():
|
||||
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
|
||||
def add_credentials_to_monkey(monkey_id, creds):
|
||||
mongo.db.monkey.update(
|
||||
|
|
|
@ -8,6 +8,8 @@ from enum import Enum
|
|||
from six import text_type
|
||||
|
||||
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.edge import EdgeService
|
||||
from cc.services.node import NodeService
|
||||
|
@ -123,9 +125,9 @@ class ReportService:
|
|||
'label': node['label'],
|
||||
'ip_addresses': node['ip_addresses'],
|
||||
'accessible_from_nodes':
|
||||
(x['hostname'] for x in
|
||||
list((x['hostname'] for x in
|
||||
(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']
|
||||
})
|
||||
|
||||
|
@ -659,26 +661,19 @@ class ReportService:
|
|||
|
||||
@staticmethod
|
||||
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:
|
||||
return False
|
||||
return generated_report['value']
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def set_report_generated():
|
||||
mongo.db.report.update(
|
||||
{'name': 'generated_report'},
|
||||
{'$set': {'value': True}},
|
||||
upsert=True)
|
||||
logger.info("Report marked as generated.")
|
||||
|
||||
@staticmethod
|
||||
def get_report():
|
||||
def generate_report():
|
||||
domain_issues = ReportService.get_domain_issues()
|
||||
issues = ReportService.get_issues()
|
||||
config_users = ReportService.get_config_users()
|
||||
config_passwords = ReportService.get_config_passwords()
|
||||
cross_segment_issues = ReportService.get_cross_segment_issues()
|
||||
monkey_latest_modify_time = list(NodeService.get_latest_modified_monkey())[0]['modifytime']
|
||||
|
||||
report = \
|
||||
{
|
||||
|
@ -710,17 +705,50 @@ class ReportService:
|
|||
{
|
||||
'issues': issues,
|
||||
'domain_issues': domain_issues
|
||||
},
|
||||
'meta':
|
||||
{
|
||||
'latest_monkey_modifytime': monkey_latest_modify_time
|
||||
}
|
||||
}
|
||||
|
||||
finished_run = NodeService.is_monkey_finished_running()
|
||||
if finished_run:
|
||||
ReportService.set_report_generated()
|
||||
ReportService.export_to_exporters(report)
|
||||
mongo.db.report.drop()
|
||||
mongo.db.report.insert_one(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
|
||||
def did_exploit_type_succeed(exploit_type):
|
||||
return mongo.db.edge.count(
|
||||
{'exploits': {'$elemMatch': {'exploiter': exploit_type, 'result': True}}},
|
||||
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
|
||||
enum34
|
||||
PyCrypto
|
||||
boto3
|
Loading…
Reference in New Issue