dino65-dev commited on
Commit
caa3f8b
·
1 Parent(s): c5bbfb6

Major: Self-evolving RL engine + OpenAI-compatible API simplification

Browse files

Self-Evolving Environment Engine (self_evolving/):
- α-Curriculum Reward from GenEnv: R_env(p̂) = -|p̂ - α|, targets zone of
proximal development (α≈0.5 success rate)
- POET-inspired mutation operators with adaptive σ (Gaussian mutation,
uniform crossover on scenario genomes)
- Fitness-proportionate selection with novelty search bonus
- Elo rating system for agent and scenario difficulty calibration
- Procedural scenario generator with 6 attack archetypes:
phishing, lateral movement, insider threat, ransomware, supply chain, APT
- ScenarioGenome: 15-parameter genetic representation with 5-dimensional
difficulty vector (evidence_obscurity, ioc_complexity, containment_complexity,
report_detail_required, time_pressure)
- Population-based evolution with elite preservation and archive

Environment Integration:
- task_id='evolved' activates self-evolving mode
- /env/evolve endpoint triggers population evolution
- /env/evolution-stats returns generation, Elo, difficulty range
- Performance auto-recorded after grading for fitness evaluation
- All 6 existing static tasks unchanged (mean baseline: 0.9442)

API Simplification:
- Replaced multi-provider auto-detection (OPENROUTER_API_KEY, ANTHROPIC_API_KEY,
LLM_API_KEY) with simple OpenAI-compatible pattern
- OPENAI_API_KEY + optional OPENAI_BASE_URL (defaults to api.openai.com/v1)
- Works with any OpenAI-compatible provider via --api-base flag

README.md CHANGED
@@ -37,6 +37,26 @@ The agent receives a cybersecurity alert and must:
37
  - **Anti-loop penalties**: Repeated identical actions receive increasing penalties, encouraging diverse investigation strategies.
38
  - **Phase discipline**: Agents are rewarded for following proper IR workflow (investigate before classify, classify before contain) and penalized for phase violations.
39
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  ---
41
 
42
  ## Action / Observation Space
@@ -257,35 +277,30 @@ OPENAI_API_KEY=sk-... python baseline_inference.py --verbose
257
 
258
  ### Supported LLM Providers
259
 
260
- The baseline inference script works with **any OpenAI-compatible API**. Set the appropriate environment variable or use `--api-key` / `--api-base` flags:
261
 
262
  ```bash
263
- # OpenAI (default)
264
  OPENAI_API_KEY=sk-... python baseline_inference.py --model gpt-4o-mini
265
 
266
  # OpenRouter — access 200+ models (Gemini, Claude, Llama, Mistral, etc.)
267
- OPENROUTER_API_KEY=sk-or-... python baseline_inference.py --model google/gemini-2.5-flash
268
 
269
  # Anthropic (OpenAI-compatible endpoint)
270
- ANTHROPIC_API_KEY=sk-ant-... python baseline_inference.py --model claude-sonnet-4-20250514
271
 
272
  # Local models via Ollama
273
  python baseline_inference.py --api-base http://localhost:11434/v1 --api-key dummy --model llama3
274
 
275
- # Any OpenAI-compatible provider
276
  python baseline_inference.py --api-key YOUR_KEY --api-base https://your-provider/v1 --model your-model
277
-
278
- # Universal override (works with any provider)
279
- LLM_API_KEY=... LLM_API_BASE=https://provider/v1 python baseline_inference.py --model model-name
280
  ```
281
 
282
- **Environment variables** (auto-detected in order):
283
- | Variable | Provider | API Base |
284
- |----------|----------|----------|
285
- | `LLM_API_KEY` + `LLM_API_BASE` | Any | Custom |
286
- | `OPENROUTER_API_KEY` | OpenRouter | `https://openrouter.ai/api/v1` |
287
- | `ANTHROPIC_API_KEY` | Anthropic | `https://api.anthropic.com/v1` |
288
- | `OPENAI_API_KEY` | OpenAI | `https://api.openai.com/v1` |
289
 
290
  ### Docker
291
 
@@ -326,6 +341,8 @@ openenv push
326
  | `/tasks` | GET | List all 6 tasks with descriptions and action schema |
327
  | `/grader` | GET | Get grader score after episode completion |
328
  | `/baseline` | POST | Run deterministic baseline on all 6 tasks |
 
 
329
 
330
  ### Example Interaction
331
 
@@ -401,6 +418,10 @@ incident-response-env/
401
  │ ├── task_medium_ransomware.py # Medium-Hard: Ransomware deployment
402
  │ ├── task_hard_supply_chain.py # Hard-Plus: Supply chain compromise
403
  │ └── task_expert_apt_zeroday.py # Expert: APT with zero-day exploitation
 
 
 
 
404
  ├── client.py # EnvClient implementation
405
  ├── openenv.yaml # OpenEnv configuration
406
  ├── pyproject.toml # Python project metadata
 
37
  - **Anti-loop penalties**: Repeated identical actions receive increasing penalties, encouraging diverse investigation strategies.
38
  - **Phase discipline**: Agents are rewarded for following proper IR workflow (investigate before classify, classify before contain) and penalized for phase violations.
39
 
40
+ ### Self-Evolving Environment (Unique Feature)
41
+
42
+ This environment features a **self-evolving scenario generation engine** that creates an open-ended curriculum of cybersecurity incidents:
43
+
44
+ - **α-Curriculum Reward**: Automatically generates scenarios in the agent's "zone of proximal development" (target success rate ~50%)
45
+ - **POET-inspired Evolution**: Population of scenario genomes with mutation, crossover, and fitness-proportionate selection
46
+ - **Novelty Search**: Diversity pressure ensures the environment explores the full space of possible incidents
47
+ - **Elo Rating System**: Both agent and scenarios are rated, providing natural difficulty calibration
48
+ - **Procedural Generation**: 6 attack archetypes (phishing, lateral movement, insider threat, ransomware, supply chain, APT) with parameterized complexity
49
+
50
+ Use `task_id="evolved"` to activate self-evolving mode:
51
+ ```python
52
+ # Activate self-evolving mode
53
+ obs = env.reset(task_id="evolved")
54
+ # ... run agent ...
55
+ # Trigger evolution based on performance
56
+ requests.post(f"{base_url}/env/evolve")
57
+ stats = requests.get(f"{base_url}/env/evolution-stats").json()
58
+ ```
59
+
60
  ---
61
 
62
  ## Action / Observation Space
 
277
 
278
  ### Supported LLM Providers
279
 
280
+ The baseline inference script works with **any OpenAI-compatible API**. Just set `OPENAI_API_KEY` and optionally `OPENAI_BASE_URL`:
281
 
282
  ```bash
283
+ # OpenAI (default — just set API key)
284
  OPENAI_API_KEY=sk-... python baseline_inference.py --model gpt-4o-mini
285
 
286
  # OpenRouter — access 200+ models (Gemini, Claude, Llama, Mistral, etc.)
287
+ OPENAI_API_KEY=sk-or-... OPENAI_BASE_URL=https://openrouter.ai/api/v1 python baseline_inference.py --model google/gemini-2.5-flash
288
 
289
  # Anthropic (OpenAI-compatible endpoint)
290
+ OPENAI_API_KEY=sk-ant-... OPENAI_BASE_URL=https://api.anthropic.com/v1 python baseline_inference.py --model claude-sonnet-4-20250514
291
 
292
  # Local models via Ollama
293
  python baseline_inference.py --api-base http://localhost:11434/v1 --api-key dummy --model llama3
294
 
295
+ # Any OpenAI-compatible provider (explicit flags)
296
  python baseline_inference.py --api-key YOUR_KEY --api-base https://your-provider/v1 --model your-model
 
 
 
297
  ```
298
 
299
+ **Environment variables:**
300
+ | Variable | Description |
301
+ |----------|-------------|
302
+ | `OPENAI_API_KEY` | API key (required) |
303
+ | `OPENAI_BASE_URL` | API base URL (optional, defaults to `https://api.openai.com/v1`) |
 
 
304
 
305
  ### Docker
306
 
 
341
  | `/tasks` | GET | List all 6 tasks with descriptions and action schema |
342
  | `/grader` | GET | Get grader score after episode completion |
343
  | `/baseline` | POST | Run deterministic baseline on all 6 tasks |
344
+ | `/env/evolve` | POST | Trigger evolution of the scenario population |
345
+ | `/env/evolution-stats` | GET | Get evolution engine statistics |
346
 
347
  ### Example Interaction
348
 
 
418
  │ ├── task_medium_ransomware.py # Medium-Hard: Ransomware deployment
419
  │ ├── task_hard_supply_chain.py # Hard-Plus: Supply chain compromise
420
  │ └── task_expert_apt_zeroday.py # Expert: APT with zero-day exploitation
421
+ ├── self_evolving/ # Self-evolving scenario generation engine
422
+ │ ├── __init__.py # Package exports
423
+ │ ├── evolution_engine.py # α-Curriculum, POET mutations, Elo ratings
424
+ │ └── scenario_generator.py # Procedural scenario generation from genomes
425
  ├── client.py # EnvClient implementation
426
  ├── openenv.yaml # OpenEnv configuration
427
  ├── pyproject.toml # Python project metadata
baseline_inference.py CHANGED
@@ -11,7 +11,7 @@ and produces reproducible baseline scores. It demonstrates how an AI agent
11
  interacts with the environment through the step/reset/state API.
12
 
13
  Supports ANY OpenAI-compatible LLM provider via the --api-base flag or
14
- auto-detection from environment variables.
15
 
16
  Architecture:
17
  This agent implements a reasoning loop with state-goal reflection,
@@ -32,29 +32,21 @@ Architecture:
32
  in the prompt context to avoid wasting steps on duplicate actions.
33
 
34
  Usage:
35
- # OpenAI (default):
36
  OPENAI_API_KEY=sk-... python baseline_inference.py
37
 
38
- # OpenRouter (auto-detected from env var):
39
- OPENROUTER_API_KEY=sk-or-... python baseline_inference.py --model google/gemini-2.5-flash
40
 
41
- # Anthropic (OpenAI-compatible endpoint):
42
- ANTHROPIC_API_KEY=sk-ant-... python baseline_inference.py --model claude-sonnet-4-20250514
43
-
44
- # Any provider via explicit flags:
45
- python baseline_inference.py --api-key KEY --api-base https://my-provider/v1 --model my-model
46
 
47
  # Local models (Ollama, vLLM, etc.):
48
  python baseline_inference.py --api-base http://localhost:11434/v1 --api-key dummy --model llama3
49
 
50
- # Run against a remote HF Space:
51
- OPENAI_API_KEY=sk-... python baseline_inference.py --base-url https://huggingface.co/spaces/your-user/incident-response-env
52
-
53
- Environment Variables (checked in order):
54
- LLM_API_KEY + LLM_API_BASE : Universal override for any provider
55
- OPENROUTER_API_KEY : OpenRouter (https://openrouter.ai/api/v1)
56
- ANTHROPIC_API_KEY : Anthropic (https://api.anthropic.com/v1)
57
- OPENAI_API_KEY : OpenAI (https://api.openai.com/v1) [default]
58
 
59
  Output:
60
  Prints scores for each task and aggregate results.
@@ -752,61 +744,48 @@ def run_llm_agent(
752
 
753
 
754
  # =============================================================================
755
- # Provider Auto-Detection
756
  # =============================================================================
757
-
758
- # Known provider configurations: (env_var_key, api_base_url, provider_name)
759
- PROVIDER_CONFIGS: List[Tuple[str, str, str]] = [
760
- ("LLM_API_KEY", os.environ.get("LLM_API_BASE", ""), "Custom"),
761
- ("OPENROUTER_API_KEY", "https://openrouter.ai/api/v1", "OpenRouter"),
762
- ("ANTHROPIC_API_KEY", "https://api.anthropic.com/v1", "Anthropic"),
763
- ("OPENAI_API_KEY", "https://api.openai.com/v1", "OpenAI"),
764
- ]
765
 
766
 
767
  def resolve_llm_config(
768
  cli_api_key: Optional[str] = None,
769
  cli_api_base: Optional[str] = None,
770
- ) -> Tuple[str, str, str]:
771
  """
772
- Resolve LLM API key, base URL, and provider name.
773
 
774
  Priority:
775
- 1. Explicit CLI flags (--api-key, --api-base)
776
- 2. Auto-detect from environment variables
777
 
778
- Returns:
779
- (api_key, api_base, provider_name)
780
 
781
- Raises:
782
- SystemExit if no API key is found.
783
  """
784
- # 1. CLI flags take highest priority
785
- if cli_api_key:
786
- base = cli_api_base or "https://api.openai.com/v1"
787
- return cli_api_key, base, "Custom" if cli_api_base else "OpenAI"
788
-
789
- # 2. Auto-detect from environment variables
790
- for env_key, default_base, provider in PROVIDER_CONFIGS:
791
- key = os.environ.get(env_key)
792
- if key:
793
- base = cli_api_base or default_base
794
- if not base:
795
- # LLM_API_KEY without LLM_API_BASE — fall back to OpenAI
796
- base = "https://api.openai.com/v1"
797
- return key, base, provider
798
-
799
- # No key found
800
- print("ERROR: No LLM API key found.")
801
- print()
802
- print("Set one of these environment variables:")
803
- print(" OPENAI_API_KEY - for OpenAI models")
804
- print(" OPENROUTER_API_KEY - for OpenRouter (any model)")
805
- print(" ANTHROPIC_API_KEY - for Anthropic models")
806
- print(" LLM_API_KEY - for any provider (set LLM_API_BASE too)")
807
- print()
808
- print("Or pass explicitly: --api-key YOUR_KEY --api-base https://provider/v1")
809
- sys.exit(1)
810
 
811
 
812
  def main():
@@ -849,17 +828,17 @@ def main():
849
  )
850
  args = parser.parse_args()
851
 
852
- # Resolve LLM provider
853
- api_key, api_base, provider_name = resolve_llm_config(
854
  cli_api_key=args.api_key,
855
  cli_api_base=args.api_base,
856
  )
857
 
858
  client = OpenAI(api_key=api_key, base_url=api_base)
859
 
860
- # Build extra headers for providers that need them
861
  extra_headers: Optional[Dict[str, str]] = None
862
- if provider_name == "OpenRouter" or "openrouter.ai" in api_base:
863
  extra_headers = {
864
  "HTTP-Referer": "https://github.com/incident-response-env",
865
  "X-Title": "Incident Response Triage Environment",
@@ -867,7 +846,7 @@ def main():
867
 
868
  print("=" * 60)
869
  print("Incident Response Triage - Baseline Inference")
870
- print(f"Provider: {provider_name} ({api_base})")
871
  print(f"Server: {args.base_url}")
872
  print(f"Model: {args.model}")
873
  print(f"Tasks: {args.tasks}")
 
11
  interacts with the environment through the step/reset/state API.
12
 
13
  Supports ANY OpenAI-compatible LLM provider via the --api-base flag or
14
+ environment variables.
15
 
16
  Architecture:
17
  This agent implements a reasoning loop with state-goal reflection,
 
32
  in the prompt context to avoid wasting steps on duplicate actions.
33
 
34
  Usage:
35
+ # OpenAI (default — just set API key):
36
  OPENAI_API_KEY=sk-... python baseline_inference.py
37
 
38
+ # Any OpenAI-compatible provider (set base URL):
39
+ OPENAI_API_KEY=sk-or-... OPENAI_BASE_URL=https://openrouter.ai/api/v1 python baseline_inference.py --model google/gemini-2.5-flash
40
 
41
+ # Explicit flags:
42
+ python baseline_inference.py --api-key KEY --api-base https://provider/v1 --model my-model
 
 
 
43
 
44
  # Local models (Ollama, vLLM, etc.):
45
  python baseline_inference.py --api-base http://localhost:11434/v1 --api-key dummy --model llama3
46
 
47
+ Environment Variables:
48
+ OPENAI_API_KEY : API key (required)
49
+ OPENAI_BASE_URL : API base URL (optional, defaults to https://api.openai.com/v1)
 
 
 
 
 
50
 
51
  Output:
52
  Prints scores for each task and aggregate results.
 
744
 
745
 
746
  # =============================================================================
747
+ # Configuration — OpenAI-Compatible API
748
  # =============================================================================
749
+ # Simple: provide an API key + optional base URL.
750
+ # If only API key given → defaults to OpenAI (https://api.openai.com/v1)
751
+ # If base URL also given → uses that provider (OpenRouter, Anthropic, local, etc.)
 
 
 
 
 
752
 
753
 
754
  def resolve_llm_config(
755
  cli_api_key: Optional[str] = None,
756
  cli_api_base: Optional[str] = None,
757
+ ) -> Tuple[str, str]:
758
  """
759
+ Resolve LLM API key and base URL.
760
 
761
  Priority:
762
+ 1. CLI flags (--api-key, --api-base)
763
+ 2. Environment variables (OPENAI_API_KEY + OPENAI_BASE_URL)
764
 
765
+ If only an API key is given (no base URL), defaults to https://api.openai.com/v1
 
766
 
767
+ Returns:
768
+ (api_key, api_base)
769
  """
770
+ # 1. CLI flags
771
+ api_key = cli_api_key or os.environ.get("OPENAI_API_KEY", "")
772
+ api_base = cli_api_base or os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1")
773
+
774
+ if not api_key:
775
+ print("ERROR: No API key found.")
776
+ print()
777
+ print("Provide an OpenAI-compatible API key:")
778
+ print(" OPENAI_API_KEY=sk-... python baseline_inference.py")
779
+ print()
780
+ print("For other providers, also set the base URL:")
781
+ print(" OPENAI_API_KEY=sk-or-... OPENAI_BASE_URL=https://openrouter.ai/api/v1 python baseline_inference.py")
782
+ print(" OPENAI_API_KEY=sk-ant-... OPENAI_BASE_URL=https://api.anthropic.com/v1 python baseline_inference.py")
783
+ print()
784
+ print("Or pass explicitly:")
785
+ print(" python baseline_inference.py --api-key KEY --api-base https://provider/v1")
786
+ sys.exit(1)
787
+
788
+ return api_key, api_base
 
 
 
 
 
 
 
789
 
790
 
791
  def main():
 
828
  )
829
  args = parser.parse_args()
830
 
831
+ # Resolve LLM provider (OpenAI-compatible)
832
+ api_key, api_base = resolve_llm_config(
833
  cli_api_key=args.api_key,
834
  cli_api_base=args.api_base,
835
  )
836
 
837
  client = OpenAI(api_key=api_key, base_url=api_base)
838
 
839
+ # Build extra headers for OpenRouter if detected
840
  extra_headers: Optional[Dict[str, str]] = None
841
+ if "openrouter.ai" in api_base:
842
  extra_headers = {
843
  "HTTP-Referer": "https://github.com/incident-response-env",
844
  "X-Title": "Incident Response Triage Environment",
 
846
 
847
  print("=" * 60)
848
  print("Incident Response Triage - Baseline Inference")
849
+ print(f"API: {api_base}")
850
  print(f"Server: {args.base_url}")
851
  print(f"Model: {args.model}")
852
  print(f"Tasks: {args.tasks}")
self_evolving/__init__.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2026 - OpenEnv Hackathon Submission
2
+ # Incident Response Triage Environment - Self-Evolving Engine Package
3
+ # BSD-3-Clause License
4
+
5
+ """
6
+ Self-Evolving Scenario Generation Engine.
7
+
8
+ Exports the core evolution engine classes and scenario generator
9
+ for procedurally generating, mutating, and adapting cybersecurity
10
+ scenarios based on agent performance.
11
+ """
12
+
13
+ try:
14
+ from .evolution_engine import (
15
+ AgentPerformanceRecord,
16
+ EvolutionEngine,
17
+ EvolutionState,
18
+ FitnessEvaluator,
19
+ MutationOperator,
20
+ ScenarioGenome,
21
+ )
22
+ from .scenario_generator import ScenarioGenerator
23
+ except ImportError:
24
+ from self_evolving.evolution_engine import (
25
+ AgentPerformanceRecord,
26
+ EvolutionEngine,
27
+ EvolutionState,
28
+ FitnessEvaluator,
29
+ MutationOperator,
30
+ ScenarioGenome,
31
+ )
32
+ from self_evolving.scenario_generator import ScenarioGenerator
33
+
34
+ __all__ = [
35
+ "AgentPerformanceRecord",
36
+ "EvolutionEngine",
37
+ "EvolutionState",
38
+ "FitnessEvaluator",
39
+ "MutationOperator",
40
+ "ScenarioGenome",
41
+ "ScenarioGenerator",
42
+ ]
self_evolving/evolution_engine.py ADDED
@@ -0,0 +1,589 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Self-Evolving Scenario Generation Engine
3
+
4
+ Implements:
5
+ 1. α-Curriculum Reward (from GenEnv): R_env(p̂) = -|p̂ - α| where α≈0.5
6
+ Rewards environment for generating scenarios in agent's "zone of proximal development"
7
+ 2. POET-inspired mutation operators: parametric mutation of scenario attributes
8
+ 3. Fitness-proportionate selection with novelty bonus
9
+ 4. Difficulty calibration via Elo-like rating system
10
+
11
+ Mathematical Framework:
12
+ - Each scenario S has a difficulty vector d ∈ R^k (k dimensions of difficulty)
13
+ - Agent competence vector c ∈ R^k estimated from performance history
14
+ - α-Curriculum: optimal scenario difficulty where P(agent solves | S) ≈ α
15
+ - Mutation: S' = mutate(S, σ) where σ is mutation strength adapted by fitness
16
+ - Fitness: F(S) = -|success_rate(S) - α| + λ * novelty(S)
17
+ - Novelty: measured as distance to k-nearest scenarios in behavior space
18
+ """
19
+
20
+ import copy
21
+ import hashlib
22
+ import json
23
+ import math
24
+ import random
25
+ from dataclasses import dataclass, field
26
+ from typing import Any, Dict, List, Optional, Set, Tuple
27
+
28
+ # IMPORTANT: Use relative imports that work both ways
29
+ try:
30
+ from ..tasks.base import (
31
+ EndpointInfo, LogEntry, Scenario, ThreatIntelEntry, UserProfile
32
+ )
33
+ from ..models import ContainmentAction, Severity, ThreatCategory
34
+ except ImportError:
35
+ import sys, os
36
+ _parent = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
37
+ if _parent not in sys.path:
38
+ sys.path.insert(0, _parent)
39
+ from tasks.base import (
40
+ EndpointInfo, LogEntry, Scenario, ThreatIntelEntry, UserProfile
41
+ )
42
+ from models import ContainmentAction, Severity, ThreatCategory
43
+
44
+
45
+ # ═══════════════════════════════════════════════════════════════════
46
+ # Data Structures
47
+ # ═══════════════════════════════════════════════════════════════════
48
+
49
+ @dataclass
50
+ class ScenarioGenome:
51
+ """
52
+ Genetic representation of a scenario for evolution.
53
+ Maps scenario parameters to a mutable genome vector.
54
+ """
55
+ # Core parameters (these get mutated)
56
+ num_log_entries: int = 12 # How many log entries (complexity)
57
+ num_critical_evidence: int = 5 # Critical evidence items to find
58
+ num_iocs: int = 4 # Number of IOCs
59
+ num_endpoints: int = 3 # Network endpoints
60
+ num_users: int = 2 # User profiles
61
+ num_threat_intel: int = 3 # Threat intel entries
62
+ num_containment_targets: int = 3 # Things to contain
63
+ max_steps: int = 25 # Steps allowed
64
+ noise_ratio: float = 0.3 # Ratio of noise/decoy evidence
65
+ correlation_depth: int = 2 # How many cross-source correlations
66
+ escalation_required: bool = True
67
+ multi_stage_attack: bool = False # Whether attack has multiple phases
68
+
69
+ # Difficulty dimensions (0.0 = easiest, 1.0 = hardest)
70
+ evidence_obscurity: float = 0.3 # How hidden is critical evidence
71
+ ioc_complexity: float = 0.3 # Sophistication of IOCs
72
+ containment_complexity: float = 0.3 # Number/precision of containment
73
+ report_detail_required: float = 0.3 # How detailed report must be
74
+ time_pressure: float = 0.3 # Steps vs required actions ratio
75
+
76
+ # Metadata
77
+ generation: int = 0
78
+ parent_id: Optional[str] = None
79
+ genome_id: str = ""
80
+
81
+ def __post_init__(self):
82
+ if not self.genome_id:
83
+ self.genome_id = hashlib.md5(
84
+ json.dumps(self.__dict__, default=str).encode()
85
+ ).hexdigest()[:12]
86
+
87
+ @property
88
+ def difficulty_vector(self) -> List[float]:
89
+ """k-dimensional difficulty vector."""
90
+ return [
91
+ self.evidence_obscurity,
92
+ self.ioc_complexity,
93
+ self.containment_complexity,
94
+ self.report_detail_required,
95
+ self.time_pressure,
96
+ ]
97
+
98
+ @property
99
+ def aggregate_difficulty(self) -> float:
100
+ """Single scalar difficulty score in [0, 1]."""
101
+ return sum(self.difficulty_vector) / len(self.difficulty_vector)
102
+
103
+
104
+ @dataclass
105
+ class AgentPerformanceRecord:
106
+ """Tracks agent performance for fitness evaluation."""
107
+ scenario_id: str
108
+ genome_id: str
109
+ score: float
110
+ steps_used: int
111
+ max_steps: int
112
+ evidence_found_ratio: float
113
+ iocs_found_ratio: float
114
+ correct_severity: bool
115
+ correct_category: bool
116
+ containment_score: float
117
+ report_quality: float
118
+ timestamp: float = 0.0
119
+
120
+
121
+ @dataclass
122
+ class EvolutionState:
123
+ """Persistent state of the evolution engine."""
124
+ generation: int = 0
125
+ population: List[ScenarioGenome] = field(default_factory=list)
126
+ archive: List[ScenarioGenome] = field(default_factory=list) # Hall of fame
127
+ performance_history: List[AgentPerformanceRecord] = field(default_factory=list)
128
+ agent_elo: float = 1000.0 # Agent Elo rating
129
+ scenario_elos: Dict[str, float] = field(default_factory=dict) # Per-scenario Elo
130
+
131
+
132
+ # ═══════════════════════════════════════════════════════════════════
133
+ # Mutation Operators
134
+ # ═══════════════════════════════════════════════════════════════════
135
+
136
+ class MutationOperator:
137
+ """
138
+ POET-inspired parametric mutation operators for scenario genomes.
139
+
140
+ Mutations are applied with adaptive strength σ based on the
141
+ α-curriculum signal: if scenarios are too easy, increase difficulty;
142
+ if too hard, decrease.
143
+ """
144
+
145
+ # Bounds for genome parameters
146
+ PARAM_BOUNDS = {
147
+ 'num_log_entries': (6, 30),
148
+ 'num_critical_evidence': (3, 12),
149
+ 'num_iocs': (2, 10),
150
+ 'num_endpoints': (2, 8),
151
+ 'num_users': (1, 5),
152
+ 'num_threat_intel': (2, 8),
153
+ 'num_containment_targets': (2, 8),
154
+ 'max_steps': (15, 40),
155
+ 'noise_ratio': (0.0, 0.6),
156
+ 'correlation_depth': (1, 5),
157
+ 'evidence_obscurity': (0.0, 1.0),
158
+ 'ioc_complexity': (0.0, 1.0),
159
+ 'containment_complexity': (0.0, 1.0),
160
+ 'report_detail_required': (0.0, 1.0),
161
+ 'time_pressure': (0.0, 1.0),
162
+ }
163
+
164
+ @staticmethod
165
+ def mutate(genome: ScenarioGenome, sigma: float = 0.15) -> ScenarioGenome:
166
+ """
167
+ Apply Gaussian mutation to genome parameters.
168
+
169
+ σ (sigma) controls mutation strength:
170
+ - Higher σ → more exploration (when agent is in comfort zone)
171
+ - Lower σ → fine-tuning (when near optimal difficulty)
172
+ """
173
+ child = copy.deepcopy(genome)
174
+ child.generation = genome.generation + 1
175
+ child.parent_id = genome.genome_id
176
+
177
+ # Mutate numeric parameters with Gaussian noise
178
+ for param, (lo, hi) in MutationOperator.PARAM_BOUNDS.items():
179
+ current = getattr(child, param)
180
+ if isinstance(current, float):
181
+ noise = random.gauss(0, sigma * (hi - lo))
182
+ new_val = max(lo, min(hi, current + noise))
183
+ setattr(child, param, round(new_val, 3))
184
+ elif isinstance(current, int):
185
+ noise = random.gauss(0, sigma * (hi - lo))
186
+ new_val = max(lo, min(hi, round(current + noise)))
187
+ setattr(child, param, int(new_val))
188
+
189
+ # Flip boolean traits with small probability
190
+ if random.random() < 0.15 * sigma:
191
+ child.escalation_required = not child.escalation_required
192
+ if random.random() < 0.15 * sigma:
193
+ child.multi_stage_attack = not child.multi_stage_attack
194
+
195
+ # Regenerate ID
196
+ child.genome_id = hashlib.md5(
197
+ json.dumps(child.__dict__, default=str).encode()
198
+ ).hexdigest()[:12]
199
+
200
+ return child
201
+
202
+ @staticmethod
203
+ def crossover(parent_a: ScenarioGenome, parent_b: ScenarioGenome) -> ScenarioGenome:
204
+ """Uniform crossover between two parent genomes."""
205
+ child = copy.deepcopy(parent_a)
206
+ child.generation = max(parent_a.generation, parent_b.generation) + 1
207
+ child.parent_id = f"{parent_a.genome_id}x{parent_b.genome_id}"
208
+
209
+ for param in MutationOperator.PARAM_BOUNDS:
210
+ if random.random() < 0.5:
211
+ setattr(child, param, getattr(parent_b, param))
212
+
213
+ if random.random() < 0.5:
214
+ child.escalation_required = parent_b.escalation_required
215
+ if random.random() < 0.5:
216
+ child.multi_stage_attack = parent_b.multi_stage_attack
217
+
218
+ child.genome_id = hashlib.md5(
219
+ json.dumps(child.__dict__, default=str).encode()
220
+ ).hexdigest()[:12]
221
+ return child
222
+
223
+
224
+ # ═══════════════════════════════════════════════════════════════════
225
+ # Fitness & Selection
226
+ # ═══════════════════════════════════════════════════════════════════
227
+
228
+ class FitnessEvaluator:
229
+ """
230
+ Evaluates scenario fitness using α-Curriculum reward.
231
+
232
+ Core formula: F(S) = -|p̂(S) - α| + λ * novelty(S) + β * info_gain(S)
233
+
234
+ Where:
235
+ - p̂(S) = estimated agent success probability on scenario S
236
+ - α = target success rate (0.5 = zone of proximal development)
237
+ - novelty(S) = average distance to k-nearest neighbors in archive
238
+ - info_gain(S) = how much new the scenario teaches the agent
239
+ """
240
+
241
+ def __init__(self, alpha: float = 0.5, lambda_novelty: float = 0.2,
242
+ beta_info: float = 0.1, k_nearest: int = 5):
243
+ self.alpha = alpha # Target success rate
244
+ self.lambda_novelty = lambda_novelty # Novelty weight
245
+ self.beta_info = beta_info # Information gain weight
246
+ self.k_nearest = k_nearest # For novelty computation
247
+
248
+ def compute_fitness(
249
+ self,
250
+ genome: ScenarioGenome,
251
+ performance_records: List[AgentPerformanceRecord],
252
+ archive: List[ScenarioGenome],
253
+ ) -> float:
254
+ """
255
+ Compute composite fitness score for a scenario genome.
256
+
257
+ Returns value in approximately [-1, 1] range.
258
+ Higher is better (more useful for training).
259
+ """
260
+ # 1. α-Curriculum component
261
+ alpha_reward = self._alpha_curriculum_reward(genome, performance_records)
262
+
263
+ # 2. Novelty component
264
+ novelty = self._compute_novelty(genome, archive)
265
+
266
+ # 3. Information gain estimate
267
+ info_gain = self._estimate_info_gain(genome, performance_records)
268
+
269
+ fitness = alpha_reward + self.lambda_novelty * novelty + self.beta_info * info_gain
270
+ return fitness
271
+
272
+ def _alpha_curriculum_reward(
273
+ self,
274
+ genome: ScenarioGenome,
275
+ records: List[AgentPerformanceRecord],
276
+ ) -> float:
277
+ """
278
+ α-Curriculum: R = -|p̂ - α|
279
+
280
+ Scenarios where the agent succeeds ~50% of the time are most useful
281
+ for learning (zone of proximal development).
282
+ """
283
+ # Estimate success probability from performance records
284
+ matching = [r for r in records if r.genome_id == genome.genome_id]
285
+ if not matching:
286
+ # No data — use difficulty as proxy
287
+ # Assume harder scenarios have lower success probability
288
+ estimated_p = 1.0 - genome.aggregate_difficulty
289
+ else:
290
+ estimated_p = sum(r.score for r in matching) / len(matching)
291
+
292
+ return -abs(estimated_p - self.alpha)
293
+
294
+ def _compute_novelty(
295
+ self, genome: ScenarioGenome, archive: List[ScenarioGenome]
296
+ ) -> float:
297
+ """
298
+ Novelty search: distance to k-nearest neighbors in difficulty space.
299
+ Encourages diverse scenario population.
300
+ """
301
+ if not archive:
302
+ return 1.0 # Maximum novelty if archive is empty
303
+
304
+ gv = genome.difficulty_vector
305
+ distances = []
306
+ for other in archive:
307
+ ov = other.difficulty_vector
308
+ dist = math.sqrt(sum((a - b) ** 2 for a, b in zip(gv, ov)))
309
+ distances.append(dist)
310
+
311
+ distances.sort()
312
+ k = min(self.k_nearest, len(distances))
313
+ avg_dist = sum(distances[:k]) / k if k > 0 else 0.0
314
+
315
+ # Normalize to [0, 1] (max possible distance in unit hypercube is sqrt(k_dims))
316
+ max_dist = math.sqrt(len(gv))
317
+ return min(avg_dist / max_dist, 1.0)
318
+
319
+ def _estimate_info_gain(
320
+ self,
321
+ genome: ScenarioGenome,
322
+ records: List[AgentPerformanceRecord],
323
+ ) -> float:
324
+ """
325
+ Estimate how much new information a scenario provides.
326
+ Scenarios that expose agent weaknesses score higher.
327
+ """
328
+ if not records:
329
+ return 0.5
330
+
331
+ # Look at what the agent is weak at
332
+ recent = records[-20:] # Last 20 episodes
333
+
334
+ weakness_dimensions = {
335
+ 'evidence_obscurity': 1.0 - (sum(r.evidence_found_ratio for r in recent) / len(recent)),
336
+ 'ioc_complexity': 1.0 - (sum(r.iocs_found_ratio for r in recent) / len(recent)),
337
+ 'containment_complexity': 1.0 - (sum(r.containment_score for r in recent) / len(recent)),
338
+ 'report_detail_required': 1.0 - (sum(r.report_quality for r in recent) / len(recent)),
339
+ }
340
+
341
+ # Scenarios that target agent weaknesses have higher info gain
342
+ dv = genome.difficulty_vector
343
+ dim_names = ['evidence_obscurity', 'ioc_complexity', 'containment_complexity',
344
+ 'report_detail_required', 'time_pressure']
345
+
346
+ info = 0.0
347
+ for i, dim_name in enumerate(dim_names):
348
+ if dim_name in weakness_dimensions:
349
+ # Higher difficulty in weak dimensions = more info gain
350
+ info += dv[i] * weakness_dimensions[dim_name]
351
+
352
+ return info / len(dim_names) if dim_names else 0.0
353
+
354
+
355
+ # ═══════════════════════════════════════════════════════════════════
356
+ # Evolution Engine (Main Class)
357
+ # ═══════════════════════════════════════════════════════════════════
358
+
359
+ class EvolutionEngine:
360
+ """
361
+ Self-evolving environment engine using POET + α-Curriculum.
362
+
363
+ Maintains a population of scenario genomes, evolves them based on
364
+ agent performance, and provides the next scenario to train on.
365
+
366
+ Usage:
367
+ engine = EvolutionEngine(population_size=10, alpha=0.5)
368
+ genome = engine.get_next_scenario()
369
+ scenario = engine.genome_to_scenario(genome)
370
+ # ... run agent on scenario ...
371
+ engine.record_performance(genome, performance_record)
372
+ engine.evolve() # Create next generation
373
+ """
374
+
375
+ def __init__(
376
+ self,
377
+ population_size: int = 10,
378
+ alpha: float = 0.5,
379
+ mutation_sigma: float = 0.15,
380
+ elite_fraction: float = 0.2,
381
+ archive_size: int = 50,
382
+ ):
383
+ self.population_size = population_size
384
+ self.mutation_sigma = mutation_sigma
385
+ self.elite_fraction = elite_fraction
386
+ self.archive_size = archive_size
387
+
388
+ self.fitness_evaluator = FitnessEvaluator(alpha=alpha)
389
+ self.state = EvolutionState()
390
+
391
+ # Initialize population with diverse seeds
392
+ self._initialize_population()
393
+
394
+ def _initialize_population(self):
395
+ """Create initial diverse population spanning difficulty space."""
396
+ templates = [
397
+ # Easy
398
+ ScenarioGenome(num_log_entries=8, num_critical_evidence=3, num_iocs=2,
399
+ num_endpoints=2, max_steps=25, noise_ratio=0.1,
400
+ evidence_obscurity=0.1, ioc_complexity=0.1,
401
+ containment_complexity=0.1, time_pressure=0.1),
402
+ # Medium
403
+ ScenarioGenome(num_log_entries=12, num_critical_evidence=5, num_iocs=4,
404
+ num_endpoints=3, max_steps=25, noise_ratio=0.25,
405
+ evidence_obscurity=0.35, ioc_complexity=0.35,
406
+ containment_complexity=0.35, time_pressure=0.3),
407
+ # Hard
408
+ ScenarioGenome(num_log_entries=18, num_critical_evidence=7, num_iocs=6,
409
+ num_endpoints=4, max_steps=30, noise_ratio=0.4,
410
+ evidence_obscurity=0.6, ioc_complexity=0.6,
411
+ containment_complexity=0.6, time_pressure=0.5),
412
+ # Expert
413
+ ScenarioGenome(num_log_entries=25, num_critical_evidence=10, num_iocs=8,
414
+ num_endpoints=6, max_steps=35, noise_ratio=0.5,
415
+ multi_stage_attack=True,
416
+ evidence_obscurity=0.85, ioc_complexity=0.85,
417
+ containment_complexity=0.85, time_pressure=0.7),
418
+ ]
419
+
420
+ # Fill population by mutating templates
421
+ self.state.population = []
422
+ for i in range(self.population_size):
423
+ template = templates[i % len(templates)]
424
+ if i < len(templates):
425
+ genome = copy.deepcopy(template)
426
+ else:
427
+ genome = MutationOperator.mutate(template, sigma=0.3)
428
+ genome.genome_id = hashlib.md5(
429
+ f"init_{i}_{json.dumps(genome.__dict__, default=str)}".encode()
430
+ ).hexdigest()[:12]
431
+ self.state.population.append(genome)
432
+
433
+ def get_next_scenario_genome(self) -> ScenarioGenome:
434
+ """
435
+ Select the next scenario genome for the agent to train on.
436
+ Uses fitness-proportionate selection favoring scenarios near α.
437
+ """
438
+ if not self.state.population:
439
+ self._initialize_population()
440
+
441
+ # Compute fitness for each genome
442
+ fitnesses = []
443
+ for genome in self.state.population:
444
+ f = self.fitness_evaluator.compute_fitness(
445
+ genome, self.state.performance_history, self.state.archive
446
+ )
447
+ fitnesses.append(f)
448
+
449
+ # Softmax selection (temperature-based)
450
+ temperature = 0.5
451
+ max_f = max(fitnesses) if fitnesses else 0
452
+ exp_f = [math.exp((f - max_f) / temperature) for f in fitnesses]
453
+ total = sum(exp_f)
454
+ probs = [e / total for e in exp_f]
455
+
456
+ # Weighted random selection
457
+ selected = random.choices(self.state.population, weights=probs, k=1)[0]
458
+ return selected
459
+
460
+ def record_performance(self, genome: ScenarioGenome, record: AgentPerformanceRecord):
461
+ """Record agent performance on a scenario for fitness evaluation."""
462
+ self.state.performance_history.append(record)
463
+
464
+ # Update Elo ratings
465
+ self._update_elo(genome, record)
466
+
467
+ # Keep history bounded
468
+ if len(self.state.performance_history) > 500:
469
+ self.state.performance_history = self.state.performance_history[-300:]
470
+
471
+ def evolve(self) -> List[ScenarioGenome]:
472
+ """
473
+ Evolve the scenario population using:
474
+ 1. Fitness evaluation
475
+ 2. Elite preservation
476
+ 3. Mutation + crossover
477
+ 4. Archive update (novelty-based hall of fame)
478
+
479
+ Returns the new population.
480
+ """
481
+ self.state.generation += 1
482
+
483
+ # Evaluate fitness
484
+ scored = []
485
+ for genome in self.state.population:
486
+ f = self.fitness_evaluator.compute_fitness(
487
+ genome, self.state.performance_history, self.state.archive
488
+ )
489
+ scored.append((genome, f))
490
+
491
+ scored.sort(key=lambda x: x[1], reverse=True)
492
+
493
+ # Elite preservation
494
+ n_elite = max(1, int(self.population_size * self.elite_fraction))
495
+ elites = [g for g, _ in scored[:n_elite]]
496
+
497
+ # Add best to archive
498
+ for genome, fitness in scored[:2]:
499
+ if len(self.state.archive) < self.archive_size:
500
+ self.state.archive.append(copy.deepcopy(genome))
501
+ elif fitness > 0: # Only archive reasonably fit scenarios
502
+ # Replace least novel archive member
503
+ self.state.archive.append(copy.deepcopy(genome))
504
+ if len(self.state.archive) > self.archive_size:
505
+ # Remove least novel
506
+ novelties = [
507
+ self.fitness_evaluator._compute_novelty(g, self.state.archive)
508
+ for g in self.state.archive
509
+ ]
510
+ min_idx = novelties.index(min(novelties))
511
+ self.state.archive.pop(min_idx)
512
+
513
+ # Adaptive mutation strength
514
+ # If scenarios are too easy (high avg score), increase σ to find harder ones
515
+ # If too hard (low avg score), decrease σ to fine-tune
516
+ recent_scores = [r.score for r in self.state.performance_history[-20:]]
517
+ if recent_scores:
518
+ avg_score = sum(recent_scores) / len(recent_scores)
519
+ # σ peaks when avg_score is far from α
520
+ sigma = self.mutation_sigma * (1.0 + abs(avg_score - self.fitness_evaluator.alpha))
521
+ else:
522
+ sigma = self.mutation_sigma
523
+
524
+ # Generate children
525
+ new_population = list(elites)
526
+ while len(new_population) < self.population_size:
527
+ if random.random() < 0.7:
528
+ # Mutation
529
+ parent = random.choice(scored[:max(3, len(scored) // 2)])[0]
530
+ child = MutationOperator.mutate(parent, sigma=sigma)
531
+ else:
532
+ # Crossover
533
+ p1, p2 = random.sample(scored[:max(3, len(scored) // 2)], 2)
534
+ child = MutationOperator.crossover(p1[0], p2[0])
535
+ child = MutationOperator.mutate(child, sigma=sigma * 0.5)
536
+
537
+ new_population.append(child)
538
+
539
+ self.state.population = new_population[:self.population_size]
540
+ return self.state.population
541
+
542
+ def _update_elo(self, genome: ScenarioGenome, record: AgentPerformanceRecord):
543
+ """
544
+ Update Elo ratings for agent and scenario.
545
+
546
+ Agent 'wins' if score > 0.7, 'loses' if score < 0.3, 'draw' otherwise.
547
+ This gives a natural difficulty calibration system.
548
+ """
549
+ K = 32 # Elo K-factor
550
+
551
+ agent_elo = self.state.agent_elo
552
+ scenario_elo = self.state.scenario_elos.get(genome.genome_id, 1000.0)
553
+
554
+ # Expected scores
555
+ ea = 1.0 / (1.0 + 10 ** ((scenario_elo - agent_elo) / 400))
556
+ es = 1.0 - ea
557
+
558
+ # Actual outcome
559
+ if record.score > 0.7:
560
+ sa, ss = 1.0, 0.0 # Agent wins
561
+ elif record.score < 0.3:
562
+ sa, ss = 0.0, 1.0 # Scenario wins
563
+ else:
564
+ sa, ss = 0.5, 0.5 # Draw
565
+
566
+ self.state.agent_elo = agent_elo + K * (sa - ea)
567
+ self.state.scenario_elos[genome.genome_id] = scenario_elo + K * (ss - es)
568
+
569
+ def get_evolution_stats(self) -> Dict[str, Any]:
570
+ """Get statistics about the current evolution state."""
571
+ recent = self.state.performance_history[-20:]
572
+ return {
573
+ "generation": self.state.generation,
574
+ "population_size": len(self.state.population),
575
+ "archive_size": len(self.state.archive),
576
+ "total_episodes": len(self.state.performance_history),
577
+ "agent_elo": round(self.state.agent_elo, 1),
578
+ "avg_recent_score": round(
579
+ sum(r.score for r in recent) / len(recent), 4
580
+ ) if recent else None,
581
+ "avg_difficulty": round(
582
+ sum(g.aggregate_difficulty for g in self.state.population)
583
+ / len(self.state.population), 3
584
+ ) if self.state.population else None,
585
+ "difficulty_range": {
586
+ "min": round(min(g.aggregate_difficulty for g in self.state.population), 3),
587
+ "max": round(max(g.aggregate_difficulty for g in self.state.population), 3),
588
+ } if self.state.population else None,
589
+ }
self_evolving/scenario_generator.py ADDED
@@ -0,0 +1,740 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Procedural Scenario Generator
3
+
4
+ Converts ScenarioGenome parameters into fully-realized Scenario objects
5
+ with realistic cybersecurity content (log entries, IOCs, endpoints, etc).
6
+
7
+ Uses template-based generation with parameterized complexity to ensure
8
+ scenarios are both varied and solvable.
9
+ """
10
+
11
+ import hashlib
12
+ import random
13
+ from typing import Dict, List, Optional, Set, Tuple
14
+
15
+ try:
16
+ from ..tasks.base import (
17
+ EndpointInfo, LogEntry, Scenario, ThreatIntelEntry, UserProfile
18
+ )
19
+ from ..models import ContainmentAction, Severity, ThreatCategory
20
+ except ImportError:
21
+ import sys, os
22
+ _parent = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
23
+ if _parent not in sys.path:
24
+ sys.path.insert(0, _parent)
25
+ from tasks.base import (
26
+ EndpointInfo, LogEntry, Scenario, ThreatIntelEntry, UserProfile
27
+ )
28
+ from models import ContainmentAction, Severity, ThreatCategory
29
+
30
+ try:
31
+ from .evolution_engine import ScenarioGenome
32
+ except ImportError:
33
+ from self_evolving.evolution_engine import ScenarioGenome
34
+
35
+
36
+ # ═══════════════════════════════════════════════════════════════════
37
+ # Content Templates (realistic cybersecurity data)
38
+ # ═══════════════════════════════════════════════════════════════════
39
+
40
+ ATTACK_TEMPLATES = {
41
+ "phishing": {
42
+ "categories": [ThreatCategory.PHISHING, ThreatCategory.MALWARE],
43
+ "severities": [Severity.HIGH, Severity.MEDIUM],
44
+ "alert_templates": [
45
+ "Suspicious email with malicious attachment detected targeting {user}",
46
+ "Spear phishing campaign detected - credential harvesting attempt on {user}",
47
+ "Business email compromise attempt flagged by DLP - sender: {attacker_email}",
48
+ ],
49
+ "log_templates": {
50
+ "email": [
51
+ "Email received from {attacker_email} to {user}@corp.local - Subject: '{subject}' - Attachment: {filename}",
52
+ "DMARC FAIL for sender domain {attacker_domain} - SPF: fail, DKIM: fail",
53
+ "Attachment {filename} (SHA256: {file_hash}) downloaded by {user}",
54
+ ],
55
+ "edr": [
56
+ "Process {proc_name} spawned by outlook.exe on {hostname} - PID: {pid}",
57
+ "Suspicious DLL injection detected: {filename} loaded into {proc_name}",
58
+ "Network beacon detected from {hostname} to {c2_ip}:{c2_port} every {interval}s",
59
+ ],
60
+ "proxy": [
61
+ "HTTP POST to {c2_domain}/beacon - User-Agent: {ua} - Host: {hostname}",
62
+ "Data upload detected: {hostname} -> {c2_ip} ({data_size}MB)",
63
+ "SSL connection to known malicious domain: {c2_domain} from {hostname}",
64
+ ],
65
+ "auth": [
66
+ "Successful login for {user} from {src_ip} at {timestamp}",
67
+ "Failed login attempt for {user} - source: {src_ip} - {attempts} attempts",
68
+ ],
69
+ "firewall": [
70
+ "ALLOW: {hostname}({src_ip}) -> {c2_ip}:443 (HTTPS) - {bytes} bytes",
71
+ "BLOCK: {c2_ip} -> {hostname}:{port} (reverse shell attempt)",
72
+ ],
73
+ "dns": [
74
+ "DNS query: {c2_domain} -> {c2_ip} from {hostname}",
75
+ "Suspicious DNS TXT record query: {domain} from {hostname}",
76
+ ],
77
+ },
78
+ "containment_actions": [
79
+ (ContainmentAction.QUARANTINE_FILE, "{file_hash}"),
80
+ (ContainmentAction.ISOLATE_HOST, "{hostname}"),
81
+ (ContainmentAction.BLOCK_IP, "{c2_ip}"),
82
+ (ContainmentAction.DISABLE_ACCOUNT, "{user}"),
83
+ ],
84
+ "escalation": "tier3",
85
+ "report_keywords": ["phishing", "malicious attachment", "C2 beacon", "credential theft"],
86
+ },
87
+ "lateral_movement": {
88
+ "categories": [ThreatCategory.LATERAL_MOVEMENT, ThreatCategory.BRUTE_FORCE],
89
+ "severities": [Severity.CRITICAL, Severity.HIGH],
90
+ "alert_templates": [
91
+ "Multiple failed authentication attempts followed by lateral movement from {hostname}",
92
+ "Credential stuffing attack detected - {attempts} failed logins then successful RDP to {target_host}",
93
+ "Pass-the-hash attack detected: {user} authenticating across multiple systems",
94
+ ],
95
+ "log_templates": {
96
+ "auth": [
97
+ "Failed login: {user} from {src_ip} - {attempts} attempts in {window}min",
98
+ "Successful login: {user} from {src_ip} after {failed_count} failures",
99
+ "Kerberos TGT request: {user} from {hostname} - ticket type: {ticket_type}",
100
+ "NTLM authentication: {user} from {src_ip} to {target_host}",
101
+ ],
102
+ "edr": [
103
+ "PsExec.exe execution detected on {target_host} from {hostname}",
104
+ "Mimikatz-like memory access detected on {hostname} - LSASS.exe read",
105
+ "WMI remote execution: {hostname} -> {target_host} - command: {command}",
106
+ "Credential dump detected: {hostname} - tool: {tool_name}",
107
+ ],
108
+ "firewall": [
109
+ "ALLOW: {hostname}({src_ip}) -> {target_host}:3389 (RDP)",
110
+ "ALLOW: {hostname} -> {target_host}:445 (SMB)",
111
+ "Spike in traffic: {hostname} -> internal subnets ({connection_count} connections in {window}min)",
112
+ ],
113
+ "dns": [
114
+ "DNS query: {target_host}.corp.local from {hostname}",
115
+ "Internal DNS enumeration detected from {hostname} - {query_count} queries",
116
+ ],
117
+ "proxy": [
118
+ "Internal proxy: {hostname} -> {target_host}:8080 ({data_size}MB transferred)",
119
+ ],
120
+ "email": [
121
+ "No relevant email logs for this incident type",
122
+ ],
123
+ },
124
+ "containment_actions": [
125
+ (ContainmentAction.ISOLATE_HOST, "{hostname}"),
126
+ (ContainmentAction.ISOLATE_HOST, "{target_host}"),
127
+ (ContainmentAction.DISABLE_ACCOUNT, "{user}"),
128
+ (ContainmentAction.REVOKE_SESSIONS, "{user}"),
129
+ (ContainmentAction.BLOCK_IP, "{src_ip}"),
130
+ ],
131
+ "escalation": "tier3",
132
+ "report_keywords": ["lateral movement", "credential theft", "brute force", "RDP", "privilege escalation"],
133
+ },
134
+ "insider_threat": {
135
+ "categories": [ThreatCategory.INSIDER_THREAT, ThreatCategory.DATA_EXFILTRATION],
136
+ "severities": [Severity.CRITICAL, Severity.HIGH],
137
+ "alert_templates": [
138
+ "Data exfiltration alert: {user} transferring sensitive files to external storage",
139
+ "Insider threat indicator: {user} accessing files outside normal scope at unusual hours",
140
+ "DLP alert: Bulk download of confidential documents by {user}",
141
+ ],
142
+ "log_templates": {
143
+ "email": [
144
+ "Email from {user}@corp.local to {ext_email} - {attachment_count} attachments ({total_size}MB)",
145
+ "Email forwarding rule created by {user} to {ext_email}",
146
+ ],
147
+ "auth": [
148
+ "Off-hours login: {user} at {timestamp} (normal hours: 9-17)",
149
+ "VPN connection: {user} from {src_ip} ({geo_location})",
150
+ "Privilege escalation: {user} added to {group_name} group",
151
+ ],
152
+ "edr": [
153
+ "USB device connected: {device_name} on {hostname} - {user} session",
154
+ "File copy to removable media: {file_count} files ({total_size}MB) by {user}",
155
+ "Screen capture tool detected: {tool_name} on {hostname}",
156
+ ],
157
+ "proxy": [
158
+ "Upload to {cloud_service}: {hostname} ({user}) - {data_size}MB",
159
+ "Connection to file sharing site: {cloud_service} from {hostname}",
160
+ ],
161
+ "dns": [
162
+ "DNS query: {cloud_service_domain} from {hostname}",
163
+ ],
164
+ "firewall": [
165
+ "Outbound data transfer: {hostname} -> {ext_ip}:{port} ({data_size}MB)",
166
+ "ALLOW: {hostname} -> {ext_ip}:443 (HTTPS to cloud storage)",
167
+ ],
168
+ },
169
+ "containment_actions": [
170
+ (ContainmentAction.DISABLE_ACCOUNT, "{user}"),
171
+ (ContainmentAction.REVOKE_SESSIONS, "{user}"),
172
+ (ContainmentAction.ISOLATE_HOST, "{hostname}"),
173
+ ],
174
+ "escalation": "management",
175
+ "report_keywords": ["insider threat", "data exfiltration", "unauthorized access", "sensitive data"],
176
+ },
177
+ "ransomware": {
178
+ "categories": [ThreatCategory.RANSOMWARE, ThreatCategory.MALWARE],
179
+ "severities": [Severity.CRITICAL],
180
+ "alert_templates": [
181
+ "Ransomware activity detected: Mass file encryption on {target_host}",
182
+ "File encryption alert: {file_count} files encrypted on {target_host} in {window} minutes",
183
+ "Known ransomware variant {malware_name} detected on {hostname}",
184
+ ],
185
+ "log_templates": {
186
+ "edr": [
187
+ "Mass file modification: {file_count} files renamed to .{extension} on {target_host}",
188
+ "Suspicious process: {proc_name} modifying files in {directory} on {target_host}",
189
+ "Ransom note created: {ransom_file} in {directory} on {target_host}",
190
+ "Shadow copy deletion: vssadmin.exe delete shadows on {target_host}",
191
+ ],
192
+ "auth": [
193
+ "Service account {user} used to access {target_host} from {hostname}",
194
+ "Failed RDP attempts: {hostname} -> {target_host} ({attempts} attempts)",
195
+ "Successful authentication: {user} on {target_host} via {auth_method}",
196
+ ],
197
+ "firewall": [
198
+ "ALLOW: {hostname} -> {c2_ip}:443 (C2 communication)",
199
+ "ALLOW: {hostname} -> {target_host}:445 (SMB lateral movement)",
200
+ "BLOCK: {target_host} -> {c2_ip}:{port} (ransom payment page)",
201
+ ],
202
+ "dns": [
203
+ "DNS query: {c2_domain} from {hostname}",
204
+ "DNS query: {ransom_domain}.onion.to from {target_host}",
205
+ ],
206
+ "proxy": [
207
+ "TOR traffic detected from {target_host}",
208
+ "HTTP GET to {c2_domain}/key - Encrypted payload received",
209
+ ],
210
+ "email": [
211
+ "Phishing email to {user}: '{subject}' with malicious macro attachment",
212
+ ],
213
+ },
214
+ "containment_actions": [
215
+ (ContainmentAction.ISOLATE_HOST, "{hostname}"),
216
+ (ContainmentAction.ISOLATE_HOST, "{target_host}"),
217
+ (ContainmentAction.BLOCK_IP, "{c2_ip}"),
218
+ (ContainmentAction.DISABLE_ACCOUNT, "{user}"),
219
+ (ContainmentAction.QUARANTINE_FILE, "{file_hash}"),
220
+ ],
221
+ "escalation": "tier3",
222
+ "report_keywords": ["ransomware", "encryption", "C2", "lateral movement", "shadow copies"],
223
+ },
224
+ "supply_chain": {
225
+ "categories": [ThreatCategory.SUPPLY_CHAIN, ThreatCategory.MALWARE],
226
+ "severities": [Severity.CRITICAL],
227
+ "alert_templates": [
228
+ "Supply chain compromise detected: Backdoor in {software_name} update",
229
+ "Compromised third-party library {library_name} detected in production",
230
+ "Suspicious code execution from auto-updated {software_name} package",
231
+ ],
232
+ "log_templates": {
233
+ "edr": [
234
+ "Code execution from {software_name} update: {proc_name} spawned {child_proc}",
235
+ "Backdoor detected: {file_name} in {install_path} ({file_hash})",
236
+ "Persistence mechanism: Registry key added by {proc_name} on {hostname}",
237
+ "Memory injection detected: {proc_name} injected into {target_proc}",
238
+ ],
239
+ "dns": [
240
+ "DNS query to staging server: {staging_domain} from {hostname}",
241
+ "DNS tunneling detected: long subdomain queries to {c2_domain} from {hostname}",
242
+ ],
243
+ "firewall": [
244
+ "ALLOW: {hostname} -> {c2_ip}:{port} (backdoor C2)",
245
+ "ALLOW: {hostname} -> {staging_ip}:443 (data staging)",
246
+ ],
247
+ "proxy": [
248
+ "HTTPS to {staging_domain}: {hostname} - Certificate mismatch detected",
249
+ "Data upload: {hostname} -> {staging_domain} ({data_size}MB)",
250
+ ],
251
+ "auth": [
252
+ "Service account {svc_account} created by {software_name} installer",
253
+ "Elevated privileges: {svc_account} added to Administrators group on {hostname}",
254
+ ],
255
+ "email": [
256
+ "Vendor notification email from {vendor_email} about update {version}",
257
+ ],
258
+ },
259
+ "containment_actions": [
260
+ (ContainmentAction.ISOLATE_HOST, "{hostname}"),
261
+ (ContainmentAction.QUARANTINE_FILE, "{file_hash}"),
262
+ (ContainmentAction.BLOCK_IP, "{c2_ip}"),
263
+ (ContainmentAction.DISABLE_ACCOUNT, "{svc_account}"),
264
+ ],
265
+ "escalation": "management",
266
+ "report_keywords": ["supply chain", "backdoor", "compromise", "third-party", "update"],
267
+ },
268
+ "apt_zeroday": {
269
+ "categories": [ThreatCategory.APT_ZERO_DAY],
270
+ "severities": [Severity.CRITICAL],
271
+ "alert_templates": [
272
+ "APT activity detected: Zero-day exploit in {software_name} targeting {target_host}",
273
+ "Advanced persistent threat: Multi-stage attack with unknown exploit on {hostname}",
274
+ "Sophisticated attack chain: {exploit_name} vulnerability exploitation with DNS tunneling",
275
+ ],
276
+ "log_templates": {
277
+ "edr": [
278
+ "Zero-day exploit: Buffer overflow in {software_name} on {target_host} (CVE pending)",
279
+ "Post-exploitation: {tool_name} dropped in {directory} on {target_host}",
280
+ "Living-off-the-land: {proc_name} executing encoded PowerShell on {hostname}",
281
+ "DCSync attack detected: {user} replicating AD credentials from {dc_host}",
282
+ "Keylogger installed: {file_name} injected into {target_proc} on {hostname}",
283
+ ],
284
+ "dns": [
285
+ "DNS tunneling: {subdomain}.{c2_domain} from {hostname} ({query_count} queries/hr)",
286
+ "DNS C2 channel: TXT record responses from {c2_domain} to {hostname}",
287
+ "Fast-flux DNS detected: {c2_domain} resolving to {ip_count} IPs",
288
+ ],
289
+ "auth": [
290
+ "Golden ticket detected: Kerberos ticket for {user} with suspicious lifetime",
291
+ "Admin account compromise: {admin_user} authenticating from {attacker_ip}",
292
+ "Privilege escalation: {user} -> {admin_user} via {technique}",
293
+ ],
294
+ "firewall": [
295
+ "ALLOW: {hostname} -> {c2_ip}:{port} (encrypted C2)",
296
+ "Covert channel: {hostname} -> {c2_ip} via ICMP (data embedded in payload)",
297
+ "Suspicious outbound: {target_host} -> {exfil_ip}:53 ({data_size}MB via DNS)",
298
+ ],
299
+ "proxy": [
300
+ "Encrypted traffic anomaly: {hostname} -> {c2_ip} ({data_size}MB, high entropy)",
301
+ "Certificate pinning bypass detected from {hostname} to {c2_domain}",
302
+ ],
303
+ "email": [
304
+ "Initial access: Spear phishing to {user} from {attacker_email} with zero-day PDF",
305
+ ],
306
+ },
307
+ "containment_actions": [
308
+ (ContainmentAction.ISOLATE_HOST, "{hostname}"),
309
+ (ContainmentAction.ISOLATE_HOST, "{target_host}"),
310
+ (ContainmentAction.BLOCK_IP, "{c2_ip}"),
311
+ (ContainmentAction.DISABLE_ACCOUNT, "{user}"),
312
+ (ContainmentAction.DISABLE_ACCOUNT, "{admin_user}"),
313
+ (ContainmentAction.QUARANTINE_FILE, "{file_hash}"),
314
+ ],
315
+ "escalation": "legal",
316
+ "report_keywords": ["APT", "zero-day", "DNS tunneling", "DCSync", "advanced persistent threat"],
317
+ },
318
+ }
319
+
320
+ # Random data pools for filling templates
321
+ IP_POOL = [
322
+ "185.220.101.42", "203.0.113.50", "198.51.100.23", "192.0.2.100",
323
+ "45.33.32.156", "94.102.49.190", "195.123.237.18", "104.248.29.53",
324
+ "64.225.8.203", "161.35.38.117", "178.128.83.12", "134.209.76.34",
325
+ "159.89.115.241", "167.172.182.15", "142.93.118.76", "68.183.44.143",
326
+ "10.50.25.101", "10.50.25.102", "10.50.25.103", "10.50.25.104",
327
+ ]
328
+ DOMAIN_POOL = [
329
+ "malware-cdn.evil.com", "secure-update.net", "cdn-analytics.cloud",
330
+ "api-sync.services", "update-service.io", "data-backup.cloud",
331
+ "metrics-relay.net", "auth-verify.cloud", "cloud-sync.services",
332
+ "cdn-static.cloud", "api-gateway.services", "log-collector.net",
333
+ ]
334
+ HOSTNAME_POOL = [
335
+ "WS-USER01-PC", "WS-ADMIN-PC", "SRV-DC-01", "SRV-FILE-01",
336
+ "SRV-WEB-01", "SRV-DB-01", "WS-DEV-PC", "SRV-APP-01",
337
+ "SRV-MAIL-01", "WS-FINANCE-PC", "SRV-BACKUP-01", "SRV-DNS-01",
338
+ ]
339
+ USERNAME_POOL = [
340
+ "jsmith", "agarcia", "mbrown", "klee", "tnguyen",
341
+ "dwilson", "schen", "rjohnson", "lpatel", "mkim",
342
+ "svc_backup", "svc_deploy", "admin_ops", "svc_monitor",
343
+ ]
344
+ HASH_POOL = [
345
+ "e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6",
346
+ "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2",
347
+ "f0e1d2c3b4a5f6e7d8c9b0a1f2e3d4c5b6a7f8e9d0c1b2a3f4e5d6c7b8a9f0e1",
348
+ "c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8",
349
+ ]
350
+ FILENAMES = [
351
+ "svchost_update.exe", "chrome_helper.dll", "sys32_patch.bin",
352
+ "update_agent.ps1", "backup_tool.py", "report.xlsm",
353
+ "invoice_Q4.docm", "driver_update.exe", "monitoring_agent.dll",
354
+ ]
355
+
356
+
357
+ class ScenarioGenerator:
358
+ """
359
+ Procedural scenario generator that converts genome parameters
360
+ into fully-realized Scenario objects.
361
+ """
362
+
363
+ def __init__(self, seed: Optional[int] = None):
364
+ if seed is not None:
365
+ random.seed(seed)
366
+
367
+ def generate(self, genome: ScenarioGenome, seed: Optional[int] = None) -> Scenario:
368
+ """
369
+ Generate a complete Scenario from a ScenarioGenome.
370
+
371
+ The genome's parameters control:
372
+ - Number and complexity of evidence items
373
+ - Number of IOCs and their types
374
+ - Network topology (endpoints, users)
375
+ - Time pressure (max_steps vs required actions)
376
+ - Noise level (decoy/irrelevant evidence)
377
+ """
378
+ if seed is not None:
379
+ random.seed(seed)
380
+
381
+ # Select attack type based on difficulty
382
+ attack_type = self._select_attack_type(genome)
383
+ template = ATTACK_TEMPLATES[attack_type]
384
+
385
+ # Generate scenario variables
386
+ variables = self._generate_variables(genome)
387
+
388
+ # Build scenario components
389
+ severity = self._select_severity(genome, template)
390
+ category = random.choice(template["categories"])
391
+
392
+ log_entries = self._generate_logs(genome, template, variables)
393
+ threat_intel = self._generate_threat_intel(genome, variables)
394
+ endpoints = self._generate_endpoints(genome, variables)
395
+ users = self._generate_users(genome, variables)
396
+ correlations = self._generate_correlations(genome, variables)
397
+ containment = self._generate_containment(genome, template, variables)
398
+
399
+ # Determine difficulty label
400
+ diff = genome.aggregate_difficulty
401
+ if diff < 0.2:
402
+ difficulty = "easy"
403
+ task_id = f"evolved_easy_{genome.genome_id}"
404
+ elif diff < 0.4:
405
+ difficulty = "medium"
406
+ task_id = f"evolved_medium_{genome.genome_id}"
407
+ elif diff < 0.6:
408
+ difficulty = "medium_hard"
409
+ task_id = f"evolved_medhard_{genome.genome_id}"
410
+ elif diff < 0.8:
411
+ difficulty = "hard"
412
+ task_id = f"evolved_hard_{genome.genome_id}"
413
+ else:
414
+ difficulty = "expert"
415
+ task_id = f"evolved_expert_{genome.genome_id}"
416
+
417
+ # Build alert summary
418
+ alert_summary = random.choice(template["alert_templates"]).format(**variables)
419
+
420
+ # Determine critical evidence and IOCs
421
+ critical_evidence = set()
422
+ critical_iocs = set()
423
+ for entry in log_entries:
424
+ if entry.is_critical:
425
+ for kw in entry.keywords:
426
+ critical_evidence.add(kw)
427
+ for ti in threat_intel:
428
+ critical_iocs.add(ti.ioc)
429
+
430
+ # Build containment pairs
431
+ containment_actions = []
432
+ containment_targets = {}
433
+ required_containment_pairs = []
434
+ for action, target in containment:
435
+ containment_actions.append(action)
436
+ containment_targets[action.value] = target
437
+ required_containment_pairs.append((action.value, target))
438
+
439
+ scenario = Scenario(
440
+ scenario_id=f"evolved_{genome.genome_id}",
441
+ task_id=task_id,
442
+ difficulty=difficulty,
443
+ alert_summary=alert_summary,
444
+ alert_source="SOC-SIEM-EVOLVED",
445
+ alert_timestamp="2026-03-28T10:00:00Z",
446
+ initial_observation=f"[Evolved Scenario Gen-{genome.generation}] {alert_summary}. Begin investigation.",
447
+ true_severity=severity,
448
+ true_category=category,
449
+ required_containment=containment_actions,
450
+ containment_targets=containment_targets,
451
+ is_false_positive=False,
452
+ correct_escalation=template.get("escalation"),
453
+ log_entries=log_entries,
454
+ threat_intel=threat_intel,
455
+ endpoints=endpoints,
456
+ users=users,
457
+ correlation_findings=correlations,
458
+ critical_evidence=critical_evidence,
459
+ critical_iocs=critical_iocs,
460
+ max_steps=genome.max_steps,
461
+ report_keywords=template["report_keywords"],
462
+ required_containment_pairs=required_containment_pairs,
463
+ )
464
+
465
+ return scenario
466
+
467
+ def _select_attack_type(self, genome: ScenarioGenome) -> str:
468
+ """Select attack type based on difficulty."""
469
+ diff = genome.aggregate_difficulty
470
+ if diff < 0.25:
471
+ weights = {"phishing": 5, "lateral_movement": 2, "insider_threat": 1,
472
+ "ransomware": 0, "supply_chain": 0, "apt_zeroday": 0}
473
+ elif diff < 0.5:
474
+ weights = {"phishing": 2, "lateral_movement": 4, "insider_threat": 3,
475
+ "ransomware": 2, "supply_chain": 1, "apt_zeroday": 0}
476
+ elif diff < 0.75:
477
+ weights = {"phishing": 1, "lateral_movement": 2, "insider_threat": 2,
478
+ "ransomware": 4, "supply_chain": 3, "apt_zeroday": 1}
479
+ else:
480
+ weights = {"phishing": 0, "lateral_movement": 1, "insider_threat": 1,
481
+ "ransomware": 2, "supply_chain": 3, "apt_zeroday": 5}
482
+
483
+ types = list(weights.keys())
484
+ w = [weights[t] for t in types]
485
+ return random.choices(types, weights=w, k=1)[0]
486
+
487
+ def _generate_variables(self, genome: ScenarioGenome) -> Dict[str, str]:
488
+ """Generate random scenario variables."""
489
+ hostnames = random.sample(HOSTNAME_POOL, min(genome.num_endpoints + 2, len(HOSTNAME_POOL)))
490
+ ips = random.sample(IP_POOL, min(genome.num_endpoints + 4, len(IP_POOL)))
491
+ users = random.sample(USERNAME_POOL, min(genome.num_users + 2, len(USERNAME_POOL)))
492
+
493
+ return {
494
+ "hostname": hostnames[0],
495
+ "target_host": hostnames[1] if len(hostnames) > 1 else hostnames[0],
496
+ "dc_host": "SRV-DC-01",
497
+ "user": users[0],
498
+ "admin_user": users[1] if len(users) > 1 else "admin_ops",
499
+ "svc_account": f"svc_{random.choice(['deploy', 'backup', 'monitor', 'update'])}",
500
+ "src_ip": ips[0],
501
+ "c2_ip": random.choice(IP_POOL[:10]), # External IPs
502
+ "ext_ip": random.choice(IP_POOL[:10]),
503
+ "attacker_ip": random.choice(IP_POOL[:10]),
504
+ "exfil_ip": random.choice(IP_POOL[:10]),
505
+ "staging_ip": random.choice(IP_POOL[:10]),
506
+ "c2_domain": random.choice(DOMAIN_POOL),
507
+ "staging_domain": random.choice(DOMAIN_POOL),
508
+ "attacker_domain": random.choice(DOMAIN_POOL),
509
+ "c2_port": str(random.choice([443, 8443, 4444, 9090, 8080])),
510
+ "port": str(random.choice([443, 445, 3389, 22, 8080])),
511
+ "file_hash": random.choice(HASH_POOL),
512
+ "filename": random.choice(FILENAMES),
513
+ "file_name": random.choice(FILENAMES),
514
+ "proc_name": random.choice(["svchost.exe", "rundll32.exe", "powershell.exe", "cmd.exe"]),
515
+ "child_proc": random.choice(["cmd.exe", "powershell.exe", "certutil.exe"]),
516
+ "target_proc": random.choice(["explorer.exe", "lsass.exe", "svchost.exe"]),
517
+ "tool_name": random.choice(["Cobalt Strike", "Mimikatz", "BloodHound", "PsExec"]),
518
+ "pid": str(random.randint(1000, 65535)),
519
+ "interval": str(random.choice([30, 60, 120, 300])),
520
+ "attempts": str(random.randint(5, 500)),
521
+ "failed_count": str(random.randint(3, 20)),
522
+ "window": str(random.choice([5, 10, 15, 30])),
523
+ "data_size": str(round(random.uniform(0.5, 500), 1)),
524
+ "total_size": str(round(random.uniform(10, 1000), 1)),
525
+ "bytes": str(random.randint(1024, 10485760)),
526
+ "ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
527
+ "timestamp": "2026-03-28T" + f"{random.randint(0,23):02d}:{random.randint(0,59):02d}:00Z",
528
+ "subject": random.choice([
529
+ "Urgent: Invoice Payment Required",
530
+ "Action Required: Account Verification",
531
+ "Q4 Financial Report - Confidential",
532
+ "Password Reset Request",
533
+ "IT Security Update - Please Install",
534
+ ]),
535
+ "attacker_email": f"attacker_{random.randint(100,999)}@{random.choice(DOMAIN_POOL)}",
536
+ "ext_email": f"external_{random.randint(100,999)}@gmail.com",
537
+ "vendor_email": f"updates@vendor-{random.randint(1,99)}.com",
538
+ "device_name": random.choice(["USB_Drive_SanDisk", "External_HDD_WD", "USB_Kingston_128GB"]),
539
+ "cloud_service": random.choice(["Dropbox", "Google Drive", "OneDrive", "Mega.nz"]),
540
+ "cloud_service_domain": random.choice(["dropbox.com", "drive.google.com", "onedrive.live.com"]),
541
+ "group_name": random.choice(["Domain Admins", "Backup Operators", "Enterprise Admins"]),
542
+ "geo_location": random.choice(["Moscow, RU", "Beijing, CN", "Unknown VPN", "São Paulo, BR"]),
543
+ "software_name": random.choice(["SolarUpdate", "NetMonitor Pro", "CloudSync Agent", "DevPipeline"]),
544
+ "library_name": random.choice(["node-ipc", "ua-parser-js", "event-stream", "coa"]),
545
+ "version": f"v{random.randint(1,5)}.{random.randint(0,9)}.{random.randint(0,99)}",
546
+ "install_path": random.choice(["C:\\Program Files\\", "C:\\Windows\\System32\\", "/opt/", "/usr/local/bin/"]),
547
+ "exploit_name": random.choice(["CVE-2026-XXXX", "Log4Shell-variant", "ProxyNotShell", "ZeroLogon-v2"]),
548
+ "malware_name": random.choice(["LockBit 4.0", "BlackCat v3", "REvil-NG", "DarkSide-X"]),
549
+ "extension": random.choice(["locked", "encrypted", "crypt", "ransom"]),
550
+ "ransom_file": "README_DECRYPT.txt",
551
+ "ransom_domain": f"ransom-{random.randint(1000,9999)}",
552
+ "directory": random.choice(["C:\\Users\\", "D:\\Shares\\Finance\\", "C:\\Data\\", "/home/"]),
553
+ "file_count": str(random.randint(100, 50000)),
554
+ "attachment_count": str(random.randint(1, 10)),
555
+ "auth_method": random.choice(["RDP", "SMB", "WinRM", "NTLM"]),
556
+ "ticket_type": random.choice(["TGT", "TGS", "golden_ticket"]),
557
+ "command": random.choice(["whoami", "net user /domain", "ipconfig /all", "systeminfo"]),
558
+ "connection_count": str(random.randint(20, 200)),
559
+ "query_count": str(random.randint(50, 5000)),
560
+ "ip_count": str(random.randint(5, 50)),
561
+ "technique": random.choice(["token impersonation", "DLL hijacking", "service account abuse"]),
562
+ "domain": random.choice(DOMAIN_POOL),
563
+ "subdomain": f"{''.join(random.choices('abcdef0123456789', k=32))}",
564
+ }
565
+
566
+ def _generate_logs(
567
+ self, genome: ScenarioGenome, template: Dict, variables: Dict
568
+ ) -> List[LogEntry]:
569
+ """Generate log entries with appropriate noise and critical evidence."""
570
+ entries = []
571
+ sources = ["email", "edr", "auth", "proxy", "firewall", "dns"]
572
+
573
+ # Generate critical entries from template
574
+ critical_count = 0
575
+ for source in sources:
576
+ if source not in template["log_templates"]:
577
+ continue
578
+ source_templates = template["log_templates"][source]
579
+
580
+ # Number of entries per source
581
+ n = max(1, genome.num_log_entries // len(sources))
582
+
583
+ for i in range(min(n, len(source_templates))):
584
+ tmpl = source_templates[i % len(source_templates)]
585
+ try:
586
+ content = tmpl.format(**variables)
587
+ except (KeyError, IndexError):
588
+ content = tmpl # Use raw if format fails
589
+
590
+ is_crit = critical_count < genome.num_critical_evidence
591
+ keywords = self._extract_keywords(content, variables)
592
+
593
+ entries.append(LogEntry(
594
+ source=source,
595
+ content=content,
596
+ is_critical=is_crit,
597
+ keywords=keywords,
598
+ ))
599
+ if is_crit:
600
+ critical_count += 1
601
+
602
+ # Add noise entries (decoys)
603
+ noise_count = int(len(entries) * genome.noise_ratio)
604
+ for _ in range(noise_count):
605
+ source = random.choice(sources)
606
+ entries.append(LogEntry(
607
+ source=source,
608
+ content=f"[BENIGN] Routine {source} activity - {random.choice(['scan', 'update', 'backup', 'maintenance'])} completed successfully",
609
+ is_critical=False,
610
+ keywords=["benign", "routine"],
611
+ ))
612
+
613
+ random.shuffle(entries)
614
+ return entries
615
+
616
+ def _generate_threat_intel(
617
+ self, genome: ScenarioGenome, variables: Dict
618
+ ) -> List[ThreatIntelEntry]:
619
+ """Generate threat intelligence entries."""
620
+ entries = []
621
+ ioc_sources = [
622
+ (variables.get("c2_ip", ""), "ip", "Known C2 server", "critical"),
623
+ (variables.get("file_hash", ""), "hash", "Known malware hash", "high"),
624
+ (variables.get("c2_domain", ""), "domain", "Malicious domain", "high"),
625
+ (variables.get("attacker_email", ""), "email", "Phishing sender", "medium"),
626
+ (variables.get("attacker_ip", variables.get("src_ip", "")), "ip", "Attack source", "medium"),
627
+ ]
628
+
629
+ for ioc, ioc_type, desc, sev in ioc_sources[:genome.num_threat_intel]:
630
+ if ioc:
631
+ entries.append(ThreatIntelEntry(
632
+ ioc=ioc,
633
+ ioc_type=ioc_type,
634
+ description=f"{desc} associated with current campaign",
635
+ severity=sev,
636
+ source=random.choice(["VirusTotal", "AlienVault OTX", "IBM X-Force", "Mandiant"]),
637
+ keywords=[ioc.lower(), ioc_type],
638
+ ))
639
+
640
+ return entries
641
+
642
+ def _generate_endpoints(
643
+ self, genome: ScenarioGenome, variables: Dict
644
+ ) -> List[EndpointInfo]:
645
+ """Generate endpoint information."""
646
+ endpoints = []
647
+ hostnames = [variables.get("hostname", "WS-01"), variables.get("target_host", "SRV-01")]
648
+ hostnames.extend(random.sample(HOSTNAME_POOL, min(genome.num_endpoints, len(HOSTNAME_POOL))))
649
+
650
+ for i, host in enumerate(hostnames[:genome.num_endpoints]):
651
+ ip = variables.get("src_ip", "10.0.0.1") if i == 0 else f"10.50.25.{100+i}"
652
+ endpoints.append(EndpointInfo(
653
+ endpoint_id=host,
654
+ hostname=host,
655
+ os=random.choice(["Windows Server 2022", "Windows 11 Pro", "Ubuntu 22.04"]),
656
+ ip=ip,
657
+ status="compromised" if i < 2 else "active",
658
+ processes=[variables.get("proc_name", "svchost.exe")] if i < 2 else [],
659
+ connections=[f"-> {variables.get('c2_ip', '1.2.3.4')}:443"] if i == 0 else [],
660
+ is_compromised=i < 2,
661
+ ))
662
+
663
+ return endpoints
664
+
665
+ def _generate_users(
666
+ self, genome: ScenarioGenome, variables: Dict
667
+ ) -> List[UserProfile]:
668
+ """Generate user profiles."""
669
+ users = []
670
+ usernames = [variables.get("user", "jsmith")]
671
+ if genome.num_users > 1:
672
+ usernames.append(variables.get("admin_user", "admin"))
673
+ usernames.extend(random.sample(USERNAME_POOL, min(genome.num_users, len(USERNAME_POOL))))
674
+
675
+ for i, uid in enumerate(usernames[:genome.num_users]):
676
+ users.append(UserProfile(
677
+ user_id=uid,
678
+ display_name=uid.replace("_", " ").title(),
679
+ department=random.choice(["IT", "Finance", "Engineering", "Operations", "HR"]),
680
+ role=random.choice(["Analyst", "Engineer", "Manager", "Admin", "Developer"]),
681
+ risk_score=0.8 if i == 0 else random.uniform(0.1, 0.5),
682
+ notes="Primary suspect" if i == 0 else "",
683
+ ))
684
+
685
+ return users
686
+
687
+ def _generate_correlations(
688
+ self, genome: ScenarioGenome, variables: Dict
689
+ ) -> List[str]:
690
+ """Generate event correlations."""
691
+ correlations = [
692
+ f"Timeline correlation: Initial compromise at {variables.get('hostname', 'HOST')} "
693
+ f"followed by lateral movement to {variables.get('target_host', 'TARGET')}",
694
+ f"Network correlation: C2 traffic from {variables.get('hostname', 'HOST')} "
695
+ f"to {variables.get('c2_ip', 'C2_IP')} matches known threat pattern",
696
+ ]
697
+
698
+ if genome.correlation_depth >= 3:
699
+ correlations.append(
700
+ f"Credential correlation: Compromised user {variables.get('user', 'USER')} "
701
+ f"was the initial access vector via {variables.get('attacker_email', 'EMAIL')}"
702
+ )
703
+ if genome.correlation_depth >= 4:
704
+ correlations.append(
705
+ f"Data flow correlation: {variables.get('data_size', '0')}MB exfiltrated "
706
+ f"from {variables.get('target_host', 'TARGET')} to {variables.get('c2_ip', 'C2_IP')}"
707
+ )
708
+
709
+ return correlations[:genome.correlation_depth]
710
+
711
+ def _generate_containment(
712
+ self, genome: ScenarioGenome, template: Dict, variables: Dict
713
+ ) -> List[Tuple[ContainmentAction, str]]:
714
+ """Generate containment actions with targets."""
715
+ containment = []
716
+ for action, target_template in template["containment_actions"][:genome.num_containment_targets]:
717
+ try:
718
+ target = target_template.format(**variables)
719
+ except (KeyError, IndexError):
720
+ target = target_template
721
+ containment.append((action, target))
722
+
723
+ return containment
724
+
725
+ def _select_severity(self, genome: ScenarioGenome, template: Dict) -> Severity:
726
+ """Select severity based on genome difficulty."""
727
+ if genome.aggregate_difficulty > 0.6:
728
+ return Severity.CRITICAL
729
+ else:
730
+ return random.choice(template["severities"])
731
+
732
+ def _extract_keywords(self, content: str, variables: Dict) -> List[str]:
733
+ """Extract searchable keywords from log content."""
734
+ keywords = []
735
+ for key in ["hostname", "target_host", "user", "c2_ip", "file_hash",
736
+ "c2_domain", "src_ip", "attacker_email", "proc_name", "filename"]:
737
+ val = variables.get(key, "")
738
+ if val and val.lower() in content.lower():
739
+ keywords.append(val.lower())
740
+ return keywords
server/app.py CHANGED
@@ -181,6 +181,27 @@ async def run_baseline():
181
  return JSONResponse(content=results)
182
 
183
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
  def main(host: str = "0.0.0.0", port: int = 8000):
185
  """Entry point for direct execution."""
186
  import uvicorn
 
181
  return JSONResponse(content=results)
182
 
183
 
184
+ @app.post("/env/evolve")
185
+ async def evolve_population():
186
+ """Trigger evolution of the scenario population."""
187
+ if not hasattr(_shared_env, '_evolution_engine') or _shared_env._evolution_engine is None:
188
+ raise HTTPException(status_code=400, detail="Evolution engine not initialized. Reset with task_id='evolved' first.")
189
+ new_pop = _shared_env._evolution_engine.evolve()
190
+ return JSONResponse(content={
191
+ "status": "evolved",
192
+ "generation": _shared_env._evolution_engine.state.generation,
193
+ "population_size": len(new_pop),
194
+ })
195
+
196
+
197
+ @app.get("/env/evolution-stats")
198
+ async def evolution_stats():
199
+ """Get evolution engine statistics."""
200
+ if not hasattr(_shared_env, '_evolution_engine') or _shared_env._evolution_engine is None:
201
+ return JSONResponse(content={"status": "not_initialized", "message": "Reset with task_id='evolved' to activate"})
202
+ return JSONResponse(content=_shared_env._evolution_engine.get_evolution_stats())
203
+
204
+
205
  def main(host: str = "0.0.0.0", port: int = 8000):
206
  """Entry point for direct execution."""
207
  import uvicorn
server/incident_response_env_environment.py CHANGED
@@ -52,6 +52,12 @@ try:
52
  )
53
  from ..tasks import SCENARIOS, TASK_DEFINITIONS
54
  from ..tasks.base import Scenario
 
 
 
 
 
 
55
  except ImportError:
56
  # When running from server/ directory, add parent to path
57
  _parent = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -68,6 +74,12 @@ except ImportError:
68
  )
69
  from tasks import SCENARIOS, TASK_DEFINITIONS
70
  from tasks.base import Scenario
 
 
 
 
 
 
71
 
72
 
73
  class IncidentResponseEnvEnvironment(Environment):
@@ -126,6 +138,10 @@ class IncidentResponseEnvEnvironment(Environment):
126
  self._ti_iocs_checked: Set[str] = set()
127
  self._investigated_before_classify: bool = False
128
  self._classified_before_contain: bool = True # starts true, set false if violated
 
 
 
 
129
 
130
  def reset(self, seed=None, episode_id=None, task_id: str = None, **kwargs) -> IncidentObservation:
131
  """
@@ -142,6 +158,11 @@ class IncidentResponseEnvEnvironment(Environment):
142
  # Determine task
143
  if task_id is None:
144
  task_id = kwargs.get("task_id", "easy")
 
 
 
 
 
145
  self._task_id = task_id if task_id in SCENARIOS else "easy"
146
 
147
  # Load scenario
@@ -190,6 +211,74 @@ class IncidentResponseEnvEnvironment(Environment):
190
  reward=0.0,
191
  )
192
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
  def step(self, action: IncidentAction) -> IncidentObservation:
194
  """
195
  Execute an investigation or response action.
@@ -1291,29 +1380,50 @@ class IncidentResponseEnvEnvironment(Environment):
1291
  def get_grader_score(self) -> Dict[str, Any]:
1292
  """Return the grader score for the current/last episode."""
1293
  final_score = self.grade()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1294
  return {
1295
  "score": final_score,
1296
  "task_id": self._task_id,
1297
- "breakdown": {
1298
- "investigation_completeness": round(self._calculate_investigation_completeness(), 4),
1299
- "ioc_identification": round(
1300
- len(self._iocs_discovered & self._scenario.critical_iocs) / max(len(self._scenario.critical_iocs), 1), 4
1301
- ) if self._scenario else 0.0,
1302
- "severity_correct": self._severity_set == self._scenario.true_severity if self._scenario else False,
1303
- "category_correct": self._category_set == self._scenario.true_category if self._scenario else False,
1304
- "containment_score": round(self._grade_containment(), 4),
1305
- "containment_precision": round(self._grade_containment_precision(), 4),
1306
- "report_submitted": self._report_submitted,
1307
- "escalation_correct": (
1308
- self._escalated_to in ([self._scenario.correct_escalation] if self._task_id != "expert"
1309
- else ["tier3", "management", "legal"])
1310
- if self._scenario and self._scenario.correct_escalation and self._escalated_to
1311
- else self._escalated_to is None and (not self._scenario or not self._scenario.correct_escalation)
1312
- ),
1313
- "evidence_chain_coherence": self._investigated_before_classify,
1314
- "phase_discipline": self._classified_before_contain,
1315
- "log_sources_queried": len(self._log_sources_queried),
1316
- "steps_used": self._state.step_count,
1317
- "max_steps": self._scenario.max_steps if self._scenario else 0,
1318
- },
1319
  }
 
52
  )
53
  from ..tasks import SCENARIOS, TASK_DEFINITIONS
54
  from ..tasks.base import Scenario
55
+ from ..self_evolving import (
56
+ AgentPerformanceRecord,
57
+ EvolutionEngine,
58
+ ScenarioGenerator,
59
+ ScenarioGenome,
60
+ )
61
  except ImportError:
62
  # When running from server/ directory, add parent to path
63
  _parent = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
 
74
  )
75
  from tasks import SCENARIOS, TASK_DEFINITIONS
76
  from tasks.base import Scenario
77
+ from self_evolving import (
78
+ AgentPerformanceRecord,
79
+ EvolutionEngine,
80
+ ScenarioGenerator,
81
+ ScenarioGenome,
82
+ )
83
 
84
 
85
  class IncidentResponseEnvEnvironment(Environment):
 
138
  self._ti_iocs_checked: Set[str] = set()
139
  self._investigated_before_classify: bool = False
140
  self._classified_before_contain: bool = True # starts true, set false if violated
141
+ # Self-evolving engine
142
+ self._evolution_engine: Optional[EvolutionEngine] = None
143
+ self._scenario_generator: Optional[ScenarioGenerator] = None
144
+ self._current_genome: Optional[ScenarioGenome] = None
145
 
146
  def reset(self, seed=None, episode_id=None, task_id: str = None, **kwargs) -> IncidentObservation:
147
  """
 
158
  # Determine task
159
  if task_id is None:
160
  task_id = kwargs.get("task_id", "easy")
161
+
162
+ # Self-evolving mode
163
+ if task_id == "evolved" or (task_id and task_id.startswith("evolved_")):
164
+ return self._reset_evolved(seed=seed, episode_id=episode_id)
165
+
166
  self._task_id = task_id if task_id in SCENARIOS else "easy"
167
 
168
  # Load scenario
 
211
  reward=0.0,
212
  )
213
 
214
+ def _reset_evolved(self, seed=None, episode_id=None) -> IncidentObservation:
215
+ """Reset with a procedurally generated evolved scenario."""
216
+ if self._evolution_engine is None:
217
+ self._evolution_engine = EvolutionEngine(population_size=10, alpha=0.5)
218
+ self._scenario_generator = ScenarioGenerator()
219
+
220
+ # Get next scenario genome from evolution engine
221
+ genome = self._evolution_engine.get_next_scenario_genome()
222
+
223
+ # Generate scenario from genome
224
+ scenario = self._scenario_generator.generate(genome, seed=seed)
225
+
226
+ # Store genome reference for performance recording
227
+ self._current_genome = genome
228
+
229
+ # Use standard reset logic with the generated scenario
230
+ self._task_id = scenario.task_id
231
+ self._scenario = scenario
232
+
233
+ # Reset all state (same as normal reset)
234
+ self._state = IncidentState(
235
+ episode_id=episode_id or str(uuid4()),
236
+ step_count=0,
237
+ current_task=self._task_id,
238
+ )
239
+ self._evidence_discovered = set()
240
+ self._iocs_discovered = set()
241
+ self._actions_history = []
242
+ self._containment_executed = []
243
+ self._severity_set = None
244
+ self._category_set = None
245
+ self._escalated_to = None
246
+ self._report_submitted = False
247
+ self._report_text = ""
248
+ self._episode_done = False
249
+ self._reward_this_step = 0.0
250
+ self._total_reward = 0.0
251
+ self._closed_as_fp = False
252
+ self._log_sources_queried = set()
253
+ self._ti_iocs_checked = set()
254
+ self._investigated_before_classify = False
255
+ self._classified_before_contain = True
256
+
257
+ if seed is not None:
258
+ random.seed(seed)
259
+
260
+ return IncidentObservation(
261
+ alert_id=scenario.scenario_id,
262
+ alert_summary=scenario.alert_summary,
263
+ alert_source=scenario.alert_source,
264
+ timestamp=scenario.alert_timestamp,
265
+ findings=scenario.initial_observation,
266
+ evidence_collected=[],
267
+ iocs_discovered=[],
268
+ action_result="[Self-Evolving Mode] Environment initialized with evolved scenario. Begin your investigation.",
269
+ available_actions=[at.value for at in ActionType],
270
+ steps_remaining=scenario.max_steps,
271
+ investigation_progress=0.0,
272
+ done=False,
273
+ reward=0.0,
274
+ )
275
+
276
+ def get_evolution_stats(self) -> Dict[str, Any]:
277
+ """Get statistics about the current evolution state."""
278
+ if self._evolution_engine is None:
279
+ return {"status": "not_initialized", "message": "Reset with task_id='evolved' to activate"}
280
+ return self._evolution_engine.get_evolution_stats()
281
+
282
  def step(self, action: IncidentAction) -> IncidentObservation:
283
  """
284
  Execute an investigation or response action.
 
1380
  def get_grader_score(self) -> Dict[str, Any]:
1381
  """Return the grader score for the current/last episode."""
1382
  final_score = self.grade()
1383
+ scores = {
1384
+ "investigation_completeness": round(self._calculate_investigation_completeness(), 4),
1385
+ "ioc_identification": round(
1386
+ len(self._iocs_discovered & self._scenario.critical_iocs) / max(len(self._scenario.critical_iocs), 1), 4
1387
+ ) if self._scenario else 0.0,
1388
+ "severity_correct": self._severity_set == self._scenario.true_severity if self._scenario else False,
1389
+ "category_correct": self._category_set == self._scenario.true_category if self._scenario else False,
1390
+ "containment_score": round(self._grade_containment(), 4),
1391
+ "containment_precision": round(self._grade_containment_precision(), 4),
1392
+ "report_submitted": self._report_submitted,
1393
+ "escalation_correct": (
1394
+ self._escalated_to in ([self._scenario.correct_escalation] if self._task_id != "expert"
1395
+ else ["tier3", "management", "legal"])
1396
+ if self._scenario and self._scenario.correct_escalation and self._escalated_to
1397
+ else self._escalated_to is None and (not self._scenario or not self._scenario.correct_escalation)
1398
+ ),
1399
+ "evidence_chain_coherence": self._investigated_before_classify,
1400
+ "phase_discipline": self._classified_before_contain,
1401
+ "log_sources_queried": len(self._log_sources_queried),
1402
+ "steps_used": self._state.step_count,
1403
+ "max_steps": self._scenario.max_steps if self._scenario else 0,
1404
+ }
1405
+
1406
+ # Record performance for evolution engine
1407
+ if self._evolution_engine and self._current_genome:
1408
+ import time
1409
+ record = AgentPerformanceRecord(
1410
+ scenario_id=self._scenario.scenario_id if self._scenario else "",
1411
+ genome_id=self._current_genome.genome_id,
1412
+ score=final_score,
1413
+ steps_used=self._state.step_count,
1414
+ max_steps=self._scenario.max_steps if self._scenario else 25,
1415
+ evidence_found_ratio=scores.get("investigation_completeness", 0),
1416
+ iocs_found_ratio=scores.get("ioc_identification", 0),
1417
+ correct_severity=scores.get("severity_correct", False),
1418
+ correct_category=scores.get("category_correct", False),
1419
+ containment_score=scores.get("containment_score", 0),
1420
+ report_quality=0.0,
1421
+ timestamp=time.time(),
1422
+ )
1423
+ self._evolution_engine.record_performance(self._current_genome, record)
1424
+
1425
  return {
1426
  "score": final_score,
1427
  "task_id": self._task_id,
1428
+ "breakdown": scores,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1429
  }