nevernever69/redveil
0
1"""RedVeil Environment Implementation.2 3A cybersecurity-themed RL environment where agents make decisions under4uncertainty, use tools effectively, and avoid deceptive signals.5 6This environment runs a REAL vulnerable Flask web application and sends7REAL HTTP requests. SQL injections are genuine, login bypasses are real,8and honeypot responses come from actual HTTP endpoints.9 10KEY DESIGN: Endpoints are HIDDEN. The agent only sees ports at the start.11Scanning a port reveals the endpoints hosted on it (mix of real + honeypots).12Endpoint paths are randomized per episode -- the agent cannot memorize routes.13"""14 15import threading16import time17from typing import Any, Optional18from uuid import uuid419 20from openenv.core.env_server.interfaces import Environment21from openenv.core.env_server.types import State22 23try:24 from ..models import ActionType, RedVeilAction, RedVeilObservation25 from ..noise import DeceptionEngine, NoiseEngine26 from ..tasks import ALL_TASKS, TaskConfig27 from ..grader import grade_task28 from ..vulnerable_app import create_vulnerable_app29except (ImportError, ModuleNotFoundError):30 from models import ActionType, RedVeilAction, RedVeilObservation31 from noise import DeceptionEngine, NoiseEngine32 from tasks import ALL_TASKS, TaskConfig33 from grader import grade_task34 from vulnerable_app import create_vulnerable_app35 36 37# ---------------------------------------------------------------------------38# Vulnerable app management39# ---------------------------------------------------------------------------40 41_vuln_app_started = False42_vuln_app_lock = threading.Lock()43VULN_APP_PORT = 500044VULN_APP_URL = f"http://127.0.0.1:{VULN_APP_PORT}"45 46 47def _ensure_vuln_app_running():48 """Start the vulnerable Flask app in a background thread if not already running."""49 global _vuln_app_started50 51 with _vuln_app_lock:52 if _vuln_app_started:53 return54 55 app = create_vulnerable_app()56 57 def run_app():58 import logging59 log = logging.getLogger('werkzeug')60 log.setLevel(logging.WARNING)61 app.run(62 host='127.0.0.1',63 port=VULN_APP_PORT,64 debug=False,65 use_reloader=False,66 threaded=True,67 )68 69 thread = threading.Thread(target=run_app, daemon=True)70 thread.start()71 _vuln_app_started = True72 73 import requests74 for _ in range(30):75 try:76 resp = requests.get(f"{VULN_APP_URL}/health", timeout=1)77 if resp.status_code == 200:78 return79 except requests.RequestException:80 pass81 time.sleep(0.1)82 83 84class RedVeilEnvironment(Environment):85 """RedVeil: Decision-making under uncertainty with real tool interaction.86 87 Endpoints are HIDDEN until the agent scans the port they live on.88 Paths are randomized per episode. Real HTTP requests are sent to a89 genuine vulnerable Flask application with real SQL injection vulnerabilities.90 """91 92 SUPPORTS_CONCURRENT_SESSIONS: bool = True93 94 def __init__(self):95 super().__init__()96 self._state = State(episode_id=str(uuid4()), step_count=0)97 self._task: Optional[TaskConfig] = None98 self._noise_engine: Optional[NoiseEngine] = None99 self._deception_engine: Optional[DeceptionEngine] = None100 101 # Game state tracking102 self._budget_remaining: int = 0103 self._scan_counts: dict = {}104 self._revealed_endpoints: set = set() # Endpoints revealed by scanning105 self._discovered_endpoints: set = set() # Endpoints the agent has fuzzed106 self._fuzzed_endpoints: set = set()107 self._identified_real_ports: set = set()108 self._identified_fake_ports: set = set()109 self._vuln_found: bool = False110 self._vuln_endpoint: Optional[str] = None111 self._exploit_success: bool = False112 self._creds_extracted: bool = False113 self._extracted_creds: Optional[dict] = None114 self._admin_login: bool = False115 self._flagged_honeypots: set = set()116 self._action_log: list = []117 self._session_token: Optional[str] = None # Token from /api/profile118 self._config_fetched: bool = False # Found hidden paths via config119 self._hidden_endpoints_found: set = set() # Endpoints found via config/robots120 self._low_priv_login: bool = False # Logged in as non-admin user121 122 # Endpoint path -> EndpointConfig lookup123 self._endpoint_map: dict = {}124 125 _ensure_vuln_app_running()126 127 def reset(128 self,129 seed: Optional[int] = None,130 episode_id: Optional[str] = None,131 **kwargs: Any,132 ) -> RedVeilObservation:133 """Reset the environment with a specific task."""134 task_id = kwargs.get("task_id", "easy_recon")135 actual_seed = seed if seed is not None else 42136 137 self._task = ALL_TASKS.get(task_id, ALL_TASKS["easy_recon"])138 self._state = State(139 episode_id=episode_id or str(uuid4()),140 step_count=0,141 )142 143 self._noise_engine = NoiseEngine(144 noise_level=self._task.noise_level,145 conflicting_scans=self._task.conflicting_scans,146 seed=actual_seed,147 )148 self._deception_engine = DeceptionEngine(149 deception_active=self._task.deception_active,150 target_base_url=VULN_APP_URL,151 seed=actual_seed,152 )153 154 # Reset game state155 self._budget_remaining = self._task.budget156 self._scan_counts = {}157 self._revealed_endpoints = set()158 self._discovered_endpoints = set()159 self._fuzzed_endpoints = set()160 self._identified_real_ports = set()161 self._identified_fake_ports = set()162 self._vuln_found = False163 self._vuln_endpoint = None164 self._exploit_success = False165 self._creds_extracted = False166 self._extracted_creds = None167 self._admin_login = False168 self._flagged_honeypots = set()169 self._action_log = []170 self._session_token = None171 self._config_fetched = False172 self._hidden_endpoints_found = set()173 self._low_priv_login = False174 175 # Build endpoint lookup176 self._endpoint_map = {e.path: e for e in self._task.endpoints}177 178 # Build initial observation -- endpoints are HIDDEN179 port_list = ", ".join(str(p.port) for p in self._task.ports)180 181 if self._task.task_id == "easy_recon":182 # Easy task: no endpoints, just ports183 targets_info = f"Ports: {port_list}\nEndpoints: N/A (port scan task only)"184 else:185 # Medium/Hard: endpoints are hidden behind ports186 targets_info = (187 f"Ports: {port_list}\n"188 f"Endpoints: UNKNOWN -- scan ports to discover web endpoints"189 )190 191 intro = (192 f"[ENVIRONMENT INITIALIZED]\n"193 f"Task: {self._task.description}\n"194 f"Difficulty: {self._task.difficulty}\n"195 f"Budget: {self._budget_remaining} actions\n\n"196 f"[OBJECTIVE]\n{self._task.objective}\n\n"197 f"[KNOWN TARGETS]\n"198 f"{targets_info}\n\n"199 f"[AVAILABLE ACTIONS]\n"200 f"- scan <port>: Scan a port for services and discover endpoints\n"201 f"- fuzz <endpoint>: Send probe requests to a discovered endpoint\n"202 f"- inject_payload <endpoint>: Attempt SQL injection on an endpoint\n"203 f"- login <username:password>: Attempt authentication with credentials\n"204 f"- analyze <target>: Deep probe -- check status, get profile (user:pass), or query restricted endpoint (with payload)\n"205 f"- fetch_config <target>: Retrieve config files (robots.txt, config) to discover hidden paths"206 )207 208 return RedVeilObservation(209 observation_text=intro,210 budget_remaining=self._budget_remaining,211 task_id=self._task.task_id,212 task_description=self._task.description,213 milestones_reached=[],214 done=False,215 reward=0.01,216 )217 218 def step(219 self,220 action: RedVeilAction,221 timeout_s: Optional[float] = None,222 **kwargs: Any,223 ) -> RedVeilObservation:224 """Execute an action in the environment."""225 self._state.step_count += 1226 227 if self._budget_remaining <= 0:228 return self._make_observation(229 "[BUDGET EXHAUSTED] No actions remaining. Episode complete.",230 done=True,231 )232 233 self._budget_remaining -= 1234 235 self._action_log.append({236 "step": self._state.step_count,237 "action": action.action_type.value,238 "target": action.target,239 })240 241 if action.action_type == ActionType.SCAN:242 obs_text = self._handle_scan(action.target)243 elif action.action_type == ActionType.FUZZ:244 obs_text = self._handle_fuzz(action.target)245 elif action.action_type == ActionType.INJECT_PAYLOAD:246 obs_text = self._handle_inject(action.target, payload=action.payload)247 # Honeypot penalty: injecting a honeypot triggers IDS, costs extra budget248 target_path = action.target if action.target.startswith("/") else "/" + action.target249 ep = self._endpoint_map.get(target_path)250 if ep and ep.is_honeypot:251 self._budget_remaining = max(0, self._budget_remaining - 1)252 obs_text += "\n[IDS ALERT] Anomalous activity detected. Security response initiated."253 elif action.action_type == ActionType.LOGIN:254 obs_text = self._handle_login(action.target)255 elif action.action_type == ActionType.ANALYZE:256 obs_text = self._handle_analyze(action.target, payload=action.payload)257 elif action.action_type == ActionType.FETCH_CONFIG:258 obs_text = self._handle_fetch_config(action.target)259 else:260 obs_text = f"[ERROR] Unknown action: {action.action_type}"261 262 done = self._budget_remaining <= 0 or self._admin_login263 264 if self._task and self._task.task_id == "easy_recon":265 if len(self._identified_real_ports) >= len(self._task.real_port_ids):266 done = True267 268 return self._make_observation(obs_text, done=done)269 270 def _handle_scan(self, target: str) -> str:271 """Handle scan: noise-modeled port scan + endpoint discovery."""272 try:273 port_num = int(target)274 except ValueError:275 return f"[ERROR] Invalid port: {target}. Provide a numeric port."276 277 port_config = None278 for p in self._task.ports:279 if p.port == port_num:280 port_config = p281 break282 283 if port_config is None:284 return f"[SCAN RESULT]\nPort {port_num}: no response (host may be filtering)"285 286 scan_count = self._scan_counts.get(port_num, 0)287 self._scan_counts[port_num] = scan_count + 1288 289 result = self._noise_engine.scan_port(port_config, scan_count)290 formatted = self._noise_engine.format_scan_result(result)291 292 if result.status in ("open", "open|filtered") and result.confidence > 0.6:293 if port_config.is_real:294 self._identified_real_ports.add(port_num)295 else:296 self._identified_fake_ports.add(port_num)297 298 # PROGRESSIVE DISCOVERY: reveal endpoints hosted on this port299 # Under high noise, only a fraction of endpoints are revealed per scan300 if port_config.hosted_endpoints and result.status in ("open", "open|filtered"):301 import random302 rng = random.Random(self._state.step_count + port_num)303 304 candidates = [ep for ep in port_config.hosted_endpoints if ep not in self._revealed_endpoints]305 306 if candidates:307 # Noise level determines discovery rate: 0.0 noise = 100%, 0.5 noise = 60%308 discovery_rate = max(0.4, 1.0 - self._task.noise_level * 0.8)309 num_to_reveal = max(1, int(len(candidates) * discovery_rate))310 # On rescan, reveal different subset (seeded by step count)311 to_reveal = rng.sample(candidates, min(num_to_reveal, len(candidates)))312 313 newly_revealed = []314 for ep_path in to_reveal:315 self._revealed_endpoints.add(ep_path)316 newly_revealed.append(ep_path)317 318 if newly_revealed:319 formatted += "\n\n[DISCOVERY] Web endpoints found on port " + str(port_num) + ":"320 for ep in newly_revealed:321 formatted += f"\n - {ep}"322 unrevealed_count = len(port_config.hosted_endpoints) - len(323 [e for e in port_config.hosted_endpoints if e in self._revealed_endpoints]324 )325 if unrevealed_count > 0:326 formatted += f"\n[NOTE] Scan incomplete -- {unrevealed_count} additional endpoint(s) may exist. Rescan to discover more."327 else:328 formatted += "\n[NOTE] Endpoint purpose is unknown. Use fuzz to investigate."329 330 return formatted331 332 def _handle_fuzz(self, target: str) -> str:333 """Handle fuzz: only works on revealed endpoints, sends real HTTP."""334 if not target.startswith("/"):335 target = "/" + target336 337 # Check if endpoint has been revealed by scanning338 if self._task.task_id != "easy_recon" and target not in self._revealed_endpoints:339 return (340 f"[FUZZ RESULT] {target}\n"341 f"[ERROR] Endpoint not discovered. Scan ports first to discover endpoints."342 )343 344 endpoint = self._endpoint_map.get(target)345 if endpoint is None:346 return f"[FUZZ RESULT] {target}\n[HTTP 404] Endpoint not found on target server."347 348 self._discovered_endpoints.add(target)349 self._fuzzed_endpoints.add(target)350 351 # Send REAL HTTP request using the endpoint's real_route352 formatted = self._deception_engine.fuzz_endpoint(endpoint)353 354 if endpoint.has_vulnerability and not endpoint.is_honeypot:355 self._vuln_found = True356 self._vuln_endpoint = target357 358 return formatted359 360 def _handle_inject(self, target: str, payload: str = None) -> str:361 """Handle injection: only works on discovered endpoints, real SQLi."""362 if not target.startswith("/"):363 target = "/" + target364 365 if self._task.task_id != "easy_recon" and target not in self._revealed_endpoints:366 return (367 f"[INJECT RESULT] {target}\n"368 f"[ERROR] Endpoint not discovered. Scan ports first."369 )370 371 endpoint = self._endpoint_map.get(target)372 if endpoint is None:373 return f"[INJECT RESULT] Target {target} not found."374 375 response_text, success, creds = self._deception_engine.inject_payload(endpoint, agent_payload=payload)376 377 if success:378 self._exploit_success = True379 if creds:380 self._creds_extracted = True381 self._extracted_creds = creds382 383 if endpoint.is_honeypot:384 self._flagged_honeypots.add(target)385 386 return response_text387 388 def _handle_login(self, target: str) -> str:389 """Handle login: sends real auth request. Requires login endpoint discovery."""390 if ":" not in target:391 return "[LOGIN RESULT] Invalid format. Use: login username:password"392 393 # For non-easy tasks, agent must have discovered a login endpoint first394 if self._task and self._task.task_id != "easy_recon":395 login_discovered = False396 for ep_path in self._revealed_endpoints:397 ep = self._endpoint_map.get(ep_path)398 if ep and ep.real_route == "/login":399 login_discovered = True400 break401 if not login_discovered:402 return (403 "[LOGIN RESULT] No authentication endpoint discovered.\n"404 "You must scan ports and discover a login endpoint before attempting authentication."405 )406 407 parts = target.split(":", 1)408 username = parts[0].strip()409 password = parts[1].strip()410 411 response_text, is_admin = self._deception_engine.attempt_login(username, password)412 413 if is_admin:414 self._admin_login = True415 elif "successful" in response_text.lower():416 self._low_priv_login = True417 418 return response_text419 420 def _handle_analyze(self, target: str, payload: str = None) -> str:421 """Handle analyze: deep probe of an endpoint with optional auth token.422 423 Sends requests to /api/profile (with creds) or /api/internal/db (with token).424 """425 import requests as req426 427 if not target.startswith("/"):428 target = "/" + target429 430 # Check if it's a profile request (needs username:password in target)431 if "profile" in target or (payload and ":" in target):432 # target = "username:password" for profile433 creds_str = target434 if ":" in creds_str:435 parts = creds_str.split(":", 1)436 username, password = parts[0].strip().strip("/"), parts[1].strip()437 else:438 return "[ANALYZE RESULT] For profile, use: analyze username:password"439 440 try:441 resp = req.get(442 f"{VULN_APP_URL}/api/profile",443 params={"username": username, "password": password},444 timeout=5,445 )446 body = resp.json()447 lines = [f"[ANALYZE RESULT] /api/profile", f"[HTTP {resp.status_code}]"]448 449 if resp.status_code == 200 and body.get("status") == "success":450 profile = body.get("profile", {})451 lines.append(f"Username: {profile.get('username')}")452 lines.append(f"Role: {profile.get('role')}")453 lines.append(f"Session token: {profile.get('session_token', 'N/A')}")454 455 if profile.get("session_token"):456 self._session_token = profile["session_token"]457 lines.append("[TOKEN ACQUIRED] Use this token for restricted endpoints.")458 else:459 lines.append(body.get("message", "Request failed."))460 461 return "\n".join(lines)462 except req.RequestException as e:463 return f"[ANALYZE RESULT] Connection failed: {str(e)[:100]}"464 465 # Analyze with token -- for restricted endpoints like /api/internal/db466 if payload and self._session_token:467 try:468 resp = req.get(469 f"{VULN_APP_URL}/api/internal/db",470 params={"q": payload, "token": self._session_token},471 timeout=5,472 )473 body = resp.json()474 lines = [f"[ANALYZE RESULT] /api/internal/db", f"[HTTP {resp.status_code}]"]475 476 if resp.status_code == 200:477 data = body.get("data", [])478 if data:479 lines.append(f"Query returned {len(data)} record(s):")480 for item in data:481 if isinstance(item, dict):482 content = item.get("content", "")483 lines.append(f" [{item.get('category', '?')}] {content}")484 # Check if admin creds are in the restricted data485 if ":" in content and any(486 w in content.lower() for w in ("admin", "root")487 ):488 parts = content.split(":", 1)489 self._creds_extracted = True490 self._extracted_creds = {491 "username": parts[0].strip(),492 "password": parts[1].strip(),493 }494 else:495 lines.append("No data returned.")496 else:497 lines.append(body.get("message", "Access denied."))498 499 return "\n".join(lines)500 except req.RequestException as e:501 return f"[ANALYZE RESULT] Connection failed: {str(e)[:100]}"502 503 # Generic analyze -- hits /api/status?verbose=true for info disclosure504 try:505 resp = req.get(f"{VULN_APP_URL}/api/status", params={"verbose": "true"}, timeout=5)506 body = resp.json()507 lines = [f"[ANALYZE RESULT] /api/status", f"[HTTP {resp.status_code}]"]508 509 debug = body.get("debug", {})510 if debug:511 lines.append(f"Database tables: {', '.join(debug.get('database_tables', []))}")512 lines.append(f"Active sessions: {debug.get('active_sessions', 0)}")513 internal_eps = debug.get("internal_endpoints", [])514 if internal_eps:515 lines.append(f"Internal endpoints: {', '.join(internal_eps)}")516 for ep in internal_eps:517 self._hidden_endpoints_found.add(ep)518 auth = debug.get("auth_method", "")519 if auth:520 lines.append(f"Auth method: {auth}")521 self._config_fetched = True522 else:523 lines.append(f"Server: {body.get('server', 'unknown')}")524 lines.append(f"Uptime: {body.get('uptime', 'unknown')}")525 526 return "\n".join(lines)527 except req.RequestException as e:528 return f"[ANALYZE RESULT] Connection failed: {str(e)[:100]}"529 530 def _handle_fetch_config(self, target: str) -> str:531 """Handle fetch_config: retrieve configuration files like robots.txt.532 533 Can discover hidden endpoints that aren't on any port.534 """535 import requests as req536 537 target = target.strip().lower()538 539 if target in ("robots.txt", "/robots.txt", "robots"):540 try:541 resp = req.get(f"{VULN_APP_URL}/robots.txt", timeout=5)542 lines = [f"[CONFIG RESULT] /robots.txt", f"[HTTP {resp.status_code}]"]543 lines.append(resp.text)544 self._config_fetched = True545 546 # Parse disallowed paths as hidden endpoints547 for line in resp.text.split("\n"):548 if line.startswith("Disallow:"):549 path = line.split(":", 1)[1].strip()550 if path and path != "/":551 self._hidden_endpoints_found.add(path)552 553 return "\n".join(lines)554 except req.RequestException as e:555 return f"[CONFIG RESULT] Connection failed: {str(e)[:100]}"556 557 if target in ("config", "/api/config", "api/config"):558 try:559 resp = req.get(f"{VULN_APP_URL}/api/config", timeout=5)560 body = resp.json()561 lines = [f"[CONFIG RESULT] /api/config", f"[HTTP {resp.status_code}]"]562 config = body.get("config", {})563 lines.append(f"Version: {config.get('version', '?')}")564 lines.append(f"Environment: {config.get('environment', '?')}")565 endpoints = config.get("endpoints", [])566 if endpoints:567 lines.append("Registered endpoints:")568 for ep in endpoints:569 lines.append(f" - {ep.get('path', '?')}: {ep.get('description', '?')}")570 self._config_fetched = True571 return "\n".join(lines)572 except req.RequestException as e:573 return f"[CONFIG RESULT] Connection failed: {str(e)[:100]}"574 575 return f"[CONFIG RESULT] Unknown config target: {target}. Try: robots.txt, config"576 577 def _make_observation(self, obs_text: str, done: bool) -> RedVeilObservation:578 milestones = self._get_reached_milestones()579 reward = self._compute_reward()580 581 budget_info = f"\n\n[STATUS] Budget remaining: {self._budget_remaining}/{self._task.budget}"582 if milestones:583 budget_info += f"\n[PROGRESS] Milestones: {', '.join(milestones)}"584 if self._revealed_endpoints:585 budget_info += f"\n[DISCOVERED ENDPOINTS] {', '.join(sorted(self._revealed_endpoints))}"586 if self._hidden_endpoints_found:587 budget_info += f"\n[HIDDEN PATHS FOUND] {', '.join(sorted(self._hidden_endpoints_found))}"588 if self._session_token:589 budget_info += f"\n[SESSION] Active token acquired"590 591 full_text = obs_text + budget_info592 593 return RedVeilObservation(594 observation_text=full_text,595 budget_remaining=self._budget_remaining,596 task_id=self._task.task_id,597 task_description=self._task.description,598 milestones_reached=milestones,599 done=done,600 reward=reward,601 )602 603 def _get_reached_milestones(self) -> list:604 milestones = []605 606 if self._task.task_id == "easy_recon":607 if self._scan_counts:608 milestones.append("scanned_port")609 if len(self._identified_real_ports) >= 1:610 milestones.append("identified_1_real")611 if len(self._identified_real_ports) >= 2:612 milestones.append("identified_2_real")613 if (len(self._identified_real_ports) >= 3 and614 len(self._identified_fake_ports) == 0):615 milestones.append("identified_all_3_clean")616 617 elif self._task.task_id == "medium_deception":618 if self._discovered_endpoints:619 milestones.append("discovered_endpoint")620 if self._vuln_found:621 milestones.append("found_real_vuln")622 if self._exploit_success:623 milestones.append("exploited_vuln")624 if self._exploit_success and self._flagged_honeypots:625 milestones.append("flagged_honeypot")626 627 elif self._task.task_id == "hard_chain":628 if self._scan_counts or self._discovered_endpoints:629 milestones.append("useful_recon")630 if self._config_fetched:631 milestones.append("found_config")632 if self._vuln_found:633 milestones.append("found_real_vuln")634 if self._exploit_success:635 milestones.append("exploited_vuln")636 if self._creds_extracted:637 milestones.append("extracted_creds")638 if self._admin_login:639 milestones.append("admin_login")640 641 elif self._task.task_id == "expert_chain":642 if self._scan_counts or self._discovered_endpoints:643 milestones.append("useful_recon")644 if self._config_fetched or self._hidden_endpoints_found:645 milestones.append("info_disclosure")646 if self._low_priv_login:647 milestones.append("low_priv_access")648 if self._session_token:649 milestones.append("acquired_token")650 if self._creds_extracted:651 milestones.append("extracted_admin_creds")652 if self._admin_login:653 milestones.append("admin_login")654 655 return milestones656 657 def _compute_reward(self) -> float:658 milestones = self._get_reached_milestones()659 if not milestones or not self._task:660 return 0.01661 662 reward = 0.01663 milestone_rewards = {name: val for name, val in self._task.milestones}664 for m in milestones:665 if m in milestone_rewards:666 reward = max(reward, milestone_rewards[m])667 668 return max(0.01, min(0.99, round(reward, 2)))669 670 @property671 def state(self) -> State:672 return self._state673 674 def get_game_state(self) -> dict:675 return {676 "task_id": self._task.task_id if self._task else None,677 "budget_remaining": self._budget_remaining,678 "budget_total": self._task.budget if self._task else 0,679 "scan_counts": dict(self._scan_counts),680 "revealed_endpoints": list(self._revealed_endpoints),681 "discovered_endpoints": list(self._discovered_endpoints),682 "fuzzed_endpoints": list(self._fuzzed_endpoints),683 "identified_real_ports": list(self._identified_real_ports),684 "identified_fake_ports": list(self._identified_fake_ports),685 "vuln_found": self._vuln_found,686 "vuln_endpoint": self._vuln_endpoint,687 "exploit_success": self._exploit_success,688 "creds_extracted": self._creds_extracted,689 "admin_login": self._admin_login,690 "flagged_honeypots": list(self._flagged_honeypots),691 "config_fetched": self._config_fetched,692 "hidden_endpoints_found": list(self._hidden_endpoints_found),693 "session_token_acquired": self._session_token is not None,694 "low_priv_login": self._low_priv_login,695 "milestones": self._get_reached_milestones(),696 "reward": self._compute_reward(),697 "action_log": self._action_log,698 }699 