CoolFace
Apppublic

nevernever69/redveil

sourceHugging Faceupdated 6mo agoView on Hugging Face
0likes
redveil_environment.py699 linesDownload Raw Back to server
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