Add Task 5: Security Incident Response (DDoS detection and mitigation)
Browse files- api.py +17 -1
- data/runbooks/security_incident.md +23 -0
- env.py +2 -1
- models.py +2 -0
- openenv.yaml +14 -0
- server/app.py +17 -1
- tasks/__init__.py +2 -1
- tasks/task_security.py +220 -0
api.py
CHANGED
|
@@ -25,7 +25,7 @@ app.add_middleware(
|
|
| 25 |
allow_headers=["*"],
|
| 26 |
)
|
| 27 |
|
| 28 |
-
VALID_TASKS = ("easy", "medium", "hard", "bonus")
|
| 29 |
_env: Optional[DevOpsIncidentEnv] = None
|
| 30 |
|
| 31 |
|
|
@@ -95,6 +95,7 @@ def dashboard():
|
|
| 95 |
.medium {{ background: #3a2a1a; color: #ff9800; }}
|
| 96 |
.hard {{ background: #3a1a1a; color: #f44336; }}
|
| 97 |
.bonus {{ background: #1a1a3a; color: #9c27b0; }}
|
|
|
|
| 98 |
.endpoints {{ background: #1a1d27; border: 1px solid #2d3148; border-radius: 8px; padding: 1.25rem; margin-bottom: 2rem; }}
|
| 99 |
.endpoints h3 {{ margin: 0 0 1rem; color: #fff; }}
|
| 100 |
.endpoint {{ display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.5rem; }}
|
|
@@ -133,6 +134,11 @@ def dashboard():
|
|
| 133 |
<h3>Dual Simultaneous Failure</h3>
|
| 134 |
<p>Two independent failures at once. Both must be fixed for full credit. Max 25 steps.</p>
|
| 135 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
</div>
|
| 137 |
|
| 138 |
<div class="endpoints">
|
|
@@ -254,6 +260,16 @@ def list_tasks():
|
|
| 254 |
"model reload CPU loop on ml-inference. Both must be fixed for full credit."
|
| 255 |
),
|
| 256 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 257 |
]
|
| 258 |
}
|
| 259 |
|
|
|
|
| 25 |
allow_headers=["*"],
|
| 26 |
)
|
| 27 |
|
| 28 |
+
VALID_TASKS = ("easy", "medium", "hard", "bonus", "security")
|
| 29 |
_env: Optional[DevOpsIncidentEnv] = None
|
| 30 |
|
| 31 |
|
|
|
|
| 95 |
.medium {{ background: #3a2a1a; color: #ff9800; }}
|
| 96 |
.hard {{ background: #3a1a1a; color: #f44336; }}
|
| 97 |
.bonus {{ background: #1a1a3a; color: #9c27b0; }}
|
| 98 |
+
.security {{ background: #3a1a1a; color: #ff5252; }}
|
| 99 |
.endpoints {{ background: #1a1d27; border: 1px solid #2d3148; border-radius: 8px; padding: 1.25rem; margin-bottom: 2rem; }}
|
| 100 |
.endpoints h3 {{ margin: 0 0 1rem; color: #fff; }}
|
| 101 |
.endpoint {{ display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.5rem; }}
|
|
|
|
| 134 |
<h3>Dual Simultaneous Failure</h3>
|
| 135 |
<p>Two independent failures at once. Both must be fixed for full credit. Max 25 steps.</p>
|
| 136 |
</div>
|
| 137 |
+
<div class="task">
|
| 138 |
+
<span class="badge security">SECURITY</span>
|
| 139 |
+
<h3>Security Incident (DDoS)</h3>
|
| 140 |
+
<p>Botnet DDoS and credential stuffing attack. Requires traffic blocking and security escalation. Max 20 steps.</p>
|
| 141 |
+
</div>
|
| 142 |
</div>
|
| 143 |
|
| 144 |
<div class="endpoints">
|
|
|
|
| 260 |
"model reload CPU loop on ml-inference. Both must be fixed for full credit."
|
| 261 |
),
|
| 262 |
},
|
| 263 |
+
{
|
| 264 |
+
"id": "security",
|
| 265 |
+
"name": "Security Incident (DDoS)",
|
| 266 |
+
"difficulty": "hard",
|
| 267 |
+
"max_steps": 20,
|
| 268 |
+
"description": (
|
| 269 |
+
"A botnet is performing a DDoS and credential stuffing attack against the login endpoint. "
|
| 270 |
+
"The agent must read access logs, diagnose the attack IP range, block the CIDR, and alert the security team."
|
| 271 |
+
),
|
| 272 |
+
},
|
| 273 |
]
|
| 274 |
}
|
| 275 |
|
data/runbooks/security_incident.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Security Incident Response
|
| 2 |
+
|
| 3 |
+
This runbook outlines the recommended procedure for handling suspected security incidents, particularly Distributed Denial of Service (DDoS) and automated credential stuffing attacks.
|
| 4 |
+
|
| 5 |
+
## 1. Identify the Attack
|
| 6 |
+
Not all traffic spikes are legitimate user traffic. Distinguish between a spike and an attack:
|
| 7 |
+
- **Traffic Spike:** Sustained increase across multiple endpoints, proportional to user growth or a marketing event.
|
| 8 |
+
- **DDoS/Botnet Attack:** Extreme spikes (e.g. 10x-100x normal baseline) often originating from a single IP range or targeting a single endpoint (like `/api/v1/login`).
|
| 9 |
+
|
| 10 |
+
## 2. Reading Access Logs
|
| 11 |
+
Investigate the `api-gateway` logs to identify malicious patterns:
|
| 12 |
+
- Check for high volume requests returning 429 (Rate Limited) or 401/403.
|
| 13 |
+
- Identify common IP subnets. E.g. `185.x.x.x` appearing thousands of times per second.
|
| 14 |
+
- Check downstream `auth-service` logs for failed logins with `NULL` user IDs or recycled passwords (Credential Stuffing).
|
| 15 |
+
|
| 16 |
+
## 3. Mitigation Strategies
|
| 17 |
+
Rate Limiters are effective against single-IP actors but often fail against distributed botnets because the attack is spread across thousands of distinct IPs within a rented subnet.
|
| 18 |
+
- **DO NOT** restart services. This will only temporarily clear connection pools before being immediately overwhelmed again.
|
| 19 |
+
- **DO NOT** rollback. This is an external attack, not a bad deployment.
|
| 20 |
+
- **DO:** Apply a firewall block to the offending IP range (CIDR format). Example: `185.220.0.0/16`.
|
| 21 |
+
|
| 22 |
+
## 4. Escalation
|
| 23 |
+
**CRITICAL:** Always alert the security team for any attacks (`alert_oncall` with a reason mentioning 'security' or 'ddos'). Mitigation alone is not enough; the security operations center (SOC) must investigate potential data exfiltration and apply network-level web application firewall (WAF) rules.
|
env.py
CHANGED
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
| 2 |
import random
|
| 3 |
from typing import Optional
|
| 4 |
from models import Action, Observation, StepResult, State
|
| 5 |
-
from tasks import EasyTask, MediumTask, HardTask, BonusTask
|
| 6 |
from tasks.base import InternalState
|
| 7 |
|
| 8 |
TASK_MAP = {
|
|
@@ -10,6 +10,7 @@ TASK_MAP = {
|
|
| 10 |
"medium": MediumTask,
|
| 11 |
"hard": HardTask,
|
| 12 |
"bonus": BonusTask,
|
|
|
|
| 13 |
}
|
| 14 |
|
| 15 |
|
|
|
|
| 2 |
import random
|
| 3 |
from typing import Optional
|
| 4 |
from models import Action, Observation, StepResult, State
|
| 5 |
+
from tasks import EasyTask, MediumTask, HardTask, BonusTask, SecurityTask
|
| 6 |
from tasks.base import InternalState
|
| 7 |
|
| 8 |
TASK_MAP = {
|
|
|
|
| 10 |
"medium": MediumTask,
|
| 11 |
"hard": HardTask,
|
| 12 |
"bonus": BonusTask,
|
| 13 |
+
"security": SecurityTask,
|
| 14 |
}
|
| 15 |
|
| 16 |
|
models.py
CHANGED
|
@@ -16,6 +16,7 @@ class ActionType(str, Enum):
|
|
| 16 |
ACKNOWLEDGE = "acknowledge"
|
| 17 |
NOOP = "noop"
|
| 18 |
SEARCH_LOGS = "search_logs"
|
|
|
|
| 19 |
|
| 20 |
|
| 21 |
class Action(BaseModel):
|
|
@@ -26,6 +27,7 @@ class Action(BaseModel):
|
|
| 26 |
version: Optional[str] = None
|
| 27 |
reason: Optional[str] = None
|
| 28 |
query: Optional[str] = None # used with search_logs
|
|
|
|
| 29 |
|
| 30 |
|
| 31 |
class Alert(BaseModel):
|
|
|
|
| 16 |
ACKNOWLEDGE = "acknowledge"
|
| 17 |
NOOP = "noop"
|
| 18 |
SEARCH_LOGS = "search_logs"
|
| 19 |
+
BLOCK_IP_RANGE = "block_ip_range"
|
| 20 |
|
| 21 |
|
| 22 |
class Action(BaseModel):
|
|
|
|
| 27 |
version: Optional[str] = None
|
| 28 |
reason: Optional[str] = None
|
| 29 |
query: Optional[str] = None # used with search_logs
|
| 30 |
+
ip_range: Optional[str] = None
|
| 31 |
|
| 32 |
|
| 33 |
class Alert(BaseModel):
|
openenv.yaml
CHANGED
|
@@ -75,6 +75,18 @@ tasks:
|
|
| 75 |
expected_score_random_agent: 0.01
|
| 76 |
expected_score_strong_llm: 0.40
|
| 77 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
action_space:
|
| 79 |
type: structured
|
| 80 |
description: >
|
|
@@ -104,6 +116,8 @@ action_space:
|
|
| 104 |
description: Acknowledge an active alert by ID
|
| 105 |
- name: noop
|
| 106 |
description: Take no action this step
|
|
|
|
|
|
|
| 107 |
|
| 108 |
observation_space:
|
| 109 |
type: structured
|
|
|
|
| 75 |
expected_score_random_agent: 0.01
|
| 76 |
expected_score_strong_llm: 0.40
|
| 77 |
|
| 78 |
+
- id: security
|
| 79 |
+
name: Security Incident (DDoS)
|
| 80 |
+
description: >
|
| 81 |
+
A botnet is performing a DDoS and credential stuffing attack against the login endpoint.
|
| 82 |
+
The API gateway and Auth service are overwhelmed. The agent must read access logs,
|
| 83 |
+
diagnose the attack IP range, block the CIDR, and alert the security team.
|
| 84 |
+
difficulty: hard
|
| 85 |
+
max_steps: 20
|
| 86 |
+
reward_range: [0.0, 1.0]
|
| 87 |
+
expected_score_random_agent: 0.01
|
| 88 |
+
expected_score_strong_llm: 0.35
|
| 89 |
+
|
| 90 |
action_space:
|
| 91 |
type: structured
|
| 92 |
description: >
|
|
|
|
| 116 |
description: Acknowledge an active alert by ID
|
| 117 |
- name: noop
|
| 118 |
description: Take no action this step
|
| 119 |
+
- name: block_ip_range
|
| 120 |
+
description: Block traffic from an IP range (CIDR format)
|
| 121 |
|
| 122 |
observation_space:
|
| 123 |
type: structured
|
server/app.py
CHANGED
|
@@ -14,7 +14,7 @@ try:
|
|
| 14 |
except ImportError:
|
| 15 |
HAS_WEB_INTERFACE = False
|
| 16 |
|
| 17 |
-
VALID_TASKS = ("easy", "medium", "hard", "bonus")
|
| 18 |
_env = DevOpsEnvironment()
|
| 19 |
app = FastAPI(
|
| 20 |
title="DevOps Incident Response — OpenEnv",
|
|
@@ -95,6 +95,7 @@ def dashboard():
|
|
| 95 |
.medium {{ background: #3a2a1a; color: #ff9800; }}
|
| 96 |
.hard {{ background: #3a1a1a; color: #f44336; }}
|
| 97 |
.bonus {{ background: #1a1a3a; color: #9c27b0; }}
|
|
|
|
| 98 |
.endpoints {{ background: #1a1d27; border: 1px solid #2d3148; border-radius: 8px; padding: 1.25rem; margin-bottom: 2rem; }}
|
| 99 |
.endpoints h3 {{ margin: 0 0 1rem; color: #fff; }}
|
| 100 |
.endpoint {{ display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.5rem; }}
|
|
@@ -133,6 +134,11 @@ def dashboard():
|
|
| 133 |
<h3>Dual Simultaneous Failure</h3>
|
| 134 |
<p>Two independent failures at once. Both must be fixed for full credit. Max 25 steps.</p>
|
| 135 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
</div>
|
| 137 |
|
| 138 |
<div class="endpoints">
|
|
@@ -252,6 +258,16 @@ def list_tasks():
|
|
| 252 |
"model reload CPU loop on ml-inference. Both must be fixed for full credit."
|
| 253 |
),
|
| 254 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 255 |
]
|
| 256 |
}
|
| 257 |
|
|
|
|
| 14 |
except ImportError:
|
| 15 |
HAS_WEB_INTERFACE = False
|
| 16 |
|
| 17 |
+
VALID_TASKS = ("easy", "medium", "hard", "bonus", "security")
|
| 18 |
_env = DevOpsEnvironment()
|
| 19 |
app = FastAPI(
|
| 20 |
title="DevOps Incident Response — OpenEnv",
|
|
|
|
| 95 |
.medium {{ background: #3a2a1a; color: #ff9800; }}
|
| 96 |
.hard {{ background: #3a1a1a; color: #f44336; }}
|
| 97 |
.bonus {{ background: #1a1a3a; color: #9c27b0; }}
|
| 98 |
+
.security {{ background: #3a1a1a; color: #ff5252; }}
|
| 99 |
.endpoints {{ background: #1a1d27; border: 1px solid #2d3148; border-radius: 8px; padding: 1.25rem; margin-bottom: 2rem; }}
|
| 100 |
.endpoints h3 {{ margin: 0 0 1rem; color: #fff; }}
|
| 101 |
.endpoint {{ display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.5rem; }}
|
|
|
|
| 134 |
<h3>Dual Simultaneous Failure</h3>
|
| 135 |
<p>Two independent failures at once. Both must be fixed for full credit. Max 25 steps.</p>
|
| 136 |
</div>
|
| 137 |
+
<div class="task">
|
| 138 |
+
<span class="badge security">SECURITY</span>
|
| 139 |
+
<h3>Security Incident (DDoS)</h3>
|
| 140 |
+
<p>Botnet DDoS and credential stuffing attack. Requires traffic blocking and security escalation. Max 20 steps.</p>
|
| 141 |
+
</div>
|
| 142 |
</div>
|
| 143 |
|
| 144 |
<div class="endpoints">
|
|
|
|
| 258 |
"model reload CPU loop on ml-inference. Both must be fixed for full credit."
|
| 259 |
),
|
| 260 |
},
|
| 261 |
+
{
|
| 262 |
+
"id": "security",
|
| 263 |
+
"name": "Security Incident (DDoS)",
|
| 264 |
+
"difficulty": "hard",
|
| 265 |
+
"max_steps": 20,
|
| 266 |
+
"description": (
|
| 267 |
+
"A botnet is performing a DDoS and credential stuffing attack against the login endpoint. "
|
| 268 |
+
"The agent must read access logs, diagnose the attack IP range, block the CIDR, and alert the security team."
|
| 269 |
+
),
|
| 270 |
+
},
|
| 271 |
]
|
| 272 |
}
|
| 273 |
|
tasks/__init__.py
CHANGED
|
@@ -2,5 +2,6 @@ from tasks.task_easy import EasyTask
|
|
| 2 |
from tasks.task_medium import MediumTask
|
| 3 |
from tasks.task_hard import HardTask
|
| 4 |
from tasks.task_bonus import BonusTask
|
|
|
|
| 5 |
|
| 6 |
-
__all__ = ["EasyTask", "MediumTask", "HardTask", "BonusTask"]
|
|
|
|
| 2 |
from tasks.task_medium import MediumTask
|
| 3 |
from tasks.task_hard import HardTask
|
| 4 |
from tasks.task_bonus import BonusTask
|
| 5 |
+
from tasks.task_security import SecurityTask
|
| 6 |
|
| 7 |
+
__all__ = ["EasyTask", "MediumTask", "HardTask", "BonusTask", "SecurityTask"]
|
tasks/task_security.py
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
import uuid
|
| 3 |
+
from typing import Dict, Any, List
|
| 4 |
+
from models import Action, ActionType
|
| 5 |
+
from tasks.base import BaseTask, InternalState, StepOutput, semantic_match
|
| 6 |
+
|
| 7 |
+
INCIDENT_TIME = "2026-04-12T11:37:00Z"
|
| 8 |
+
|
| 9 |
+
DEPENDENCIES = [
|
| 10 |
+
{"service": "api-gateway", "calls": ["auth-service", "user-service"], "called_by": []},
|
| 11 |
+
{"service": "auth-service", "calls": ["backend-db", "rate-limiter"], "called_by": ["api-gateway"]},
|
| 12 |
+
{"service": "rate-limiter", "calls": [], "called_by": ["auth-service"]},
|
| 13 |
+
{"service": "user-service", "calls": ["backend-db"], "called_by": ["api-gateway"]},
|
| 14 |
+
{"service": "backend-db", "calls": [], "called_by": ["auth-service", "user-service"]},
|
| 15 |
+
]
|
| 16 |
+
|
| 17 |
+
API_LOGS = [
|
| 18 |
+
"[11:37:00] INFO Traffic normal: 820 req/s",
|
| 19 |
+
"[11:37:30] WARN Traffic spike: 2400 req/s - monitoring",
|
| 20 |
+
"[11:38:00] WARN Traffic spike: 5800 req/s - alert fired",
|
| 21 |
+
"[11:38:30] ERROR Traffic: 12000 req/s - rate limiter overwhelmed",
|
| 22 |
+
"[11:38:45] ERROR 94.2% of requests from 185.x.x.x IP range",
|
| 23 |
+
"[11:38:46] ERROR 99.8% of high-volume requests targeting POST /api/v1/login",
|
| 24 |
+
"[11:38:47] WARN Dropping 78% of requests - circuit breaker opening",
|
| 25 |
+
"[11:39:00] ERROR Connection pool to auth-service exhausted: 500/500 connections active",
|
| 26 |
+
"[11:45:01] INFO GET /api/v1/products 200 12ms 203.0.113.42",
|
| 27 |
+
"[11:45:01] WARN POST /api/v1/login 429 8ms 185.220.101.45 [rate-limited]",
|
| 28 |
+
"[11:45:01] WARN POST /api/v1/login 429 8ms 185.220.101.46 [rate-limited]",
|
| 29 |
+
"[11:45:02] INFO GET /api/v1/health 200 3ms 10.0.0.1",
|
| 30 |
+
]
|
| 31 |
+
|
| 32 |
+
AUTH_LOGS = [
|
| 33 |
+
"[11:37:45] INFO Login attempt: user_id=NULL ip=185.220.101.45 (failed - no such user)",
|
| 34 |
+
"[11:37:45] INFO Login attempt: user_id=NULL ip=185.220.101.46 (failed - no such user)",
|
| 35 |
+
"[11:38:00] WARN 98% of login attempts are credential stuffing pattern (NULL user_ids)",
|
| 36 |
+
"[11:38:30] ERROR Thread pool saturation: 498/500 threads active",
|
| 37 |
+
"[11:38:45] ERROR Response time degraded: avg 4200ms (normal: 45ms)",
|
| 38 |
+
"[11:39:00] CRIT Auth service overwhelmed - dropping 60% of legitimate login attempts",
|
| 39 |
+
]
|
| 40 |
+
|
| 41 |
+
RATE_LIMITER_LOGS = [
|
| 42 |
+
"[11:38:00] INFO Rate limit config: 100 req/min per IP (no subnet blocking configured)",
|
| 43 |
+
"[11:38:30] WARN 185.220.101.x subnet generating 8400 req/min across 84 IPs",
|
| 44 |
+
"[11:38:45] WARN Per-IP rate limiting ineffective against distributed botnet",
|
| 45 |
+
"[11:38:46] INFO Subnet 185.220.101.0/24: 84 active IPs, avg 100 req/min each = bypassing limit",
|
| 46 |
+
]
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class SecurityTask(BaseTask):
|
| 50 |
+
def initialize(self) -> InternalState:
|
| 51 |
+
logs = {
|
| 52 |
+
"api-gateway": API_LOGS[:],
|
| 53 |
+
"auth-service": AUTH_LOGS[:],
|
| 54 |
+
"rate-limiter": RATE_LIMITER_LOGS[:],
|
| 55 |
+
"user-service": ["[11:37:00] INFO Service normal"],
|
| 56 |
+
"backend-db": ["[11:38:30] WARN High connection count detected from auth-service"],
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
services = {
|
| 60 |
+
"api-gateway": {
|
| 61 |
+
"name": "api-gateway", "status": "degraded",
|
| 62 |
+
"cpu_percent": 95.0, "memory_percent": 45.0,
|
| 63 |
+
"error_rate": 78.0, "latency_p99_ms": 3500.0,
|
| 64 |
+
"replicas_running": 5, "replicas_desired": 5,
|
| 65 |
+
"current_version": "v3.1.0", "last_deployed": "2026-03-20T08:00:00Z",
|
| 66 |
+
"minutes_degraded": 8, "sla_breach": False,
|
| 67 |
+
},
|
| 68 |
+
"auth-service": {
|
| 69 |
+
"name": "auth-service", "status": "degraded",
|
| 70 |
+
"cpu_percent": 99.0, "memory_percent": 80.0,
|
| 71 |
+
"error_rate": 60.0, "latency_p99_ms": 4200.0,
|
| 72 |
+
"replicas_running": 3, "replicas_desired": 3,
|
| 73 |
+
"current_version": "v1.5.0", "last_deployed": "2026-04-10T11:00:00Z",
|
| 74 |
+
"minutes_degraded": 8, "sla_breach": False,
|
| 75 |
+
},
|
| 76 |
+
"backend-db": {
|
| 77 |
+
"name": "backend-db", "status": "degraded",
|
| 78 |
+
"cpu_percent": 82.0, "memory_percent": 65.0,
|
| 79 |
+
"error_rate": 0.0, "latency_p99_ms": 150.0,
|
| 80 |
+
"replicas_running": 1, "replicas_desired": 1,
|
| 81 |
+
"current_version": "v14.1", "last_deployed": "2025-01-01T00:00:00Z",
|
| 82 |
+
"minutes_degraded": 5, "sla_breach": False,
|
| 83 |
+
},
|
| 84 |
+
"rate-limiter": {
|
| 85 |
+
"name": "rate-limiter", "status": "healthy",
|
| 86 |
+
"cpu_percent": 40.0, "memory_percent": 25.0,
|
| 87 |
+
"error_rate": 0.0, "latency_p99_ms": 5.0,
|
| 88 |
+
"replicas_running": 2, "replicas_desired": 2,
|
| 89 |
+
"current_version": "v2.0.0", "last_deployed": "2026-01-15T00:00:00Z",
|
| 90 |
+
"minutes_degraded": 0, "sla_breach": False,
|
| 91 |
+
},
|
| 92 |
+
"user-service": {
|
| 93 |
+
"name": "user-service", "status": "healthy",
|
| 94 |
+
"cpu_percent": 15.0, "memory_percent": 30.0,
|
| 95 |
+
"error_rate": 0.0, "latency_p99_ms": 25.0,
|
| 96 |
+
"replicas_running": 2, "replicas_desired": 2,
|
| 97 |
+
"current_version": "v1.1.2", "last_deployed": "2026-03-01T00:00:00Z",
|
| 98 |
+
"minutes_degraded": 0, "sla_breach": False,
|
| 99 |
+
},
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
alerts = [
|
| 103 |
+
{
|
| 104 |
+
"id": "A001", "severity": "critical", "service": "api-gateway",
|
| 105 |
+
"message": "Error rate 78% - requests being dropped (traffic: 12000 req/s)",
|
| 106 |
+
"timestamp": "2026-04-12T11:38:47Z", "acknowledged": False,
|
| 107 |
+
},
|
| 108 |
+
{
|
| 109 |
+
"id": "A002", "severity": "critical", "service": "auth-service",
|
| 110 |
+
"message": "Response time 4200ms (threshold: 500ms) - connection pool exhausted",
|
| 111 |
+
"timestamp": "2026-04-12T11:38:45Z", "acknowledged": False,
|
| 112 |
+
},
|
| 113 |
+
{
|
| 114 |
+
"id": "A003", "severity": "warning", "service": "backend-db",
|
| 115 |
+
"message": "Connection pool 89% utilized - auth query storm",
|
| 116 |
+
"timestamp": "2026-04-12T11:38:50Z", "acknowledged": False,
|
| 117 |
+
},
|
| 118 |
+
{
|
| 119 |
+
"id": "A004", "severity": "info", "service": "rate-limiter",
|
| 120 |
+
"message": "Per-IP rate limits being bypassed by distributed source",
|
| 121 |
+
"timestamp": "2026-04-12T11:38:46Z", "acknowledged": False,
|
| 122 |
+
},
|
| 123 |
+
]
|
| 124 |
+
|
| 125 |
+
state = InternalState(
|
| 126 |
+
episode_id=str(uuid.uuid4()), task_id="security", step=0, max_steps=20,
|
| 127 |
+
services=services, alerts=alerts, logs=logs,
|
| 128 |
+
action_history=[], total_reward=0.0, incident_resolved=False,
|
| 129 |
+
ground_truth_root_cause="ddos_attack_185.x.x.x_botnet_targeting_login_endpoint",
|
| 130 |
+
ground_truth_fix="block_ip_range_185.x.x.x AND alert_oncall security team",
|
| 131 |
+
incident_start_time=INCIDENT_TIME,
|
| 132 |
+
healthy_services=["rate-limiter", "user-service"],
|
| 133 |
+
service_dependencies=DEPENDENCIES,
|
| 134 |
+
)
|
| 135 |
+
return state
|
| 136 |
+
|
| 137 |
+
def step(self, state: InternalState, action: Action) -> StepOutput:
|
| 138 |
+
state.step += 1
|
| 139 |
+
state._apply_sla_degradation()
|
| 140 |
+
at = action.action_type
|
| 141 |
+
svc = action.service or ""
|
| 142 |
+
reward = 0.0
|
| 143 |
+
done = False
|
| 144 |
+
info: Dict[str, Any] = {}
|
| 145 |
+
|
| 146 |
+
result_text, error_text = self._apply_action_to_logs(state, action)
|
| 147 |
+
|
| 148 |
+
gather_map = {
|
| 149 |
+
("read_logs", "api-gateway"): ("rl_api", 0.10),
|
| 150 |
+
("search_logs", "api-gateway"): ("rl_api", 0.10),
|
| 151 |
+
("read_logs", "auth-service"): ("rl_auth", 0.10),
|
| 152 |
+
("search_logs", "auth-service"): ("rl_auth", 0.10),
|
| 153 |
+
("read_logs", "rate-limiter"): ("rl_rate", 0.05),
|
| 154 |
+
("search_logs", "rate-limiter"): ("rl_rate", 0.05),
|
| 155 |
+
}
|
| 156 |
+
k = (at.value, svc)
|
| 157 |
+
if k in gather_map:
|
| 158 |
+
tag, r = gather_map[k]
|
| 159 |
+
if tag not in state.rewards_given:
|
| 160 |
+
reward += r; state.rewards_given.add(tag)
|
| 161 |
+
|
| 162 |
+
if at == ActionType.READ_RUNBOOK:
|
| 163 |
+
rb = action.runbook or ""
|
| 164 |
+
if rb.endswith("security_incident.md"):
|
| 165 |
+
if "runbook_security" not in state.rewards_given:
|
| 166 |
+
reward += 0.05; state.rewards_given.add("runbook_security")
|
| 167 |
+
|
| 168 |
+
if at == ActionType.DIAGNOSE:
|
| 169 |
+
rc = action.root_cause or ""
|
| 170 |
+
if semantic_match(rc, ["ddos", "botnet", "185", "attack", "credential stuffing"]):
|
| 171 |
+
if "diagnose_correct" not in state.rewards_given:
|
| 172 |
+
reward += 0.20; state.rewards_given.add("diagnose_correct")
|
| 173 |
+
result_text = f"Diagnosis recorded: {rc}"
|
| 174 |
+
|
| 175 |
+
if at == ActionType.BLOCK_IP_RANGE:
|
| 176 |
+
ip_range = action.ip_range or ""
|
| 177 |
+
if "185" in ip_range:
|
| 178 |
+
if "fix_block" not in state.rewards_given:
|
| 179 |
+
reward += 0.30; state.rewards_given.add("fix_block")
|
| 180 |
+
if ip_range == "185.0.0.0/8" or ip_range == "185.220.0.0/16":
|
| 181 |
+
if "bonus_cidr" not in state.rewards_given:
|
| 182 |
+
reward += 0.10; state.rewards_given.add("bonus_cidr")
|
| 183 |
+
result_text = f"Successfully applied firewall block rule for IP range {ip_range}."
|
| 184 |
+
|
| 185 |
+
if "fix_alert" in state.rewards_given:
|
| 186 |
+
state.incident_resolved = True; done = True; info["resolution"] = "incident_resolved"
|
| 187 |
+
else:
|
| 188 |
+
reward -= 0.10
|
| 189 |
+
result_text = f"Blocked IP range {ip_range}, but it did not stop the attack."
|
| 190 |
+
|
| 191 |
+
if at == ActionType.ALERT_ONCALL:
|
| 192 |
+
reason = (action.reason or "").lower()
|
| 193 |
+
if semantic_match(reason, ["security", "ddos", "attack"]):
|
| 194 |
+
if "fix_alert" not in state.rewards_given:
|
| 195 |
+
reward += 0.20; state.rewards_given.add("fix_alert")
|
| 196 |
+
result_text = "Security team paged. They are actively monitoring the situation."
|
| 197 |
+
|
| 198 |
+
if "fix_block" in state.rewards_given:
|
| 199 |
+
state.incident_resolved = True; done = True; info["resolution"] = "incident_resolved"
|
| 200 |
+
else:
|
| 201 |
+
result_text = "On-call paged, but without security context they cannot escalate."
|
| 202 |
+
|
| 203 |
+
if at == ActionType.RESTART_SERVICE:
|
| 204 |
+
reward -= 0.15
|
| 205 |
+
result_text = f"Restarted {svc}. Connection pool dropped but immediately overwhelmed again by DDoS."
|
| 206 |
+
|
| 207 |
+
if at == ActionType.ROLLBACK:
|
| 208 |
+
reward -= 0.10
|
| 209 |
+
result_text = f"Rolled back {svc}, but this is an external attack, not a bad deployment."
|
| 210 |
+
|
| 211 |
+
if at == ActionType.NOOP and state.step > 5:
|
| 212 |
+
reward -= 0.03
|
| 213 |
+
|
| 214 |
+
state.total_reward = self._clamp(state.total_reward + reward)
|
| 215 |
+
if state.step >= state.max_steps and not done:
|
| 216 |
+
done = True; info["reason"] = "max_steps_reached"
|
| 217 |
+
|
| 218 |
+
obs = state._build_observation(last_action_result=result_text, last_action_error=error_text)
|
| 219 |
+
state.action_history.append({"step": state.step, "action": action.model_dump(), "reward": round(reward, 4)})
|
| 220 |
+
return StepOutput(next_state=state, reward=round(reward, 4), done=done, info=info)
|