Island: extract monkey exploitations into a separate service and a separate endpoint

This change not only removes complexity from the huge report service, but also allows different UI components to call the API without forcing the whole report to be generated
This commit is contained in:
VakarisZ 2021-07-28 12:13:37 +03:00
parent 1013347b3c
commit 1448bb1850
11 changed files with 172 additions and 112 deletions

View File

@ -25,6 +25,7 @@ from monkey_island.cc.resources.configuration_import import ConfigurationImport
from monkey_island.cc.resources.edge import Edge
from monkey_island.cc.resources.environment import Environment
from monkey_island.cc.resources.exploitations.manual_exploitation import ManualExploitation
from monkey_island.cc.resources.exploitations.monkey_exploitation import MonkeyExploitation
from monkey_island.cc.resources.island_configuration import IslandConfiguration
from monkey_island.cc.resources.island_logs import IslandLog
from monkey_island.cc.resources.island_mode import IslandMode
@ -156,6 +157,7 @@ def init_api_resources(api):
api.add_resource(AttackReport, "/api/report/attack")
api.add_resource(RansomwareReport, "/api/report/ransomware")
api.add_resource(ManualExploitation, "/api/exploitations/manual")
api.add_resource(MonkeyExploitation, "/api/exploitations/monkey")
api.add_resource(ZeroTrustFindingEvent, "/api/zero-trust/finding-event/<string:finding_id>")
api.add_resource(TelemetryFeed, "/api/telemetry-feed", "/api/telemetry-feed/")

View File

@ -0,0 +1,13 @@
import flask_restful
from monkey_island.cc.resources.auth.auth import jwt_required
from monkey_island.cc.services.reporting.exploitations.monkey_exploitation import (
get_monkey_exploited,
)
class MonkeyExploitation(flask_restful.Resource):
@jwt_required
def get(self):
monkey_exploitations = [exploitation.__dict__ for exploitation in get_monkey_exploited()]
return {"monkey_exploitations": monkey_exploitations}

View File

@ -1,11 +1,15 @@
from typing import Dict, List
from monkey_island.cc.services.reporting.exploitations.monkey_exploitation import (
MonkeyExploitation,
get_monkey_exploited,
)
from monkey_island.cc.services.reporting.report import ReportService
def get_propagation_stats() -> Dict:
scanned = ReportService.get_scanned()
exploited = ReportService.get_exploited()
exploited = get_monkey_exploited()
return {
"num_scanned_nodes": len(scanned),
@ -14,11 +18,11 @@ def get_propagation_stats() -> Dict:
}
def _get_exploit_counts(exploited: List[Dict]) -> Dict:
def _get_exploit_counts(exploited: List[MonkeyExploitation]) -> Dict:
exploit_counts = {}
for node in exploited:
for exploit in node["exploits"]:
for exploit in node.exploits:
exploit_counts[exploit] = exploit_counts.get(exploit, 0) + 1
return exploit_counts

View File

@ -0,0 +1,60 @@
import logging
from dataclasses import dataclass
from typing import List
from monkey_island.cc.database import mongo
from monkey_island.cc.services.node import NodeService
from monkey_island.cc.services.reporting.issue_processing.exploit_processing.exploiter_descriptor_enum import ( # noqa: E501
ExploiterDescriptorEnum,
)
logger = logging.getLogger(__name__)
@dataclass
class MonkeyExploitation:
label: str
ip_addresses: List[str]
domain_name: str
exploits: List[str]
def get_monkey_exploited() -> List[MonkeyExploitation]:
exploited_with_monkeys = [
NodeService.get_displayed_node_by_id(monkey["_id"], True)
for monkey in mongo.db.monkey.find({}, {"_id": 1})
if not NodeService.get_monkey_manual_run(NodeService.get_monkey_by_id(monkey["_id"]))
]
exploited_without_monkeys = [
NodeService.get_displayed_node_by_id(node["_id"], True)
for node in mongo.db.node.find({"exploited": True}, {"_id": 1})
]
exploited = exploited_with_monkeys + exploited_without_monkeys
exploited = [
MonkeyExploitation(
label=exploited_node["label"],
ip_addresses=exploited_node["ip_addresses"],
domain_name=exploited_node["domain_name"],
exploits=get_exploits_used_on_node(exploited_node),
)
for exploited_node in exploited
]
logger.info("Exploited nodes generated for reporting")
return exploited
def get_exploits_used_on_node(node: dict) -> List[str]:
return list(
set(
[
ExploiterDescriptorEnum.get_by_class_name(exploit["exploiter"]).display_name
for exploit in node["exploits"]
if exploit["result"]
]
)
)

View File

@ -21,8 +21,11 @@ from monkey_island.cc.services.config import ConfigService
from monkey_island.cc.services.configuration.utils import (
get_config_network_segments_as_subnet_groups,
)
from monkey_island.cc.services.exploitations.manual_exploitation import get_manual_monkeys
from monkey_island.cc.services.node import NodeService
from monkey_island.cc.services.reporting.exploitations.manual_exploitation import get_manual_monkeys
from monkey_island.cc.services.reporting.exploitations.monkey_exploitation import (
get_monkey_exploited,
)
from monkey_island.cc.services.reporting.issue_processing.exploit_processing.exploiter_descriptor_enum import ( # noqa: E501
ExploiterDescriptorEnum,
)
@ -150,47 +153,6 @@ class ReportService:
nodes = nodes_without_monkeys + nodes_with_monkeys
return nodes
@staticmethod
def get_exploited():
exploited_with_monkeys = [
NodeService.get_displayed_node_by_id(monkey["_id"], True)
for monkey in mongo.db.monkey.find({}, {"_id": 1})
if not NodeService.get_monkey_manual_run(NodeService.get_monkey_by_id(monkey["_id"]))
]
exploited_without_monkeys = [
NodeService.get_displayed_node_by_id(node["_id"], True)
for node in mongo.db.node.find({"exploited": True}, {"_id": 1})
]
exploited = exploited_with_monkeys + exploited_without_monkeys
exploited = [
{
"label": exploited_node["label"],
"ip_addresses": exploited_node["ip_addresses"],
"domain_name": exploited_node["domain_name"],
"exploits": ReportService.get_exploits_used_on_node(exploited_node),
}
for exploited_node in exploited
]
logger.info("Exploited nodes generated for reporting")
return exploited
@staticmethod
def get_exploits_used_on_node(node: dict) -> List[str]:
return list(
set(
[
ExploiterDescriptorEnum.get_by_class_name(exploit["exploiter"]).display_name
for exploit in node["exploits"]
if exploit["result"]
]
)
)
@staticmethod
def get_stolen_creds():
creds = []
@ -648,7 +610,7 @@ class ReportService:
monkey_latest_modify_time = Monkey.get_latest_modifytime()
scanned_nodes = ReportService.get_scanned()
exploited_nodes = ReportService.get_exploited()
exploited_nodes = get_monkey_exploited()
report = {
"overview": {
"manual_monkeys": ReportService.get_manual_monkey_hostnames(),

View File

@ -566,7 +566,7 @@ class ReportPageComponent extends AuthComponent {
</div>
<div style={{marginBottom: '20px'}}>
<BreachedServers data={this.state.report.glance.exploited}/>
<BreachedServers />
</div>
<div style={{marginBottom: '20px'}}>

View File

@ -1,50 +0,0 @@
import React from 'react';
import ReactTable from 'react-table';
import Pluralize from 'pluralize';
import {renderArray, renderIpAddresses} from '../common/RenderArrays';
const columns = [
{
Header: 'Breached Servers',
columns: [
{Header: 'Machine', accessor: 'label'},
{
Header: 'IP Addresses', id: 'ip_addresses',
accessor: x => renderIpAddresses(x)
},
{Header: 'Exploits', id: 'exploits', accessor: x => renderArray(x.exploits)}
]
}
];
const pageSize = 10;
class BreachedServersComponent extends React.Component {
constructor(props) {
super(props);
}
render() {
let defaultPageSize = this.props.data.length > pageSize ? pageSize : this.props.data.length;
let showPagination = this.props.data.length > pageSize;
return (
<>
<p>
The Monkey successfully breached <span
className="badge badge-danger">{this.props.data.length}</span> {Pluralize('machine', this.props.data.length)}:
</p>
<div className="data-table-container">
<ReactTable
columns={columns}
data={this.props.data}
showPagination={showPagination}
defaultPageSize={defaultPageSize}
/>
</div>
</>
);
}
}
export default BreachedServersComponent;

View File

@ -0,0 +1,59 @@
import React, {useEffect, useState} from 'react';
import ReactTable from 'react-table';
import Pluralize from 'pluralize';
import {renderArray, renderIpAddresses} from '../common/RenderArrays';
import LoadingIcon from '../../ui-components/LoadingIcon';
import IslandHttpClient from '../../IslandHttpClient';
const columns = [
{
Header: 'Breached Servers',
columns: [
{Header: 'Machine', accessor: 'label'},
{
Header: 'IP Addresses', id: 'ip_addresses',
accessor: x => renderIpAddresses(x)
},
{Header: 'Exploits', id: 'exploits', accessor: x => renderArray(x.exploits)}
]
}
];
const pageSize = 10;
function BreachedServersComponent() {
const [exploitations, setExploitations] = useState(null);
useEffect(() => {
IslandHttpClient.get('/api/exploitations/monkey')
.then(res => setExploitations(res.body['monkey_exploitations']))
}, []);
if(exploitations === null){
return <LoadingIcon />
}
let defaultPageSize = exploitations.length > pageSize ? pageSize : exploitations.length;
let showPagination = exploitations.length > pageSize;
return (
<>
<p>
The Monkey successfully breached <span
className="badge badge-danger">{exploitations.length}</span> {Pluralize('machine', exploitations.length)}:
</p>
<div className="data-table-container">
<ReactTable
columns={columns}
data={exploitations}
showPagination={showPagination}
defaultPageSize={defaultPageSize}
/>
</div>
</>
);
}
export default BreachedServersComponent;

View File

@ -1,6 +1,7 @@
import pytest
from monkey_island.cc.services.ransomware import ransomware_report
from monkey_island.cc.services.reporting.exploitations.monkey_exploitation import MonkeyExploitation
from monkey_island.cc.services.reporting.report import ReportService
@ -8,13 +9,13 @@ from monkey_island.cc.services.reporting.report import ReportService
def patch_report_service_for_stats(monkeypatch):
TEST_SCANNED_RESULTS = [{}, {}, {}, {}]
TEST_EXPLOITED_RESULTS = [
{"exploits": ["SSH Exploiter"]},
{"exploits": ["SSH Exploiter", "SMB Exploiter"]},
{"exploits": ["WMI Exploiter"]},
MonkeyExploitation("", [], "", exploits=["SSH Exploiter"]),
MonkeyExploitation("", [], "", exploits=["SSH Exploiter", "SMB Exploiter"]),
MonkeyExploitation("", [], "", exploits=["WMI Exploiter"]),
]
monkeypatch.setattr(ReportService, "get_scanned", lambda: TEST_SCANNED_RESULTS)
monkeypatch.setattr(ReportService, "get_exploited", lambda: TEST_EXPLOITED_RESULTS)
monkeypatch.setattr(ransomware_report, "get_monkey_exploited", lambda: TEST_EXPLOITED_RESULTS)
def test_get_propagation_stats__num_scanned(patch_report_service_for_stats):

View File

@ -0,0 +1,20 @@
from tests.unit_tests.monkey_island.cc.services.reporting.test_report import (
NODE_DICT,
NODE_DICT_DUPLICATE_EXPLOITS,
NODE_DICT_FAILED_EXPLOITS,
)
from monkey_island.cc.services.reporting.exploitations.monkey_exploitation import (
get_exploits_used_on_node,
)
def test_get_exploits_used_on_node():
exploits = get_exploits_used_on_node(NODE_DICT)
assert sorted(exploits) == sorted(["Elastic Groovy Exploiter", "Drupal Server Exploiter"])
exploits = get_exploits_used_on_node(NODE_DICT_DUPLICATE_EXPLOITS)
assert exploits == ["Drupal Server Exploiter"]
exploits = get_exploits_used_on_node(NODE_DICT_FAILED_EXPLOITS)
assert exploits == []

View File

@ -164,14 +164,3 @@ def test_get_stolen_creds_no_creds(fake_mongo):
expected_stolen_creds_no_creds = []
assert expected_stolen_creds_no_creds == stolen_creds_no_creds
def test_get_exploits_used_on_node():
exploits = ReportService.get_exploits_used_on_node(NODE_DICT)
assert sorted(exploits) == sorted(["Elastic Groovy Exploiter", "Drupal Server Exploiter"])
exploits = ReportService.get_exploits_used_on_node(NODE_DICT_DUPLICATE_EXPLOITS)
assert exploits == ["Drupal Server Exploiter"]
exploits = ReportService.get_exploits_used_on_node(NODE_DICT_FAILED_EXPLOITS)
assert exploits == []