hirann commited on
Commit
4b6a7cb
·
verified ·
1 Parent(s): 76a3520

Upload immunoorg/environment.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. immunoorg/environment.py +646 -640
immunoorg/environment.py CHANGED
@@ -1,640 +1,646 @@
1
- """
2
- ImmunoOrg Core Environment
3
- ==========================
4
- OpenEnv Environment subclass orchestrating the dual-layer simulation.
5
- """
6
-
7
- import random
8
- import uuid
9
- import logging
10
- import json
11
- from typing import Any
12
-
13
- try:
14
- from openenv import OpenEnvEnvironment
15
- except ImportError:
16
- # openenv package not installed — define minimal stub for HF Spaces / standalone use
17
- class OpenEnvEnvironment:
18
- """Minimal stub when openenv package is unavailable."""
19
- pass
20
-
21
- from immunoorg.models import (
22
- ActionType, ApprovalStatus, Attack, AttackVector,
23
- ImmunoAction, ImmunoObservation, ImmunoState, IncidentPhase,
24
- TacticalAction, StrategicAction, DiagnosticAction,
25
- )
26
- from immunoorg.network_graph import NetworkGraph
27
- from immunoorg.org_graph import OrgGraph
28
- from immunoorg.permission_flow import PermissionFlowEngine
29
- from immunoorg.attack_engine import AttackEngine
30
- from immunoorg.belief_map import BeliefMap
31
- from immunoorg.curriculum import CurriculumEngine
32
- from immunoorg.reward import RewardCalculator
33
- from immunoorg.agents.department import DepartmentAgentPool
34
- from immunoorg.self_improvement import SelfImprovementEngine
35
- # ImmunoOrg 2.0 modules
36
- from immunoorg.war_room import WarRoom
37
- from immunoorg.devsecops_mesh import DevSecOpsMesh
38
- from immunoorg.knowledge_base import CVEKnowledgeBase
39
- from immunoorg.migration_engine import MigrationEngine
40
-
41
- from immunoorg.executive_context import ExecutiveContextEngine
42
-
43
-
44
- class ImmunoOrgEnvironment(OpenEnvEnvironment):
45
- """The Self-Healing Autonomous Enterprise environment."""
46
-
47
- def __init__(self, difficulty: int = 1, seed: int | None = None):
48
- self.seed = seed
49
- self.rng = random.Random(seed)
50
- self.curriculum = CurriculumEngine(start_level=difficulty)
51
- self.self_improvement = SelfImprovementEngine(seed=seed)
52
- self._state = ImmunoState()
53
- # Sub-engines initialized on reset
54
- self.network: NetworkGraph | None = None
55
- self.org: OrgGraph | None = None
56
- self.permissions: PermissionFlowEngine | None = None
57
- self.attacks: AttackEngine | None = None
58
- self.belief_map: BeliefMap | None = None
59
- self.reward_calc: RewardCalculator | None = None
60
- self.dept_agents: DepartmentAgentPool | None = None
61
- self._pending_actions: dict[str, ImmunoAction] = {} # approval_id -> action
62
- # ImmunoOrg 2.0 engines
63
- self.war_room: WarRoom = WarRoom(seed=seed)
64
- self.devsecops_mesh: DevSecOpsMesh = DevSecOpsMesh(seed=seed)
65
- self.migration_engine: MigrationEngine = MigrationEngine(rng=self.rng)
66
- self.executive_context: ExecutiveContextEngine = ExecutiveContextEngine(rng=self.rng)
67
- self.knowledge_base: CVEKnowledgeBase = CVEKnowledgeBase()
68
- # 2.0 per-step state
69
- self._last_war_room_turns: int = 0
70
- self._last_pipeline_integrity: float = 1.0
71
- self._last_pipeline_gate = None
72
-
73
- @property
74
- def state(self) -> ImmunoState:
75
- return self._state
76
-
77
- def reset(self, task: str | None = None) -> ImmunoObservation:
78
- """Initialize a new episode."""
79
- config = self.curriculum.get_current_config()
80
- s = self.rng.randint(0, 999999) if self.seed is None else self.seed
81
-
82
- # Initialize sub-engines
83
- self.network = NetworkGraph(difficulty=config.level, seed=s)
84
- self.network.generate_topology()
85
-
86
- self.org = OrgGraph(difficulty=config.level, seed=s)
87
- self.org.generate_org_structure(list(self.network.nodes.keys()))
88
-
89
- self.permissions = PermissionFlowEngine(self.org, seed=s)
90
- self.attacks = AttackEngine(self.network, difficulty=config.level, seed=s)
91
- self.belief_map = BeliefMap()
92
- self.reward_calc = RewardCalculator(config.reward_coefficients)
93
- self.dept_agents = DepartmentAgentPool(self.org.get_all_nodes(), seed=s)
94
-
95
- # Reset state
96
- self._state = ImmunoState(
97
- max_steps=config.max_steps,
98
- difficulty_level=config.level,
99
- network_nodes=self.network.get_all_nodes(),
100
- network_edges=self.network.get_all_edges(),
101
- org_nodes=self.org.get_all_nodes(),
102
- org_edges=self.org.get_all_edges(),
103
- current_phase=IncidentPhase.DETECTION,
104
- self_improvement_generation=self.self_improvement.state.current_generation,
105
- )
106
-
107
- # Reset 2.0 engines
108
- self.war_room = WarRoom(seed=s)
109
- self.devsecops_mesh = DevSecOpsMesh(seed=s)
110
- self.migration_engine = MigrationEngine(rng=random.Random(s))
111
- self.executive_context = ExecutiveContextEngine(rng=random.Random(s))
112
- self._last_war_room_turns = 0
113
- self._last_pipeline_integrity = 1.0
114
- self._last_pipeline_gate = None
115
-
116
- # Generate initial attack
117
- initial_attack = self.attacks.generate_initial_attack(sim_time=0.0)
118
- self._state.active_attacks = [initial_attack]
119
- self._state.threat_level = initial_attack.severity
120
-
121
- # Set ground truth correlations
122
- self.belief_map.set_ground_truth([{
123
- "vector": initial_attack.vector.value,
124
- "target": initial_attack.target_node,
125
- }])
126
- self._state.ground_truth_correlations = self.belief_map.ground_truth
127
-
128
- # Record phase
129
- self._state.phase_history.append({"phase": IncidentPhase.DETECTION.value, "step": 0})
130
-
131
- return self._build_observation("Episode started. Threat detected.", True)
132
-
133
- def step(self, action: ImmunoAction) -> tuple[ImmunoObservation, float, bool]:
134
- """Process one step."""
135
- self._state.step_count += 1
136
- self._state.sim_time += 1.0
137
-
138
- # Record reasoning trace
139
- self.record_reasoning(action)
140
-
141
- threats_before = len(self.attacks.get_active_attacks())
142
-
143
- # 1. Process the action
144
- result, success = self._execute_action(action)
145
-
146
- # 2. Adversary reacts
147
- self.attacks.adversary_tick(self._state.sim_time)
148
- action_name = self._get_action_name(action)
149
- self.attacks.observe_defender_action(action_name)
150
-
151
- # 2b. DevSecOps Mesh — tick pipeline simulation
152
- mesh_result = self.devsecops_mesh.simulate_pipeline_tick(
153
- self._state.sim_time,
154
- threat_active=len(self.attacks.get_active_attacks()) > 0,
155
- )
156
- self._last_pipeline_integrity = mesh_result.pipeline_integrity_score
157
- self._last_pipeline_gate = mesh_result.earliest_gate_caught
158
- # War Room: trigger on high-severity events
159
- if mesh_result.events and any(e.war_room_triggered for e in mesh_result.events):
160
- if self.attacks.active_attacks:
161
- _atk = self.attacks.active_attacks[0]
162
- _nodes = [n.model_dump() for n in self.network.get_all_nodes()]
163
- debate = self.war_room.run_debate(
164
- _atk, self._state.threat_level, _nodes, self._state.sim_time
165
- )
166
- self._last_war_room_turns = debate.turns_to_consensus
167
-
168
- # 2c. Migration engine advance if active
169
- if self.migration_engine.is_active:
170
- self.migration_engine.advance(self._state.sim_time)
171
-
172
- # 2d. Executive context — tick
173
- self.executive_context.tick(self._state.sim_time, self._state.step_count)
174
-
175
- # 2e. War Room — trigger on high-severity threat if not already triggered
176
- if (self._state.threat_level >= self.war_room.ACTIVATION_THRESHOLD
177
- and self.attacks.active_attacks
178
- and self._state.step_count % 5 == 0): # Throttle: at most every 5 steps
179
- _atk = self.attacks.active_attacks[0]
180
- _nodes = [n.model_dump() for n in self.network.get_all_nodes()]
181
- debate = self.war_room.run_debate(
182
- _atk, self._state.threat_level, _nodes, self._state.sim_time
183
- )
184
- self._last_war_room_turns = debate.turns_to_consensus
185
-
186
- # 3. Apply damage tick
187
- damage = self.network.apply_damage_tick(self._state.sim_time)
188
- self._state.total_damage += damage
189
- if damage > 0:
190
- self._state.total_downtime += 1.0
191
-
192
- # 4. Process pending approvals — execute approved actions
193
- resolved = self.permissions.process_pending(self._state.sim_time, self._state.threat_level)
194
- for req in resolved:
195
- self._state.completed_approvals.append(req)
196
- if req.status == ApprovalStatus.APPROVED and req.id in self._pending_actions:
197
- pending_action = self._pending_actions.pop(req.id)
198
- self._execute_direct(pending_action)
199
-
200
- # 5. Update state
201
- self._state.network_nodes = self.network.get_all_nodes()
202
- self._state.active_attacks = self.attacks.active_attacks
203
- self._state.contained_attacks = self.attacks.contained_attacks
204
- self._state.org_nodes = self.org.get_all_nodes()
205
- self._state.org_edges = self.org.get_all_edges()
206
- self._state.pending_approvals = self.permissions.pending
207
- self._state.agent_belief_map = self.belief_map.state
208
-
209
- # Update threat level
210
- active = self.attacks.get_active_attacks()
211
- self._state.threat_level = max((a.severity for a in active), default=0.0)
212
-
213
- # Update org chaos
214
- self._state.org_chaos_score = self.org.calculate_org_chaos()
215
-
216
- # 6. Phase transitions
217
- self._check_phase_transition()
218
-
219
- # 7. Calculate reward
220
- threats_after = len(self.attacks.get_active_attacks())
221
- belief_accuracy = self.belief_map.calculate_belief_accuracy()
222
- patronus_score = self.executive_context.get_patronus_score()
223
- reward = self.reward_calc.compute_step_reward(
224
- state=self._state, action=action, action_success=success,
225
- threats_before=threats_before, threats_after=threats_after,
226
- belief_accuracy=belief_accuracy,
227
- org_chaos=self._state.org_chaos_score,
228
- downtime_delta=1.0 if damage > 0 else 0.0,
229
- war_room_turns=self._last_war_room_turns,
230
- pipeline_integrity_score=self._last_pipeline_integrity,
231
- pipeline_gate=self._last_pipeline_gate,
232
- patronus_score=patronus_score,
233
- )
234
- self._state.cumulative_reward += reward
235
-
236
- # 8. Check termination
237
- terminated = self._check_termination()
238
- if terminated:
239
- episode_reward = self.reward_calc.compute_episode_reward(
240
- self._state, belief_accuracy, self.org.calculate_org_efficiency()
241
- )
242
- reward += episode_reward
243
- self._state.cumulative_reward += episode_reward
244
-
245
- # Record in curriculum
246
- metrics = {
247
- "threats_contained_ratio": len(self._state.contained_attacks) / max(1, len(self._state.contained_attacks) + len(self.attacks.get_active_attacks())),
248
- "total_downtime": self._state.total_downtime,
249
- "total_reward": self._state.cumulative_reward,
250
- "belief_accuracy": belief_accuracy,
251
- "org_efficiency": self.org.calculate_org_efficiency(),
252
- }
253
- self.curriculum.record_episode_result(metrics)
254
-
255
- # Record in self-improvement
256
- self.self_improvement.record_generation(
257
- org_graph=self.org,
258
- attack_complexity=self._state.threat_level,
259
- time_to_containment=self._state.sim_time,
260
- total_reward=self._state.cumulative_reward,
261
- mutations=[],
262
- )
263
-
264
- # Co-Evolution: evolve adversary based on improvement rate
265
- if self.attacks:
266
- self.attacks.evolve_adversary_complexity(self.self_improvement.state.improvement_rate)
267
-
268
- obs = self._build_observation(result, success)
269
- return obs, reward, terminated
270
-
271
- def record_reasoning(self, action: ImmunoAction) -> None:
272
- """Create a reasoning trace for the action taken."""
273
- from immunoorg.models import ReasoningTrace
274
-
275
- # In a real LLM agent, we'd ask the LLM to provide the 'trigger' separately
276
- # Here we simulate it by extracting keywords from the reasoning
277
- trigger = "Observation-based"
278
- snippet = "General environment state"
279
-
280
- if "scan" in action.reasoning.lower():
281
- trigger = "Need more info"
282
- snippet = "Low confidence in current state"
283
- elif "isolate" in action.reasoning.lower():
284
- trigger = "Containment priority"
285
- snippet = "Active threat detected on target node"
286
- elif "merge" in action.reasoning.lower() or "shortcut" in action.reasoning.lower():
287
- trigger = "Structural flaw"
288
- snippet = "Silo detected between departments"
289
-
290
- trace = ReasoningTrace(
291
- step=self._state.step_count,
292
- decision_trigger=trigger,
293
- observation_snippet=snippet,
294
- rationale=action.reasoning,
295
- timestamp=self._state.sim_time,
296
- )
297
- self._state.reasoning_traces.append(trace)
298
-
299
- def _execute_action(self, action: ImmunoAction) -> tuple[str, bool]:
300
- """Execute the agent's action and return (result_description, success)."""
301
- action_name = self._get_action_name(action)
302
-
303
- # Diagnostic actions don't need approval
304
- if action.action_type == ActionType.DIAGNOSTIC:
305
- return self._execute_diagnostic(action)
306
-
307
- # 2.0: Migration and honeypot actions are always pre-authorized (CISO authority)
308
- if action.tactical_action in (TacticalAction.START_MIGRATION, TacticalAction.DEPLOY_HONEYPOT):
309
- return self._execute_direct(action)
310
-
311
- # Check if approval needed
312
- if not self.permissions.needs_approval(action_name):
313
- return self._execute_direct(action)
314
-
315
- # Find requesting department — pick the dept that owns the target node, or security
316
- requester = "dept-security"
317
- for dept in self.org.get_all_nodes():
318
- if dept.active and action.target in dept.technical_nodes_owned:
319
- requester = dept.id
320
- break
321
-
322
- req = self.permissions.request_approval(
323
- action_name=action_name,
324
- action_type=action.action_type,
325
- requester_dept=requester,
326
- target=action.target,
327
- urgency=min(1.0, self._state.threat_level + 0.3),
328
- sim_time=self._state.sim_time,
329
- justification=action.reasoning,
330
- )
331
-
332
- # Immediate check — also try processing pending right away at high threat
333
- if req.status == ApprovalStatus.APPROVED:
334
- return self._execute_direct(action)
335
- elif req.status == ApprovalStatus.DENIED:
336
- # At high threat levels, security overrides denial
337
- if self._state.threat_level >= 0.5:
338
- return self._execute_direct(action)
339
- return f"Action '{action_name}' DENIED by {req.approver}.", False
340
- else:
341
- # Store pending action for execution when approved
342
- self._pending_actions[req.id] = action
343
- # At high urgency, fast-track: execute immediately with delay penalty
344
- if self._state.threat_level >= 0.4:
345
- return self._execute_direct(action)
346
- return f"Action '{action_name}' submitted for approval. Waiting...", False
347
-
348
-
349
- def _execute_direct(self, action: ImmunoAction) -> tuple[str, bool]:
350
- """Execute an action that has been approved or doesn't need approval."""
351
- if action.action_type == ActionType.TACTICAL:
352
- return self._execute_tactical(action)
353
- elif action.action_type == ActionType.STRATEGIC:
354
- return self._execute_strategic(action)
355
- return "Unknown action type", False
356
-
357
- def _execute_tactical(self, action: ImmunoAction) -> tuple[str, bool]:
358
- t = action.tactical_action
359
- target = action.target
360
- if t == TacticalAction.BLOCK_PORT:
361
- port = action.parameters.get("port_number", 0)
362
- ok = self.network.block_port(target, port)
363
- # Check if this contains an attack
364
- for atk in self.attacks.get_active_attacks():
365
- if atk.target_node == target and str(port) in atk.entry_point:
366
- self.attacks.contain_attack(atk.id, self._state.sim_time)
367
- return f"Port {port} blocked on {target}" if ok else f"Failed to block port on {target}", ok
368
- elif t == TacticalAction.ISOLATE_NODE:
369
- ok = self.network.isolate_node(target)
370
- for atk in self.attacks.get_active_attacks():
371
- if atk.target_node == target or target in atk.lateral_path:
372
- self.attacks.contain_attack(atk.id, self._state.sim_time)
373
- self._state.correct_identifications += 1
374
- return f"Node {target} isolated" if ok else f"Failed to isolate {target}", ok
375
- elif t == TacticalAction.SCAN_LOGS:
376
- logs = self.network.scan_logs(target)
377
- attack_logs = [l for l in logs if l.attack_indicator]
378
- return f"Scanned {len(logs)} logs on {target}. Found {len(attack_logs)} attack indicators.", True
379
- elif t == TacticalAction.DEPLOY_PATCH:
380
- ok = self.network.deploy_patch(target)
381
- return f"Patch deployed on {target}" if ok else f"Failed to patch {target}", ok
382
- elif t == TacticalAction.RESTORE_BACKUP:
383
- ok = self.network.restore_backup(target)
384
- return f"Backup restored on {target}" if ok else f"Failed to restore {target}", ok
385
- elif t == TacticalAction.ROTATE_CREDENTIALS:
386
- ok = self.network.rotate_credentials(target)
387
- return f"Credentials rotated on {target}" if ok else f"Failed to rotate on {target}", ok
388
- elif t == TacticalAction.QUARANTINE_TRAFFIC:
389
- ok = self.network.isolate_node(target)
390
- return f"Traffic quarantined on {target}" if ok else f"Failed to quarantine {target}", ok
391
- elif t == TacticalAction.ESCALATE_ALERT:
392
- self._state.threat_level = min(1.0, self._state.threat_level + 0.1)
393
- return f"Alert escalated. Threat level increased to {self._state.threat_level:.2f}", True
394
- elif t == TacticalAction.ENABLE_IDS:
395
- return f"IDS enabled on {target}. Enhanced detection active.", True
396
- elif t == TacticalAction.SNAPSHOT_FORENSICS:
397
- return f"Forensic snapshot captured for {target}.", True
398
- elif t == TacticalAction.START_MIGRATION:
399
- if not self.migration_engine.is_active:
400
- constraints = {
401
- "data_residency": "us-east-1", # Default; agents can override via parameters
402
- "tenant_compliance": action.parameters.get("compliance", "SOC2"),
403
- }
404
- if action.parameters.get("data_residency"):
405
- constraints["data_residency"] = action.parameters["data_residency"]
406
- self.migration_engine.start(self._state.sim_time, constraints=constraints)
407
- return (
408
- f" Polymorphic Migration INITIATED — 50-step Moving Target Defense workflow started. "
409
- f"Attacker will be diverted to honeypots. Constraints: {constraints}"
410
- ), True
411
- return "Migration already active.", False
412
- elif t == TacticalAction.DEPLOY_HONEYPOT:
413
- if self.migration_engine.state:
414
- node_id = f"honeypot-{self._state.step_count}"
415
- self.migration_engine.state.active_honeypots.append(node_id)
416
- return f"🍯 Honeypot node {node_id} deployed and seeded with fake credentials.", True
417
- return "Start migration first to deploy honeypots.", False
418
- return "Unknown tactical action", False
419
-
420
- def _execute_strategic(self, action: ImmunoAction) -> tuple[str, bool]:
421
- s = action.strategic_action
422
- target = action.target
423
- secondary = action.secondary_target
424
- self._state.org_changes_made += 1
425
- if s == StrategicAction.MERGE_DEPARTMENTS:
426
- result = self.org.merge_departments(target, secondary or "")
427
- return (f"Merged {target} and {secondary}" if result else "Merge failed"), result is not None
428
- elif s == StrategicAction.CREATE_SHORTCUT_EDGE:
429
- result = self.org.create_shortcut_edge(target, secondary or "")
430
- return (f"Shortcut created: {target} → {secondary}" if result else "Shortcut failed"), result is not None
431
- elif s == StrategicAction.REDUCE_BUREAUCRACY:
432
- ok = self.org.reduce_bureaucracy(target)
433
- return f"Bureaucracy reduced for {target}" if ok else "Failed", ok
434
- elif s == StrategicAction.UPDATE_APPROVAL_PROTOCOL:
435
- auths = action.parameters.get("new_authorities", [])
436
- ok = self.org.update_approval_protocol(target, auths)
437
- return f"Approval protocol updated for {target}" if ok else "Failed", ok
438
- elif s == StrategicAction.CREATE_INCIDENT_CHANNEL:
439
- self.org.create_shortcut_edge("dept-security", target)
440
- return f"Incident channel created: security → {target}", True
441
- elif s == StrategicAction.ESTABLISH_DEVSECOPS:
442
- self.org.create_shortcut_edge("dept-security", "dept-engineering")
443
- self.org.create_shortcut_edge("dept-engineering", "dept-security")
444
- return "DevSecOps integration established", True
445
- elif s == StrategicAction.REWRITE_POLICY:
446
- for node in self.org.get_all_nodes():
447
- if node.active:
448
- node.cooperation_threshold = max(0.2, node.cooperation_threshold - 0.1)
449
- return "Company policies rewritten — cooperation thresholds lowered", True
450
- elif s == StrategicAction.ADD_CROSS_FUNCTIONAL_TEAM:
451
- return "Cross-functional incident response team created", True
452
- elif s == StrategicAction.SPLIT_DEPARTMENT:
453
- return f"Department {target} split", True
454
- elif s == StrategicAction.REASSIGN_AUTHORITY:
455
- return f"Authority reassigned for {target}", True
456
- return "Unknown strategic action", False
457
-
458
- def _execute_diagnostic(self, action: ImmunoAction) -> tuple[str, bool]:
459
- d = action.diagnostic_action
460
- if d == DiagnosticAction.QUERY_BELIEF_MAP:
461
- feedback = self.belief_map.generate_feedback()
462
- return f"Belief Map: {feedback}", True
463
- elif d == DiagnosticAction.CORRELATE_FAILURE:
464
- tech = action.parameters.get("technical_indicator", action.target)
465
- org_flaw = action.parameters.get("organizational_flaw", "")
466
- confidence = action.parameters.get("confidence", 0.5)
467
- evidence = action.parameters.get("evidence", [action.reasoning])
468
- self.belief_map.agent_correlate(tech, org_flaw, confidence, evidence, self._state.sim_time)
469
- accuracy = self.belief_map.calculate_belief_accuracy()
470
- return f"Correlation recorded. Belief accuracy: {accuracy:.1%}", True
471
- elif d == DiagnosticAction.TRACE_ATTACK_PATH:
472
- active = self.attacks.get_active_attacks()
473
- paths = []
474
- for atk in active:
475
- paths.append(f"{atk.vector.value}: {' → '.join(atk.lateral_path)}")
476
- return f"Attack paths: {'; '.join(paths) if paths else 'No active attacks'}", True
477
- elif d == DiagnosticAction.IDENTIFY_SILO:
478
- silos = self.org.identify_silos()
479
- self.belief_map.update_silo_identification(silos)
480
- silo_strs = [f"{a}↔{b}" for a, b in silos]
481
- return f"Silos identified: {', '.join(silo_strs) if silo_strs else 'None found'}", True
482
- elif d == DiagnosticAction.MEASURE_ORG_LATENCY:
483
- efficiency = self.org.calculate_org_efficiency()
484
- avg_latency = self.permissions.get_average_approval_latency()
485
- return f"Org efficiency: {efficiency:.1%}, Avg approval latency: {avg_latency:.1f}", True
486
- elif d == DiagnosticAction.AUDIT_PERMISSIONS:
487
- denial_rate = self.permissions.get_denial_rate()
488
- return f"Permission audit: {denial_rate:.0%} denial rate", True
489
- elif d == DiagnosticAction.TIMELINE_RECONSTRUCT:
490
- history = self.attacks.attack_history
491
- return f"Timeline: {json.dumps(history[-10:], default=str)}", True
492
- elif d == DiagnosticAction.VULNERABILITY_SCAN:
493
- vulns = self.network.get_vulnerable_nodes()
494
- vuln_strs = [f"{n.id} (max_vuln={max((p.vulnerability_score for p in n.ports), default=0):.2f})" for n in vulns]
495
- return f"Vulnerable nodes: {', '.join(vuln_strs) if vuln_strs else 'None'}", True
496
- elif d == DiagnosticAction.CHECK_EXECUTIVE_CONTEXT:
497
- summary = self.executive_context.get_context_summary()
498
- drift_events = self.executive_context.state.drift_events
499
- migration_progress = self.migration_engine.get_progress()
500
- war_room_transcript = self.war_room.get_latest_transcript()
501
- return (
502
- f"{summary}\n"
503
- f"Migration: {migration_progress.get('current_phase','N/A')} "
504
- f"({migration_progress.get('progress_pct', 0):.0%} done)\n"
505
- f"War Room Latest: {war_room_transcript[:200]}"
506
- ), True
507
- return "Unknown diagnostic action", False
508
-
509
- def _get_action_name(self, action: ImmunoAction) -> str:
510
- if action.tactical_action:
511
- return action.tactical_action.value
512
- if action.strategic_action:
513
- return action.strategic_action.value
514
- if action.diagnostic_action:
515
- return action.diagnostic_action.value
516
- return ""
517
-
518
- def _check_phase_transition(self) -> None:
519
- """Auto-transition between incident phases based on meaningful progress.
520
-
521
- Each transition requires REAL work, not just step counts:
522
- - Detection → Containment: Agent must have scanned AND traced (identified the threat)
523
- - Containment → RCA: ALL active attacks must be contained
524
- - RCA Refactor: Belief map must have real accuracy AND multiple correlations
525
- - Refactor Validation: Multiple org changes must have been made
526
- """
527
- phase = self._state.current_phase
528
- active_attacks = self.attacks.get_active_attacks()
529
-
530
- if phase == IncidentPhase.DETECTION:
531
- # Require: at least 1 scan + 1 identification/trace action completed
532
- has_scanned = self._state.scans_performed > 0 if hasattr(self._state, 'scans_performed') else self._state.step_count >= 2
533
- has_identified = self._state.correct_identifications > 0 or len(self._state.contained_attacks) > 0
534
- if has_scanned and (has_identified or self._state.step_count >= 4):
535
- self._transition_phase(IncidentPhase.CONTAINMENT)
536
- elif phase == IncidentPhase.CONTAINMENT:
537
- # Require: ALL active attacks must be contained (no free passes)
538
- if len(active_attacks) == 0:
539
- self._transition_phase(IncidentPhase.ROOT_CAUSE_ANALYSIS)
540
- elif phase == IncidentPhase.ROOT_CAUSE_ANALYSIS:
541
- # Require: belief accuracy >= 0.4 AND at least 2 correlations
542
- belief_acc = self.belief_map.calculate_belief_accuracy()
543
- num_correlations = len(self.belief_map.state.correlations)
544
- if belief_acc >= 0.4 and num_correlations >= 2:
545
- self._transition_phase(IncidentPhase.ORG_REFACTOR)
546
- elif num_correlations >= 3: # Allow through with more evidence even if accuracy is lower
547
- self._transition_phase(IncidentPhase.ORG_REFACTOR)
548
- elif phase == IncidentPhase.ORG_REFACTOR:
549
- # Require: at least 2 organizational changes
550
- if self._state.org_changes_made >= 2:
551
- self._transition_phase(IncidentPhase.VALIDATION)
552
-
553
- def _transition_phase(self, new_phase: IncidentPhase) -> None:
554
- if new_phase != self._state.current_phase:
555
- self._state.current_phase = new_phase
556
- self._state.phase_history.append({"phase": new_phase.value, "step": self._state.step_count})
557
-
558
- def _check_termination(self) -> bool:
559
- if self._state.step_count >= self._state.max_steps:
560
- self._state.truncated = True
561
- self._state.termination_reason = "Max steps reached"
562
- return True
563
- if self._state.current_phase == IncidentPhase.VALIDATION and len(self.attacks.get_active_attacks()) == 0:
564
- self._state.terminated = True
565
- self._state.termination_reason = "Incident resolved — validation complete"
566
- return True
567
- all_critical = all(n.health <= 0 for n in self.network.get_all_nodes() if n.criticality > 0.7)
568
- if all_critical:
569
- self._state.terminated = True
570
- self._state.termination_reason = "Total system failure"
571
- return True
572
- return False
573
-
574
- def inject_directive(self, directive: str) -> None:
575
- """Inject a board directive mid-simulation."""
576
- self._state.directives.append(directive)
577
- logging.info(f"Board Directive Injected: {directive}")
578
-
579
- def _build_observation(self, action_result: str, action_success: bool) -> ImmunoObservation:
580
- compromised_ids = {n.id for n in self.network.get_compromised_nodes()}
581
- visible_nodes = []
582
- for n in self.network.get_all_nodes():
583
- if not n.compromised:
584
- # Clean nodes are always visible
585
- visible_nodes.append(n)
586
- else:
587
- # Compromised nodes: visibility depends on stealth and detection
588
- stealth = self.attacks.active_attacks[0].stealth if self.attacks.active_attacks else 0.5
589
- detection_chance = 0.3 + (1.0 - stealth) * 0.7
590
- if self.rng.random() < detection_chance:
591
- # Agent detects this compromised node
592
- visible_nodes.append(n)
593
- # else: node is hidden fog of war
594
-
595
- detected = [a for a in self.attacks.get_active_attacks()
596
- if self.rng.random() < 0.4 + (1 - a.stealth) * 0.6]
597
-
598
- # RAG Integration: Retrieve real-world CVE info for detected attacks
599
- rag_info = []
600
- if self.knowledge_base and detected:
601
- for atk in detected:
602
- info = self.knowledge_base.retrieve_cve_info(atk.vector.value)
603
- rag_info.append(f"Threat {atk.id} ({atk.vector.value}): {info}")
604
-
605
- recent_logs = []
606
- for n in self.network.get_all_nodes():
607
- recent_logs.extend(n.logs[-3:])
608
- recent_logs.sort(key=lambda l: l.timestamp, reverse=True)
609
-
610
- alerts = []
611
- if self._state.threat_level > 0.7:
612
- alerts.append(f"HIGH THREAT: Level {self._state.threat_level:.2f}")
613
- if self.permissions.pending:
614
- alerts.append(f"{len(self.permissions.pending)} approval(s) pending")
615
-
616
- # Add RAG info to alerts
617
- alerts.extend(rag_info)
618
-
619
- return ImmunoObservation(
620
- visible_nodes=visible_nodes,
621
- visible_edges=self.network.get_all_edges(),
622
- detected_attacks=detected,
623
- recent_logs=recent_logs[:15],
624
- network_health_summary=self.network.get_network_health(),
625
- org_nodes=self.org.get_all_nodes(),
626
- org_edges=self.org.get_active_edges(),
627
- pending_approvals=self.permissions.pending,
628
- action_result=action_result,
629
- action_success=action_success,
630
- approval_delay=self.permissions.get_average_approval_latency(),
631
- current_phase=self._state.current_phase,
632
- step_count=self._state.step_count,
633
- sim_time=self._state.sim_time,
634
- threat_level=min(1.0, max(0.0, self._state.threat_level)),
635
- system_downtime=self._state.total_downtime,
636
- belief_map_feedback=self.belief_map.generate_feedback() if self._state.step_count % 5 == 0 else "",
637
- alerts=alerts,
638
- directives=self._state.directives,
639
- )
640
-
 
 
 
 
 
 
 
1
+ """
2
+ ImmunoOrg Core Environment
3
+ ==========================
4
+ OpenEnv Environment subclass orchestrating the dual-layer simulation.
5
+ """
6
+
7
+ import random
8
+ import uuid
9
+ import logging
10
+ import json
11
+ from typing import Any
12
+
13
+ # Hackathon / training stacks should install ``openenv-core`` (provides ``openenv.core``).
14
+ # We keep ImmunoOrg's Gym-style ``step() -> (obs, reward, done)`` for the whole codebase
15
+ # and expose OpenEnv-compatible HTTP ``reset`` / ``step`` / ``state`` from ``server/main.py``.
16
+ try:
17
+ import openenv.core # noqa: F401 — OpenEnv framework present (EnvClient, rubrics, …)
18
+ except ImportError:
19
+ pass
20
+
21
+ from immunoorg.models import (
22
+ ActionType, ApprovalStatus, Attack, AttackVector,
23
+ ImmunoAction, ImmunoObservation, ImmunoState, IncidentPhase,
24
+ TacticalAction, StrategicAction, DiagnosticAction,
25
+ )
26
+ from immunoorg.network_graph import NetworkGraph
27
+ from immunoorg.org_graph import OrgGraph
28
+ from immunoorg.permission_flow import PermissionFlowEngine
29
+ from immunoorg.attack_engine import AttackEngine
30
+ from immunoorg.belief_map import BeliefMap
31
+ from immunoorg.curriculum import CurriculumEngine
32
+ from immunoorg.reward import RewardCalculator
33
+ from immunoorg.agents.department import DepartmentAgentPool
34
+ from immunoorg.self_improvement import SelfImprovementEngine
35
+ # ImmunoOrg 2.0 modules
36
+ from immunoorg.war_room import WarRoom
37
+ from immunoorg.devsecops_mesh import DevSecOpsMesh
38
+ from immunoorg.knowledge_base import CVEKnowledgeBase
39
+ from immunoorg.migration_engine import MigrationEngine
40
+
41
+ from immunoorg.executive_context import ExecutiveContextEngine
42
+
43
+
44
+ class ImmunoOrgEnvironment:
45
+ """The Self-Healing Autonomous Enterprise environment.
46
+
47
+ Implements the simulation API used across ImmunoOrg (tuple ``step``). The hosted
48
+ Space maps this to OpenEnv's HTTP contract and ``openenv.yaml`` manifest.
49
+ """
50
+
51
+ __module__ = "immunoorg.environment"
52
+
53
+ def __init__(self, difficulty: int = 1, seed: int | None = None):
54
+ self.seed = seed
55
+ self.rng = random.Random(seed)
56
+ self.curriculum = CurriculumEngine(start_level=difficulty)
57
+ self.self_improvement = SelfImprovementEngine(seed=seed)
58
+ self._state = ImmunoState()
59
+ # Sub-engines initialized on reset
60
+ self.network: NetworkGraph | None = None
61
+ self.org: OrgGraph | None = None
62
+ self.permissions: PermissionFlowEngine | None = None
63
+ self.attacks: AttackEngine | None = None
64
+ self.belief_map: BeliefMap | None = None
65
+ self.reward_calc: RewardCalculator | None = None
66
+ self.dept_agents: DepartmentAgentPool | None = None
67
+ self._pending_actions: dict[str, ImmunoAction] = {} # approval_id -> action
68
+ # ImmunoOrg 2.0 engines
69
+ self.war_room: WarRoom = WarRoom(seed=seed)
70
+ self.devsecops_mesh: DevSecOpsMesh = DevSecOpsMesh(seed=seed)
71
+ self.migration_engine: MigrationEngine = MigrationEngine(rng=self.rng)
72
+ self.executive_context: ExecutiveContextEngine = ExecutiveContextEngine(rng=self.rng)
73
+ self.knowledge_base: CVEKnowledgeBase = CVEKnowledgeBase()
74
+ # 2.0 per-step state
75
+ self._last_war_room_turns: int = 0
76
+ self._last_pipeline_integrity: float = 1.0
77
+ self._last_pipeline_gate = None
78
+
79
+ @property
80
+ def state(self) -> ImmunoState:
81
+ return self._state
82
+
83
+ def reset(self, task: str | None = None) -> ImmunoObservation:
84
+ """Initialize a new episode."""
85
+ config = self.curriculum.get_current_config()
86
+ s = self.rng.randint(0, 999999) if self.seed is None else self.seed
87
+
88
+ # Initialize sub-engines
89
+ self.network = NetworkGraph(difficulty=config.level, seed=s)
90
+ self.network.generate_topology()
91
+
92
+ self.org = OrgGraph(difficulty=config.level, seed=s)
93
+ self.org.generate_org_structure(list(self.network.nodes.keys()))
94
+
95
+ self.permissions = PermissionFlowEngine(self.org, seed=s)
96
+ self.attacks = AttackEngine(self.network, difficulty=config.level, seed=s)
97
+ self.belief_map = BeliefMap()
98
+ self.reward_calc = RewardCalculator(config.reward_coefficients)
99
+ self.dept_agents = DepartmentAgentPool(self.org.get_all_nodes(), seed=s)
100
+
101
+ # Reset state
102
+ self._state = ImmunoState(
103
+ max_steps=config.max_steps,
104
+ difficulty_level=config.level,
105
+ network_nodes=self.network.get_all_nodes(),
106
+ network_edges=self.network.get_all_edges(),
107
+ org_nodes=self.org.get_all_nodes(),
108
+ org_edges=self.org.get_all_edges(),
109
+ current_phase=IncidentPhase.DETECTION,
110
+ self_improvement_generation=self.self_improvement.state.current_generation,
111
+ )
112
+
113
+ # Reset 2.0 engines
114
+ self.war_room = WarRoom(seed=s)
115
+ self.devsecops_mesh = DevSecOpsMesh(seed=s)
116
+ self.migration_engine = MigrationEngine(rng=random.Random(s))
117
+ self.executive_context = ExecutiveContextEngine(rng=random.Random(s))
118
+ self._last_war_room_turns = 0
119
+ self._last_pipeline_integrity = 1.0
120
+ self._last_pipeline_gate = None
121
+
122
+ # Generate initial attack
123
+ initial_attack = self.attacks.generate_initial_attack(sim_time=0.0)
124
+ self._state.active_attacks = [initial_attack]
125
+ self._state.threat_level = initial_attack.severity
126
+
127
+ # Set ground truth correlations
128
+ self.belief_map.set_ground_truth([{
129
+ "vector": initial_attack.vector.value,
130
+ "target": initial_attack.target_node,
131
+ }])
132
+ self._state.ground_truth_correlations = self.belief_map.ground_truth
133
+
134
+ # Record phase
135
+ self._state.phase_history.append({"phase": IncidentPhase.DETECTION.value, "step": 0})
136
+
137
+ return self._build_observation("Episode started. Threat detected.", True)
138
+
139
+ def step(self, action: ImmunoAction) -> tuple[ImmunoObservation, float, bool]:
140
+ """Process one step."""
141
+ self._state.step_count += 1
142
+ self._state.sim_time += 1.0
143
+
144
+ # Record reasoning trace
145
+ self.record_reasoning(action)
146
+
147
+ threats_before = len(self.attacks.get_active_attacks())
148
+
149
+ # 1. Process the action
150
+ result, success = self._execute_action(action)
151
+
152
+ # 2. Adversary reacts
153
+ self.attacks.adversary_tick(self._state.sim_time)
154
+ action_name = self._get_action_name(action)
155
+ self.attacks.observe_defender_action(action_name)
156
+
157
+ # 2b. DevSecOps Mesh — tick pipeline simulation
158
+ mesh_result = self.devsecops_mesh.simulate_pipeline_tick(
159
+ self._state.sim_time,
160
+ threat_active=len(self.attacks.get_active_attacks()) > 0,
161
+ )
162
+ self._last_pipeline_integrity = mesh_result.pipeline_integrity_score
163
+ self._last_pipeline_gate = mesh_result.earliest_gate_caught
164
+ # War Room: trigger on high-severity events
165
+ if mesh_result.events and any(e.war_room_triggered for e in mesh_result.events):
166
+ if self.attacks.active_attacks:
167
+ _atk = self.attacks.active_attacks[0]
168
+ _nodes = [n.model_dump() for n in self.network.get_all_nodes()]
169
+ debate = self.war_room.run_debate(
170
+ _atk, self._state.threat_level, _nodes, self._state.sim_time
171
+ )
172
+ self._last_war_room_turns = debate.turns_to_consensus
173
+
174
+ # 2c. Migration engine ��� advance if active
175
+ if self.migration_engine.is_active:
176
+ self.migration_engine.advance(self._state.sim_time)
177
+
178
+ # 2d. Executive context tick
179
+ self.executive_context.tick(self._state.sim_time, self._state.step_count)
180
+
181
+ # 2e. War Room — trigger on high-severity threat if not already triggered
182
+ if (self._state.threat_level >= self.war_room.ACTIVATION_THRESHOLD
183
+ and self.attacks.active_attacks
184
+ and self._state.step_count % 5 == 0): # Throttle: at most every 5 steps
185
+ _atk = self.attacks.active_attacks[0]
186
+ _nodes = [n.model_dump() for n in self.network.get_all_nodes()]
187
+ debate = self.war_room.run_debate(
188
+ _atk, self._state.threat_level, _nodes, self._state.sim_time
189
+ )
190
+ self._last_war_room_turns = debate.turns_to_consensus
191
+
192
+ # 3. Apply damage tick
193
+ damage = self.network.apply_damage_tick(self._state.sim_time)
194
+ self._state.total_damage += damage
195
+ if damage > 0:
196
+ self._state.total_downtime += 1.0
197
+
198
+ # 4. Process pending approvals — execute approved actions
199
+ resolved = self.permissions.process_pending(self._state.sim_time, self._state.threat_level)
200
+ for req in resolved:
201
+ self._state.completed_approvals.append(req)
202
+ if req.status == ApprovalStatus.APPROVED and req.id in self._pending_actions:
203
+ pending_action = self._pending_actions.pop(req.id)
204
+ self._execute_direct(pending_action)
205
+
206
+ # 5. Update state
207
+ self._state.network_nodes = self.network.get_all_nodes()
208
+ self._state.active_attacks = self.attacks.active_attacks
209
+ self._state.contained_attacks = self.attacks.contained_attacks
210
+ self._state.org_nodes = self.org.get_all_nodes()
211
+ self._state.org_edges = self.org.get_all_edges()
212
+ self._state.pending_approvals = self.permissions.pending
213
+ self._state.agent_belief_map = self.belief_map.state
214
+
215
+ # Update threat level
216
+ active = self.attacks.get_active_attacks()
217
+ self._state.threat_level = max((a.severity for a in active), default=0.0)
218
+
219
+ # Update org chaos
220
+ self._state.org_chaos_score = self.org.calculate_org_chaos()
221
+
222
+ # 6. Phase transitions
223
+ self._check_phase_transition()
224
+
225
+ # 7. Calculate reward
226
+ threats_after = len(self.attacks.get_active_attacks())
227
+ belief_accuracy = self.belief_map.calculate_belief_accuracy()
228
+ patronus_score = self.executive_context.get_patronus_score()
229
+ reward = self.reward_calc.compute_step_reward(
230
+ state=self._state, action=action, action_success=success,
231
+ threats_before=threats_before, threats_after=threats_after,
232
+ belief_accuracy=belief_accuracy,
233
+ org_chaos=self._state.org_chaos_score,
234
+ downtime_delta=1.0 if damage > 0 else 0.0,
235
+ war_room_turns=self._last_war_room_turns,
236
+ pipeline_integrity_score=self._last_pipeline_integrity,
237
+ pipeline_gate=self._last_pipeline_gate,
238
+ patronus_score=patronus_score,
239
+ )
240
+ self._state.cumulative_reward += reward
241
+
242
+ # 8. Check termination
243
+ terminated = self._check_termination()
244
+ if terminated:
245
+ episode_reward = self.reward_calc.compute_episode_reward(
246
+ self._state, belief_accuracy, self.org.calculate_org_efficiency()
247
+ )
248
+ reward += episode_reward
249
+ self._state.cumulative_reward += episode_reward
250
+
251
+ # Record in curriculum
252
+ metrics = {
253
+ "threats_contained_ratio": len(self._state.contained_attacks) / max(1, len(self._state.contained_attacks) + len(self.attacks.get_active_attacks())),
254
+ "total_downtime": self._state.total_downtime,
255
+ "total_reward": self._state.cumulative_reward,
256
+ "belief_accuracy": belief_accuracy,
257
+ "org_efficiency": self.org.calculate_org_efficiency(),
258
+ }
259
+ self.curriculum.record_episode_result(metrics)
260
+
261
+ # Record in self-improvement
262
+ self.self_improvement.record_generation(
263
+ org_graph=self.org,
264
+ attack_complexity=self._state.threat_level,
265
+ time_to_containment=self._state.sim_time,
266
+ total_reward=self._state.cumulative_reward,
267
+ mutations=[],
268
+ )
269
+
270
+ # Co-Evolution: evolve adversary based on improvement rate
271
+ if self.attacks:
272
+ self.attacks.evolve_adversary_complexity(self.self_improvement.state.improvement_rate)
273
+
274
+ obs = self._build_observation(result, success)
275
+ return obs, reward, terminated
276
+
277
+ def record_reasoning(self, action: ImmunoAction) -> None:
278
+ """Create a reasoning trace for the action taken."""
279
+ from immunoorg.models import ReasoningTrace
280
+
281
+ # In a real LLM agent, we'd ask the LLM to provide the 'trigger' separately
282
+ # Here we simulate it by extracting keywords from the reasoning
283
+ trigger = "Observation-based"
284
+ snippet = "General environment state"
285
+
286
+ if "scan" in action.reasoning.lower():
287
+ trigger = "Need more info"
288
+ snippet = "Low confidence in current state"
289
+ elif "isolate" in action.reasoning.lower():
290
+ trigger = "Containment priority"
291
+ snippet = "Active threat detected on target node"
292
+ elif "merge" in action.reasoning.lower() or "shortcut" in action.reasoning.lower():
293
+ trigger = "Structural flaw"
294
+ snippet = "Silo detected between departments"
295
+
296
+ trace = ReasoningTrace(
297
+ step=self._state.step_count,
298
+ decision_trigger=trigger,
299
+ observation_snippet=snippet,
300
+ rationale=action.reasoning,
301
+ timestamp=self._state.sim_time,
302
+ )
303
+ self._state.reasoning_traces.append(trace)
304
+
305
+ def _execute_action(self, action: ImmunoAction) -> tuple[str, bool]:
306
+ """Execute the agent's action and return (result_description, success)."""
307
+ action_name = self._get_action_name(action)
308
+
309
+ # Diagnostic actions don't need approval
310
+ if action.action_type == ActionType.DIAGNOSTIC:
311
+ return self._execute_diagnostic(action)
312
+
313
+ # 2.0: Migration and honeypot actions are always pre-authorized (CISO authority)
314
+ if action.tactical_action in (TacticalAction.START_MIGRATION, TacticalAction.DEPLOY_HONEYPOT):
315
+ return self._execute_direct(action)
316
+
317
+ # Check if approval needed
318
+ if not self.permissions.needs_approval(action_name):
319
+ return self._execute_direct(action)
320
+
321
+ # Find requesting department — pick the dept that owns the target node, or security
322
+ requester = "dept-security"
323
+ for dept in self.org.get_all_nodes():
324
+ if dept.active and action.target in dept.technical_nodes_owned:
325
+ requester = dept.id
326
+ break
327
+
328
+ req = self.permissions.request_approval(
329
+ action_name=action_name,
330
+ action_type=action.action_type,
331
+ requester_dept=requester,
332
+ target=action.target,
333
+ urgency=min(1.0, self._state.threat_level + 0.3),
334
+ sim_time=self._state.sim_time,
335
+ justification=action.reasoning,
336
+ )
337
+
338
+ # Immediate check — also try processing pending right away at high threat
339
+ if req.status == ApprovalStatus.APPROVED:
340
+ return self._execute_direct(action)
341
+ elif req.status == ApprovalStatus.DENIED:
342
+ # At high threat levels, security overrides denial
343
+ if self._state.threat_level >= 0.5:
344
+ return self._execute_direct(action)
345
+ return f"Action '{action_name}' DENIED by {req.approver}.", False
346
+ else:
347
+ # Store pending action for execution when approved
348
+ self._pending_actions[req.id] = action
349
+ # At high urgency, fast-track: execute immediately with delay penalty
350
+ if self._state.threat_level >= 0.4:
351
+ return self._execute_direct(action)
352
+ return f"Action '{action_name}' submitted for approval. Waiting...", False
353
+
354
+
355
+ def _execute_direct(self, action: ImmunoAction) -> tuple[str, bool]:
356
+ """Execute an action that has been approved or doesn't need approval."""
357
+ if action.action_type == ActionType.TACTICAL:
358
+ return self._execute_tactical(action)
359
+ elif action.action_type == ActionType.STRATEGIC:
360
+ return self._execute_strategic(action)
361
+ return "Unknown action type", False
362
+
363
+ def _execute_tactical(self, action: ImmunoAction) -> tuple[str, bool]:
364
+ t = action.tactical_action
365
+ target = action.target
366
+ if t == TacticalAction.BLOCK_PORT:
367
+ port = action.parameters.get("port_number", 0)
368
+ ok = self.network.block_port(target, port)
369
+ # Check if this contains an attack
370
+ for atk in self.attacks.get_active_attacks():
371
+ if atk.target_node == target and str(port) in atk.entry_point:
372
+ self.attacks.contain_attack(atk.id, self._state.sim_time)
373
+ return f"Port {port} blocked on {target}" if ok else f"Failed to block port on {target}", ok
374
+ elif t == TacticalAction.ISOLATE_NODE:
375
+ ok = self.network.isolate_node(target)
376
+ for atk in self.attacks.get_active_attacks():
377
+ if atk.target_node == target or target in atk.lateral_path:
378
+ self.attacks.contain_attack(atk.id, self._state.sim_time)
379
+ self._state.correct_identifications += 1
380
+ return f"Node {target} isolated" if ok else f"Failed to isolate {target}", ok
381
+ elif t == TacticalAction.SCAN_LOGS:
382
+ logs = self.network.scan_logs(target)
383
+ attack_logs = [l for l in logs if l.attack_indicator]
384
+ return f"Scanned {len(logs)} logs on {target}. Found {len(attack_logs)} attack indicators.", True
385
+ elif t == TacticalAction.DEPLOY_PATCH:
386
+ ok = self.network.deploy_patch(target)
387
+ return f"Patch deployed on {target}" if ok else f"Failed to patch {target}", ok
388
+ elif t == TacticalAction.RESTORE_BACKUP:
389
+ ok = self.network.restore_backup(target)
390
+ return f"Backup restored on {target}" if ok else f"Failed to restore {target}", ok
391
+ elif t == TacticalAction.ROTATE_CREDENTIALS:
392
+ ok = self.network.rotate_credentials(target)
393
+ return f"Credentials rotated on {target}" if ok else f"Failed to rotate on {target}", ok
394
+ elif t == TacticalAction.QUARANTINE_TRAFFIC:
395
+ ok = self.network.isolate_node(target)
396
+ return f"Traffic quarantined on {target}" if ok else f"Failed to quarantine {target}", ok
397
+ elif t == TacticalAction.ESCALATE_ALERT:
398
+ self._state.threat_level = min(1.0, self._state.threat_level + 0.1)
399
+ return f"Alert escalated. Threat level increased to {self._state.threat_level:.2f}", True
400
+ elif t == TacticalAction.ENABLE_IDS:
401
+ return f"IDS enabled on {target}. Enhanced detection active.", True
402
+ elif t == TacticalAction.SNAPSHOT_FORENSICS:
403
+ return f"Forensic snapshot captured for {target}.", True
404
+ elif t == TacticalAction.START_MIGRATION:
405
+ if not self.migration_engine.is_active:
406
+ constraints = {
407
+ "data_residency": "us-east-1", # Default; agents can override via parameters
408
+ "tenant_compliance": action.parameters.get("compliance", "SOC2"),
409
+ }
410
+ if action.parameters.get("data_residency"):
411
+ constraints["data_residency"] = action.parameters["data_residency"]
412
+ self.migration_engine.start(self._state.sim_time, constraints=constraints)
413
+ return (
414
+ f"⚡ Polymorphic Migration INITIATED — 50-step Moving Target Defense workflow started. "
415
+ f"Attacker will be diverted to honeypots. Constraints: {constraints}"
416
+ ), True
417
+ return "Migration already active.", False
418
+ elif t == TacticalAction.DEPLOY_HONEYPOT:
419
+ if self.migration_engine.state:
420
+ node_id = f"honeypot-{self._state.step_count}"
421
+ self.migration_engine.state.active_honeypots.append(node_id)
422
+ return f"🍯 Honeypot node {node_id} deployed and seeded with fake credentials.", True
423
+ return "Start migration first to deploy honeypots.", False
424
+ return "Unknown tactical action", False
425
+
426
+ def _execute_strategic(self, action: ImmunoAction) -> tuple[str, bool]:
427
+ s = action.strategic_action
428
+ target = action.target
429
+ secondary = action.secondary_target
430
+ self._state.org_changes_made += 1
431
+ if s == StrategicAction.MERGE_DEPARTMENTS:
432
+ result = self.org.merge_departments(target, secondary or "")
433
+ return (f"Merged {target} and {secondary}" if result else "Merge failed"), result is not None
434
+ elif s == StrategicAction.CREATE_SHORTCUT_EDGE:
435
+ result = self.org.create_shortcut_edge(target, secondary or "")
436
+ return (f"Shortcut created: {target} → {secondary}" if result else "Shortcut failed"), result is not None
437
+ elif s == StrategicAction.REDUCE_BUREAUCRACY:
438
+ ok = self.org.reduce_bureaucracy(target)
439
+ return f"Bureaucracy reduced for {target}" if ok else "Failed", ok
440
+ elif s == StrategicAction.UPDATE_APPROVAL_PROTOCOL:
441
+ auths = action.parameters.get("new_authorities", [])
442
+ ok = self.org.update_approval_protocol(target, auths)
443
+ return f"Approval protocol updated for {target}" if ok else "Failed", ok
444
+ elif s == StrategicAction.CREATE_INCIDENT_CHANNEL:
445
+ self.org.create_shortcut_edge("dept-security", target)
446
+ return f"Incident channel created: security → {target}", True
447
+ elif s == StrategicAction.ESTABLISH_DEVSECOPS:
448
+ self.org.create_shortcut_edge("dept-security", "dept-engineering")
449
+ self.org.create_shortcut_edge("dept-engineering", "dept-security")
450
+ return "DevSecOps integration established", True
451
+ elif s == StrategicAction.REWRITE_POLICY:
452
+ for node in self.org.get_all_nodes():
453
+ if node.active:
454
+ node.cooperation_threshold = max(0.2, node.cooperation_threshold - 0.1)
455
+ return "Company policies rewritten — cooperation thresholds lowered", True
456
+ elif s == StrategicAction.ADD_CROSS_FUNCTIONAL_TEAM:
457
+ return "Cross-functional incident response team created", True
458
+ elif s == StrategicAction.SPLIT_DEPARTMENT:
459
+ return f"Department {target} split", True
460
+ elif s == StrategicAction.REASSIGN_AUTHORITY:
461
+ return f"Authority reassigned for {target}", True
462
+ return "Unknown strategic action", False
463
+
464
+ def _execute_diagnostic(self, action: ImmunoAction) -> tuple[str, bool]:
465
+ d = action.diagnostic_action
466
+ if d == DiagnosticAction.QUERY_BELIEF_MAP:
467
+ feedback = self.belief_map.generate_feedback()
468
+ return f"Belief Map: {feedback}", True
469
+ elif d == DiagnosticAction.CORRELATE_FAILURE:
470
+ tech = action.parameters.get("technical_indicator", action.target)
471
+ org_flaw = action.parameters.get("organizational_flaw", "")
472
+ confidence = action.parameters.get("confidence", 0.5)
473
+ evidence = action.parameters.get("evidence", [action.reasoning])
474
+ self.belief_map.agent_correlate(tech, org_flaw, confidence, evidence, self._state.sim_time)
475
+ accuracy = self.belief_map.calculate_belief_accuracy()
476
+ return f"Correlation recorded. Belief accuracy: {accuracy:.1%}", True
477
+ elif d == DiagnosticAction.TRACE_ATTACK_PATH:
478
+ active = self.attacks.get_active_attacks()
479
+ paths = []
480
+ for atk in active:
481
+ paths.append(f"{atk.vector.value}: {' '.join(atk.lateral_path)}")
482
+ return f"Attack paths: {'; '.join(paths) if paths else 'No active attacks'}", True
483
+ elif d == DiagnosticAction.IDENTIFY_SILO:
484
+ silos = self.org.identify_silos()
485
+ self.belief_map.update_silo_identification(silos)
486
+ silo_strs = [f"{a}↔{b}" for a, b in silos]
487
+ return f"Silos identified: {', '.join(silo_strs) if silo_strs else 'None found'}", True
488
+ elif d == DiagnosticAction.MEASURE_ORG_LATENCY:
489
+ efficiency = self.org.calculate_org_efficiency()
490
+ avg_latency = self.permissions.get_average_approval_latency()
491
+ return f"Org efficiency: {efficiency:.1%}, Avg approval latency: {avg_latency:.1f}", True
492
+ elif d == DiagnosticAction.AUDIT_PERMISSIONS:
493
+ denial_rate = self.permissions.get_denial_rate()
494
+ return f"Permission audit: {denial_rate:.0%} denial rate", True
495
+ elif d == DiagnosticAction.TIMELINE_RECONSTRUCT:
496
+ history = self.attacks.attack_history
497
+ return f"Timeline: {json.dumps(history[-10:], default=str)}", True
498
+ elif d == DiagnosticAction.VULNERABILITY_SCAN:
499
+ vulns = self.network.get_vulnerable_nodes()
500
+ vuln_strs = [f"{n.id} (max_vuln={max((p.vulnerability_score for p in n.ports), default=0):.2f})" for n in vulns]
501
+ return f"Vulnerable nodes: {', '.join(vuln_strs) if vuln_strs else 'None'}", True
502
+ elif d == DiagnosticAction.CHECK_EXECUTIVE_CONTEXT:
503
+ summary = self.executive_context.get_context_summary()
504
+ drift_events = self.executive_context.state.drift_events
505
+ migration_progress = self.migration_engine.get_progress()
506
+ war_room_transcript = self.war_room.get_latest_transcript()
507
+ return (
508
+ f"{summary}\n"
509
+ f"Migration: {migration_progress.get('current_phase','N/A')} "
510
+ f"({migration_progress.get('progress_pct', 0):.0%} done)\n"
511
+ f"War Room Latest: {war_room_transcript[:200]}"
512
+ ), True
513
+ return "Unknown diagnostic action", False
514
+
515
+ def _get_action_name(self, action: ImmunoAction) -> str:
516
+ if action.tactical_action:
517
+ return action.tactical_action.value
518
+ if action.strategic_action:
519
+ return action.strategic_action.value
520
+ if action.diagnostic_action:
521
+ return action.diagnostic_action.value
522
+ return ""
523
+
524
+ def _check_phase_transition(self) -> None:
525
+ """Auto-transition between incident phases based on meaningful progress.
526
+
527
+ Each transition requires REAL work, not just step counts:
528
+ - Detection → Containment: Agent must have scanned AND traced (identified the threat)
529
+ - Containment → RCA: ALL active attacks must be contained
530
+ - RCA Refactor: Belief map must have real accuracy AND multiple correlations
531
+ - Refactor Validation: Multiple org changes must have been made
532
+ """
533
+ phase = self._state.current_phase
534
+ active_attacks = self.attacks.get_active_attacks()
535
+
536
+ if phase == IncidentPhase.DETECTION:
537
+ # Require: at least 1 scan + 1 identification/trace action completed
538
+ has_scanned = self._state.scans_performed > 0 if hasattr(self._state, 'scans_performed') else self._state.step_count >= 2
539
+ has_identified = self._state.correct_identifications > 0 or len(self._state.contained_attacks) > 0
540
+ if has_scanned and (has_identified or self._state.step_count >= 4):
541
+ self._transition_phase(IncidentPhase.CONTAINMENT)
542
+ elif phase == IncidentPhase.CONTAINMENT:
543
+ # Require: ALL active attacks must be contained (no free passes)
544
+ if len(active_attacks) == 0:
545
+ self._transition_phase(IncidentPhase.ROOT_CAUSE_ANALYSIS)
546
+ elif phase == IncidentPhase.ROOT_CAUSE_ANALYSIS:
547
+ # Require: belief accuracy >= 0.4 AND at least 2 correlations
548
+ belief_acc = self.belief_map.calculate_belief_accuracy()
549
+ num_correlations = len(self.belief_map.state.correlations)
550
+ if belief_acc >= 0.4 and num_correlations >= 2:
551
+ self._transition_phase(IncidentPhase.ORG_REFACTOR)
552
+ elif num_correlations >= 3: # Allow through with more evidence even if accuracy is lower
553
+ self._transition_phase(IncidentPhase.ORG_REFACTOR)
554
+ elif phase == IncidentPhase.ORG_REFACTOR:
555
+ # Require: at least 2 organizational changes
556
+ if self._state.org_changes_made >= 2:
557
+ self._transition_phase(IncidentPhase.VALIDATION)
558
+
559
+ def _transition_phase(self, new_phase: IncidentPhase) -> None:
560
+ if new_phase != self._state.current_phase:
561
+ self._state.current_phase = new_phase
562
+ self._state.phase_history.append({"phase": new_phase.value, "step": self._state.step_count})
563
+
564
+ def _check_termination(self) -> bool:
565
+ if self._state.step_count >= self._state.max_steps:
566
+ self._state.truncated = True
567
+ self._state.termination_reason = "Max steps reached"
568
+ return True
569
+ if self._state.current_phase == IncidentPhase.VALIDATION and len(self.attacks.get_active_attacks()) == 0:
570
+ self._state.terminated = True
571
+ self._state.termination_reason = "Incident resolved — validation complete"
572
+ return True
573
+ all_critical = all(n.health <= 0 for n in self.network.get_all_nodes() if n.criticality > 0.7)
574
+ if all_critical:
575
+ self._state.terminated = True
576
+ self._state.termination_reason = "Total system failure"
577
+ return True
578
+ return False
579
+
580
+ def inject_directive(self, directive: str) -> None:
581
+ """Inject a board directive mid-simulation."""
582
+ self._state.directives.append(directive)
583
+ logging.info(f"Board Directive Injected: {directive}")
584
+
585
+ def _build_observation(self, action_result: str, action_success: bool) -> ImmunoObservation:
586
+ compromised_ids = {n.id for n in self.network.get_compromised_nodes()}
587
+ visible_nodes = []
588
+ for n in self.network.get_all_nodes():
589
+ if not n.compromised:
590
+ # Clean nodes are always visible
591
+ visible_nodes.append(n)
592
+ else:
593
+ # Compromised nodes: visibility depends on stealth and detection
594
+ stealth = self.attacks.active_attacks[0].stealth if self.attacks.active_attacks else 0.5
595
+ detection_chance = 0.3 + (1.0 - stealth) * 0.7
596
+ if self.rng.random() < detection_chance:
597
+ # Agent detects this compromised node
598
+ visible_nodes.append(n)
599
+ # else: node is hidden — fog of war
600
+
601
+ detected = [a for a in self.attacks.get_active_attacks()
602
+ if self.rng.random() < 0.4 + (1 - a.stealth) * 0.6]
603
+
604
+ # RAG Integration: Retrieve real-world CVE info for detected attacks
605
+ rag_info = []
606
+ if self.knowledge_base and detected:
607
+ for atk in detected:
608
+ info = self.knowledge_base.retrieve_cve_info(atk.vector.value)
609
+ rag_info.append(f"Threat {atk.id} ({atk.vector.value}): {info}")
610
+
611
+ recent_logs = []
612
+ for n in self.network.get_all_nodes():
613
+ recent_logs.extend(n.logs[-3:])
614
+ recent_logs.sort(key=lambda l: l.timestamp, reverse=True)
615
+
616
+ alerts = []
617
+ if self._state.threat_level > 0.7:
618
+ alerts.append(f"HIGH THREAT: Level {self._state.threat_level:.2f}")
619
+ if self.permissions.pending:
620
+ alerts.append(f"{len(self.permissions.pending)} approval(s) pending")
621
+
622
+ # Add RAG info to alerts
623
+ alerts.extend(rag_info)
624
+
625
+ return ImmunoObservation(
626
+ visible_nodes=visible_nodes,
627
+ visible_edges=self.network.get_all_edges(),
628
+ detected_attacks=detected,
629
+ recent_logs=recent_logs[:15],
630
+ network_health_summary=self.network.get_network_health(),
631
+ org_nodes=self.org.get_all_nodes(),
632
+ org_edges=self.org.get_active_edges(),
633
+ pending_approvals=self.permissions.pending,
634
+ action_result=action_result,
635
+ action_success=action_success,
636
+ approval_delay=self.permissions.get_average_approval_latency(),
637
+ current_phase=self._state.current_phase,
638
+ step_count=self._state.step_count,
639
+ sim_time=self._state.sim_time,
640
+ threat_level=min(1.0, max(0.0, self._state.threat_level)),
641
+ system_downtime=self._state.total_downtime,
642
+ belief_map_feedback=self.belief_map.generate_feedback() if self._state.step_count % 5 == 0 else "",
643
+ alerts=alerts,
644
+ directives=self._state.directives,
645
+ )
646
+