Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

2857 security report get exploit on node fix #2876

Merged
merged 12 commits into from
Jan 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
from dataclasses import asdict

from monkey_island.cc.repositories import IAgentEventRepository, IMachineRepository
from monkey_island.cc.repositories import (
IAgentEventRepository,
IAgentPluginRepository,
IMachineRepository,
)
from monkey_island.cc.resources.AbstractResource import AbstractResource
from monkey_island.cc.resources.request_authentication import jwt_required
from monkey_island.cc.services.reporting.exploitations.monkey_exploitation import (
Expand All @@ -12,17 +16,21 @@ class MonkeyExploitation(AbstractResource):
urls = ["/api/exploitations/monkey"]

def __init__(
self, event_repository: IAgentEventRepository, machine_repository: IMachineRepository
self,
event_repository: IAgentEventRepository,
machine_repository: IMachineRepository,
agent_plugin_repository: IAgentPluginRepository,
):
self._event_repository = event_repository
self._machine_repository = machine_repository
self._agent_plugin_repository = agent_plugin_repository

@jwt_required
def get(self):
monkey_exploitations = [
asdict(exploitation)
for exploitation in get_monkey_exploited(
self._event_repository, self._machine_repository
self._event_repository, self._machine_repository, self._agent_plugin_repository
)
]
return {"monkey_exploitations": monkey_exploitations}
14 changes: 11 additions & 3 deletions monkey/monkey_island/cc/resources/ransomware_report.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
from flask import jsonify

from monkey_island.cc.repositories import IAgentEventRepository, IMachineRepository
from monkey_island.cc.repositories import (
IAgentEventRepository,
IAgentPluginRepository,
IMachineRepository,
)
from monkey_island.cc.resources.AbstractResource import AbstractResource
from monkey_island.cc.resources.request_authentication import jwt_required
from monkey_island.cc.services.ransomware import ransomware_report
Expand All @@ -10,17 +14,21 @@ class RansomwareReport(AbstractResource):
urls = ["/api/report/ransomware"]

def __init__(
self, event_repository: IAgentEventRepository, machine_repository: IMachineRepository
self,
event_repository: IAgentEventRepository,
machine_repository: IMachineRepository,
agent_plugin_repository: IAgentPluginRepository,
):
self._event_repository = event_repository
self._machine_repository = machine_repository
self._agent_plugin_repository = agent_plugin_repository

@jwt_required
def get(self):
return jsonify(
{
"propagation_stats": ransomware_report.get_propagation_stats(
self._event_repository, self._machine_repository
self._event_repository, self._machine_repository, self._agent_plugin_repository
),
}
)
1 change: 1 addition & 0 deletions monkey/monkey_island/cc/services/initialize.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ def initialize_services(container: DIContainer, data_dir: Path):
container.resolve(IAgentEventRepository),
container.resolve(IMachineRepository),
container.resolve(INodeRepository),
container.resolve(IAgentPluginRepository),
)


Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
from typing import Dict, List

from monkey_island.cc.repositories import IAgentEventRepository, IMachineRepository
from monkey_island.cc.repositories import (
IAgentEventRepository,
IAgentPluginRepository,
IMachineRepository,
)
from monkey_island.cc.services.reporting.exploitations.monkey_exploitation import (
MonkeyExploitation,
get_monkey_exploited,
Expand All @@ -11,9 +15,10 @@
def get_propagation_stats(
event_repository: IAgentEventRepository,
machine_repository: IMachineRepository,
agent_plugin_repository: IAgentPluginRepository,
) -> Dict:
scanned = ReportService.get_scanned()
exploited = get_monkey_exploited(event_repository, machine_repository)
exploited = get_monkey_exploited(event_repository, machine_repository, agent_plugin_repository)

return {
"num_scanned_nodes": len(scanned),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import logging
from dataclasses import dataclass
from typing import List, Sequence
from typing import Dict, List, Sequence

from common.agent_events import ExploitationEvent
from common.agent_plugins import AgentPluginManifest, AgentPluginType
from common.hard_coded_exploiter_manifests import HARD_CODED_EXPLOITER_MANIFESTS
from monkey_island.cc.models import Machine
from monkey_island.cc.repositories import IAgentEventRepository, IMachineRepository
from monkey_island.cc.services.reporting.issue_processing.exploit_processing.exploiter_descriptor_enum import ( # noqa: E501
ExploiterDescriptorEnum,
from monkey_island.cc.repositories import (
IAgentEventRepository,
IAgentPluginRepository,
IMachineRepository,
)

logger = logging.getLogger(__name__)
Expand All @@ -23,9 +26,11 @@ class MonkeyExploitation:
def get_monkey_exploited(
event_repository: IAgentEventRepository,
machine_repository: IMachineRepository,
agent_plugin_repository: IAgentPluginRepository,
) -> List[MonkeyExploitation]:
exploits = event_repository.get_events_by_type(ExploitationEvent)
successful_exploits = [e for e in exploits if e.success]
plugin_manifests = agent_plugin_repository.get_all_plugin_manifests()

exploited_machines = {
machine_repository.get_machines_by_ip(e.target)[0] for e in successful_exploits
Expand All @@ -36,28 +41,38 @@ def get_monkey_exploited(
label=str(machine.network_interfaces[0].ip),
ip_addresses=[str(iface.ip) for iface in machine.network_interfaces],
domain_name="",
exploits=get_exploits_used_on_node(machine, successful_exploits),
exploits=get_exploits_used_on_node(machine, successful_exploits, plugin_manifests),
)
for machine in exploited_machines
]

logger.info("Exploited nodes generated for reporting")

return exploited


def get_exploits_used_on_node(
machine: Machine,
successful_exploits: Sequence[ExploitationEvent],
plugin_manifests: Dict[AgentPluginType, Dict[str, AgentPluginManifest]],
) -> List[str]:
machine_ips = [iface.ip for iface in machine.network_interfaces]
machine_exploits = (e for e in successful_exploits if e.target in machine_ips)
return list(
set(
[
ExploiterDescriptorEnum.get_by_class_name(exploit.exploiter_name).display_name
for exploit in machine_exploits
if exploit.success
]
)
)
successful_exploits = [e for e in successful_exploits if e.target in machine_ips and e.success]

plugin_exploiter_manifests = plugin_manifests.get(AgentPluginType.EXPLOITER, {})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be best to somehow add the hard-coded manifests to the repository. Otherwise the whole codebase needs to know what is hard-coded and what is not or that "hard-coded" plugins is even a thing

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would require more work. We can discuss it over webex and decide. All in all, hard-coded exploiters will go away at some point so I don't know if it is worth it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. They'll go away so it's not worth the effort.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we add a decorator to the plugin repository that would return hard-coded plugins if a plugin is missing it would allow us to simplify some code. This, we wouldn't need to add them in the AgentConfigurationSchemaCompiler, etc. Deleting a single decorator is easier than going through the whole island code base to remove HARD_CODED_PLUGIN references.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deleting a single decorator is easier than going through the whole island code base to remove HARD_CODED_PLUGIN references.

I agree, but it would introduce the case where I can ask the repository for all manifests and get a manifest for a plugin that doesn't exist. Theoretically this shouldn't be a problem, but it might be if I use the set of all manifests to query for specific plugins. Then I'll have to handle the case where the plugin doesn't exist. Maybe we'll need to handle that in the long run anyway, I'm not sure.

plugin_exploiter_manifests.update(HARD_CODED_EXPLOITER_MANIFESTS)

exploiter_titles = set()

for exploit in successful_exploits:
successful_exploiter_manifest = plugin_exploiter_manifests.get(exploit.exploiter_name)

if not successful_exploiter_manifest:
logger.warning(f"Could not find plugin manifest for exploiter {exploit.exploiter_name}")
continue

if successful_exploiter_manifest.title:
exploiter_titles.add(successful_exploiter_manifest.title)
else:
exploiter_titles.add(successful_exploiter_manifest.name)

# AgentPluginManifest title is Optional[str], list expects Iterable[str]
return list(exploiter_titles) # type: ignore[arg-type]
8 changes: 7 additions & 1 deletion monkey/monkey_island/cc/services/reporting/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from monkey_island.cc.repositories import (
IAgentConfigurationRepository,
IAgentEventRepository,
IAgentPluginRepository,
IAgentRepository,
IMachineRepository,
INodeRepository,
Expand Down Expand Up @@ -66,6 +67,7 @@ class ReportService:
_agent_event_repository: Optional[IAgentEventRepository] = None
_machine_repository: Optional[IMachineRepository] = None
_node_repository: Optional[INodeRepository] = None
_agent_plugin_repository: Optional[IAgentPluginRepository] = None
_report: Dict[str, Dict] = {}
_report_generation_lock: Lock = Lock()

Expand All @@ -80,12 +82,14 @@ def initialize(
agent_event_repository: IAgentEventRepository,
machine_repository: IMachineRepository,
node_repository: INodeRepository,
agent_plugin_repository: IAgentPluginRepository,
):
cls._agent_repository = agent_repository
cls._agent_configuration_repository = agent_configuration_repository
cls._agent_event_repository = agent_event_repository
cls._machine_repository = machine_repository
cls._node_repository = node_repository
cls._agent_plugin_repository = agent_plugin_repository

# This should pull from Simulation entity
@classmethod
Expand Down Expand Up @@ -489,7 +493,9 @@ def generate_report(cls):

scanned_nodes = ReportService.get_scanned()
exploited_cnt = len(
get_monkey_exploited(cls._agent_event_repository, cls._machine_repository)
get_monkey_exploited(
cls._agent_event_repository, cls._machine_repository, cls._agent_plugin_repository
)
)
return {
"overview": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

import pytest

from monkey_island.cc.repositories import IAgentEventRepository, IMachineRepository
from monkey_island.cc.repositories import (
IAgentEventRepository,
IAgentPluginRepository,
IMachineRepository,
)
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
Expand All @@ -20,6 +24,11 @@ def machine_repository() -> IMachineRepository:
return repository


@pytest.fixture
def agent_plugin_repository() -> IAgentPluginRepository:
return MagicMock(spec=IAgentPluginRepository)


@pytest.fixture
def patch_report_service_for_stats(monkeypatch):
TEST_SCANNED_RESULTS = [{}, {}, {}, {}]
Expand All @@ -31,30 +40,36 @@ def patch_report_service_for_stats(monkeypatch):

monkeypatch.setattr(ReportService, "get_scanned", lambda: TEST_SCANNED_RESULTS)
monkeypatch.setattr(
ransomware_report, "get_monkey_exploited", lambda e, m: TEST_EXPLOITED_RESULTS
ransomware_report, "get_monkey_exploited", lambda e, m, p: TEST_EXPLOITED_RESULTS
)


def test_get_propagation_stats__num_scanned(
patch_report_service_for_stats, event_repository, machine_repository
patch_report_service_for_stats, event_repository, machine_repository, agent_plugin_repository
):
stats = ransomware_report.get_propagation_stats(event_repository, machine_repository)
stats = ransomware_report.get_propagation_stats(
event_repository, machine_repository, agent_plugin_repository
)

assert stats["num_scanned_nodes"] == 4


def test_get_propagation_stats__num_exploited(
patch_report_service_for_stats, event_repository, machine_repository
patch_report_service_for_stats, event_repository, machine_repository, agent_plugin_repository
):
stats = ransomware_report.get_propagation_stats(event_repository, machine_repository)
stats = ransomware_report.get_propagation_stats(
event_repository, machine_repository, agent_plugin_repository
)

assert stats["num_exploited_nodes"] == 3


def test_get_propagation_stats__num_exploited_per_exploit(
patch_report_service_for_stats, event_repository, machine_repository
patch_report_service_for_stats, event_repository, machine_repository, agent_plugin_repository
):
stats = ransomware_report.get_propagation_stats(event_repository, machine_repository)
stats = ransomware_report.get_propagation_stats(
event_repository, machine_repository, agent_plugin_repository
)

assert stats["num_exploited_per_exploit"]["SSH Exploiter"] == 2
assert stats["num_exploited_per_exploit"]["SMB Exploiter"] == 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from common import OperatingSystem
from common.agent_events import ExploitationEvent
from common.agent_plugins import AgentPluginManifest, AgentPluginType
from monkey_island.cc.models import Machine
from monkey_island.cc.services.reporting.exploitations.monkey_exploitation import (
get_exploits_used_on_node,
Expand Down Expand Up @@ -37,6 +38,23 @@
exploiter_name="ZerologonExploiter",
)

EVENT_4 = ExploitationEvent(
source=AGENT_ID,
timestamp=TIMESTAMP,
target=IPv4Address(TARGET_IP_STR),
success=True,
exploiter_name="MockExploiter",
)

EVENT_5 = ExploitationEvent(
source=AGENT_ID,
timestamp=TIMESTAMP,
target=IPv4Address(TARGET_IP_STR),
success=True,
exploiter_name="Mock1Exploiter",
)


MACHINE = Machine(
id=1,
hardware_id=101,
Expand All @@ -45,17 +63,62 @@
operating_system=OperatingSystem.WINDOWS,
)

PLUGIN_MANIFESTS = {
AgentPluginType.EXPLOITER: {
"MockExploiter": AgentPluginManifest(
name="MockExploiter",
plugin_type=AgentPluginType.EXPLOITER,
title="Mock Exploiter",
target_operating_systems=(OperatingSystem.WINDOWS,),
description="Mocked description",
link_to_documentation="http://no_mocked.com",
safe=True,
),
"Mock1Exploiter": AgentPluginManifest(
name="Mock1Exploiter",
plugin_type=AgentPluginType.EXPLOITER,
title=None,
target_operating_systems=(OperatingSystem.WINDOWS,),
description="Another Mocked description",
link_to_documentation="http://nopenope.com",
safe=True,
),
}
}


def test_get_exploits_used_on_node__2_exploits():
exploits = get_exploits_used_on_node(MACHINE, [EVENT_1, EVENT_2])
exploits = get_exploits_used_on_node(MACHINE, [EVENT_1, EVENT_2], PLUGIN_MANIFESTS)
assert sorted(exploits) == sorted(["SSH Exploiter", "Log4Shell Exploiter"])


def test_get_exploits_used_on_node__duplicate_exploits():
exploits = get_exploits_used_on_node(MACHINE, [EVENT_1, EVENT_1])
exploits = get_exploits_used_on_node(MACHINE, [EVENT_1, EVENT_1], PLUGIN_MANIFESTS)
assert exploits == ["SSH Exploiter"]


def test_get_exploits_used_on_node__returns_only_exploits_for_node():
exploits = get_exploits_used_on_node(MACHINE, [EVENT_1, EVENT_2, EVENT_3])
exploits = get_exploits_used_on_node(MACHINE, [EVENT_1, EVENT_2, EVENT_3], PLUGIN_MANIFESTS)
assert sorted(exploits) == sorted(["SSH Exploiter", "Log4Shell Exploiter"])


def test_get_exploits_used_on_node__duplicate_plugin_exploits():
exploits = get_exploits_used_on_node(MACHINE, [EVENT_4, EVENT_4], PLUGIN_MANIFESTS)
assert exploits == ["Mock Exploiter"]


def test_get_exploits_used_on_node__mixed_exploits():
exploits = get_exploits_used_on_node(
MACHINE, [EVENT_1, EVENT_2, EVENT_3, EVENT_4], PLUGIN_MANIFESTS
)
assert sorted(exploits) == sorted(["SSH Exploiter", "Log4Shell Exploiter", "Mock Exploiter"])


def test_get_exploits_used_on_node__empty_plugin_manifests():
exploits = get_exploits_used_on_node(MACHINE, [EVENT_1, EVENT_2, EVENT_3], {})
assert sorted(exploits) == sorted(["SSH Exploiter", "Log4Shell Exploiter"])


def test_get_exploits_used_on_node__empty_title():
exploits = get_exploits_used_on_node(MACHINE, [EVENT_5], PLUGIN_MANIFESTS)
assert sorted(exploits) == sorted(["Mock1Exploiter"])
Loading