Skip to content

Commit 3e08e70

Browse files
committed
Merge branch '3078-rate-limit' into develop
Issue #3078 PR #3189
2 parents 146b4c8 + caff684 commit 3e08e70

File tree

17 files changed

+368
-33
lines changed

17 files changed

+368
-33
lines changed

envs/monkey_zoo/blackbox/test_blackbox.py

+31-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import logging
22
import os
33
from http import HTTPStatus
4+
from threading import Thread
45
from time import sleep
56

67
import pytest
8+
import requests
79

810
from envs.monkey_zoo.blackbox.analyzers.communication_analyzer import CommunicationAnalyzer
911
from envs.monkey_zoo.blackbox.analyzers.zerologon_analyzer import ZerologonAnalyzer
@@ -38,6 +40,9 @@
3840
start_machines,
3941
stop_machines,
4042
)
43+
from monkey_island.cc.services.authentication_service.flask_resources.agent_otp import (
44+
MAX_OTP_REQUESTS_PER_SECOND,
45+
)
4146

4247
DEFAULT_TIMEOUT_SECONDS = 2 * 60 + 30
4348
MACHINE_BOOTUP_WAIT_SECONDS = 30
@@ -48,7 +53,11 @@
4853

4954
@pytest.fixture(autouse=True, scope="session")
5055
def GCPHandler(request, no_gcp, gcp_machines_to_start):
51-
if not no_gcp:
56+
if no_gcp:
57+
return
58+
if len(gcp_machines_to_start) == 0:
59+
LOGGER.info("No GCP machines to start.")
60+
else:
5261
LOGGER.info(f"MACHINES TO START: {gcp_machines_to_start}")
5362

5463
try:
@@ -156,6 +165,27 @@ def test_logout_invalidates_all_tokens(island):
156165
assert resp.status_code == HTTPStatus.UNAUTHORIZED
157166

158167

168+
def test_agent_otp_rate_limit(island):
169+
threads = []
170+
response_codes = []
171+
agent_otp_endpoint = f"https://{island}/api/agent-otp"
172+
173+
def make_request():
174+
response = requests.get(agent_otp_endpoint, verify=False) # noqa: DUO123
175+
response_codes.append(response.status_code)
176+
177+
for _ in range(0, MAX_OTP_REQUESTS_PER_SECOND + 1):
178+
t = Thread(target=make_request, daemon=True)
179+
t.start()
180+
threads.append(t)
181+
182+
for t in threads:
183+
t.join()
184+
185+
assert response_codes.count(HTTPStatus.OK) == MAX_OTP_REQUESTS_PER_SECOND
186+
assert response_codes.count(HTTPStatus.TOO_MANY_REQUESTS) == 1
187+
188+
159189
# NOTE: These test methods are ordered to give time for the slower zoo machines
160190
# to boot up and finish starting services.
161191
# noinspection PyUnresolvedReferences

monkey/infection_monkey/exploit/i_agent_otp_provider.py

+1
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,6 @@ def get_otp(self) -> str:
1414
Get a one-time password (OTP)
1515
1616
:return: An OTP
17+
:raises RuntimeError: If an OTP cannot be retrieved
1718
"""
1819
pass
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
from infection_monkey.island_api_client import IIslandAPIClient
1+
from time import sleep
2+
3+
from infection_monkey.island_api_client import IIslandAPIClient, IslandAPIRequestLimitExceededError
24

35
from .i_agent_otp_provider import IAgentOTPProvider
46

@@ -8,4 +10,14 @@ def __init__(self, island_api_client: IIslandAPIClient):
810
self._island_api_client = island_api_client
911

1012
def get_otp(self) -> str:
11-
return self._island_api_client.get_otp()
13+
# Note: Using too large of a delay here will cause the exploiter
14+
# threads/processes to hang
15+
for _ in range(6):
16+
try:
17+
return self._island_api_client.get_otp()
18+
except IslandAPIRequestLimitExceededError:
19+
sleep(0.5)
20+
continue
21+
except Exception as err:
22+
raise RuntimeError(err)
23+
raise RuntimeError("Failed to get OTP: Too many requests.")

monkey/infection_monkey/island_api_client/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
IslandAPIRequestError,
55
IslandAPIRequestFailedError,
66
IslandAPITimeoutError,
7+
IslandAPIAuthenticationError,
8+
IslandAPIRequestLimitExceededError,
79
)
810
from .i_island_api_client import IIslandAPIClient
911
from .abstract_island_api_client_factory import AbstractIslandAPIClientFactory

monkey/infection_monkey/island_api_client/http_island_api_client.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import functools
22
import json
33
import logging
4+
from http import HTTPStatus
45
from pprint import pformat
56
from typing import Any, Dict, List, Sequence
67

@@ -20,7 +21,11 @@
2021

2122
from . import IIslandAPIClient, IslandAPIRequestError
2223
from .http_client import HTTPClient
23-
from .island_api_client_errors import IslandAPIAuthenticationError, IslandAPIResponseParsingError
24+
from .island_api_client_errors import (
25+
IslandAPIAuthenticationError,
26+
IslandAPIRequestLimitExceededError,
27+
IslandAPIResponseParsingError,
28+
)
2429

2530
logger = logging.getLogger(__name__)
2631

@@ -116,6 +121,8 @@ def get_agent_binary(self, operating_system: OperatingSystem) -> bytes:
116121
@handle_authentication_token_expiration
117122
def get_otp(self) -> str:
118123
response = self._http_client.get("/agent-otp")
124+
if response.status_code == HTTPStatus.TOO_MANY_REQUESTS:
125+
raise IslandAPIRequestLimitExceededError("Too many requests to get OTP.")
119126
return response.json()["otp"]
120127

121128
@handle_response_parsing_errors

monkey/infection_monkey/island_api_client/i_island_api_client.py

+1
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ def get_otp(self) -> str:
6161
Island due to an issue in the request sent from the client
6262
:raises IslandAPIRequestFailedError: If an error occurs while attempting to connect to the
6363
Island due to an error on the server
64+
:raises IslandAPIRequestLimitExceededError: If the request limit for OTPs has been exceeded
6465
:raises IslandAPITimeoutError: If a timeout occurs while attempting to connect to the Island
6566
:raises IslandAPIError: If an unexpected error occurs while attempting to get an OTP
6667
:return: The OTP

monkey/infection_monkey/island_api_client/island_api_client_errors.py

+10
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,13 @@ class IslandAPIResponseParsingError(IslandAPIError):
5050
"""
5151
Raised when IslandAPIClient fails to parse the response
5252
"""
53+
54+
pass
55+
56+
57+
class IslandAPIRequestLimitExceededError(IslandAPIError):
58+
"""
59+
Raised when the API request fails due to rate limiting
60+
"""
61+
62+
pass

monkey/monkey_island/Pipfile

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ requests = ">=2.24"
1717
ring = ">=0.7.3"
1818
Flask-RESTful = ">=0.3.8"
1919
Flask = ">=1.1"
20+
Flask-Limiter = "*"
2021
Werkzeug = ">=1.0.1"
2122
pyaescrypt = "*"
2223
python-dateutil = "*"

0 commit comments

Comments
 (0)