Arijit-07 commited on
Commit
8be69b1
·
1 Parent(s): fca2aa4

Add Task 5: Security Incident Response (DDoS detection and mitigation)

Browse files
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)