Spaces:
Sleeping
Sleeping
SOC Simulator OpenEnv Space β deploy snapshot without binary eval plots (see GitHub for PNGs)
8c25076 Download validate.py from Aditya9605/SOC-Simulator: direct link, hf CLI and curl.
- Browser
- Download file 18.1 kB
-
https://huggingface.co/spaces/Aditya9605/SOC-Simulator/resolve/main/validate.py
- Command line
-
hf download hf://spaces/Aditya9605/SOC-Simulator/validate.py
-
curl -L -o validate.py https://huggingface.co/spaces/Aditya9605/SOC-Simulator/resolve/main/validate.py
18.1 kB
| """ | |
| Pre-Submission Validation Script | |
| ================================== | |
| Run this before submitting to verify all checklist items pass locally. | |
| Usage | |
| ----- | |
| python validate.py | |
| Checks | |
| ------ | |
| β openenv.yaml β spec_version, required fields | |
| β Typed Pydantic models (SOCAction, SOCObservation, SOCState) | |
| β Environment API β reset() / step() / state() | |
| β All 3 tasks run and produce observations | |
| β All 3 graders return scores in [0.0, 1.0] | |
| β inference.py exists at root and is runnable | |
| β Dockerfile exists and is valid syntax | |
| β requirements.txt exists | |
| β outputs/ directory is writable | |
| β FastAPI server starts and /health returns 200 | |
| """ | |
| from __future__ import annotations | |
| import importlib | |
| import os | |
| import subprocess | |
| import sys | |
| import time | |
| import threading | |
| from pathlib import Path | |
| if sys.stdout and hasattr(sys.stdout, 'reconfigure'): | |
| sys.stdout.reconfigure(encoding='utf-8') | |
| if sys.stderr and hasattr(sys.stderr, 'reconfigure'): | |
| sys.stderr.reconfigure(encoding='utf-8') | |
| ROOT = Path(__file__).parent.resolve() | |
| sys.path.insert(0, str(ROOT)) | |
| PASS = " β " | |
| FAIL = " β" | |
| WARN = " β οΈ " | |
| def section(title: str) -> None: | |
| print(f"\n{'β' * 60}") | |
| print(f" {title}") | |
| print(f"{'β' * 60}") | |
| def check(label: str, condition: bool, detail: str = "") -> bool: | |
| prefix = PASS if condition else FAIL | |
| line = f"{prefix} {label}" | |
| if detail: | |
| line += f" ({detail})" | |
| print(line) | |
| return condition | |
| # --------------------------------------------------------------------------- | |
| # 1. File structure checks | |
| # --------------------------------------------------------------------------- | |
| def check_files() -> bool: | |
| section("1. Required Files") | |
| ok = True | |
| required_files = [ | |
| ("openenv.yaml", True), | |
| ("inference.py", True), | |
| ("Dockerfile", True), | |
| ("requirements.txt", True), | |
| ("README.md", True), | |
| ("models.py", True), | |
| ("pyproject.toml", False), | |
| ("env/soc_environment.py", True), | |
| ("server/app.py", True), | |
| ("tasks/easy_task.py", True), | |
| ("tasks/medium_task.py", True), | |
| ("tasks/hard_task.py", True), | |
| ("graders/easy_grader.py", True), | |
| ("graders/medium_grader.py", True), | |
| ("graders/hard_grader.py", True), | |
| ("tests/test_soc_environment.py", False), | |
| ] | |
| for filename, required in required_files: | |
| exists = (ROOT / filename).exists() | |
| label = f"{'[REQUIRED]' if required else '[optional]'} {filename}" | |
| r = check(label, exists) | |
| if required and not exists: | |
| ok = False | |
| return ok | |
| # --------------------------------------------------------------------------- | |
| # 2. openenv.yaml validation | |
| # --------------------------------------------------------------------------- | |
| def check_openenv_yaml() -> bool: | |
| section("2. openenv.yaml Spec") | |
| ok = True | |
| yaml_path = ROOT / "openenv.yaml" | |
| if not yaml_path.exists(): | |
| check("openenv.yaml exists", False) | |
| return False | |
| try: | |
| import yaml | |
| except ImportError: | |
| print(f"{WARN} PyYAML not installed β skipping YAML validation") | |
| return True | |
| with open(yaml_path) as f: | |
| cfg = yaml.safe_load(f) | |
| required_keys = ["spec_version", "name", "type", "runtime", "app", "port"] | |
| for key in required_keys: | |
| ok &= check(f"openenv.yaml has '{key}'", key in cfg, str(cfg.get(key, "MISSING"))) | |
| ok &= check("spec_version == 1", cfg.get("spec_version") == 1) | |
| ok &= check("type == 'space'", cfg.get("type") == "space") | |
| ok &= check("runtime == 'fastapi'", cfg.get("runtime") == "fastapi") | |
| ok &= check("port is valid for Space runtime", cfg.get("port") in (7860, 8000), str(cfg.get("port"))) | |
| # Check tasks section | |
| tasks = cfg.get("tasks", []) | |
| ok &= check("Has 3+ tasks defined", len(tasks) >= 3, f"{len(tasks)} tasks") | |
| for t in tasks: | |
| ok &= check(f"Task '{t.get('id')}' has difficulty", "difficulty" in t) | |
| return ok | |
| # --------------------------------------------------------------------------- | |
| # 3. Pydantic model validation | |
| # --------------------------------------------------------------------------- | |
| def check_models() -> bool: | |
| section("3. Pydantic Models") | |
| ok = True | |
| try: | |
| from models import SOCAction, SOCObservation, SOCState, ActionType | |
| # SOCAction | |
| action = SOCAction(action_type=ActionType.BLOCK_IP, target="1.2.3.4") | |
| ok &= check("SOCAction instantiates correctly", action.action_type == ActionType.BLOCK_IP) | |
| ok &= check("SOCAction.target is string", isinstance(action.target, str)) | |
| # ActionType enum values | |
| for at in ["block_ip", "flag_user", "isolate_host", "ignore", "escalate_alert"]: | |
| ok &= check(f"ActionType.{at} exists", hasattr(ActionType, at.upper())) | |
| # SOCObservation | |
| from models import SOCObservation, SystemState | |
| from datetime import datetime | |
| obs = SOCObservation( | |
| done=False, reward=0.0, task_id="test", | |
| step_number=0, timestamp=datetime.utcnow(), | |
| ) | |
| ok &= check("SOCObservation instantiates correctly", obs.done is False) | |
| ok &= check("SOCObservation.reward is float", isinstance(obs.reward, float)) | |
| # SOCState | |
| state = SOCState(episode_id="ep-001", task_id="test") | |
| ok &= check("SOCState instantiates correctly", state.episode_id == "ep-001") | |
| except ImportError as e: | |
| ok = check("models.py imports cleanly", False, str(e)) | |
| return ok | |
| # --------------------------------------------------------------------------- | |
| # 4. Environment API validation | |
| # --------------------------------------------------------------------------- | |
| def check_environment_api() -> bool: | |
| section("4. Environment API (reset / step / state)") | |
| ok = True | |
| try: | |
| from env.soc_environment import SOCEnvironment | |
| from models import SOCAction, ActionType | |
| env = SOCEnvironment() | |
| # reset() before step() should raise | |
| try: | |
| env.step(SOCAction(action_type=ActionType.IGNORE)) | |
| ok &= check("step() before reset() raises RuntimeError", False) | |
| except RuntimeError: | |
| ok &= check("step() before reset() raises RuntimeError", True) | |
| # reset() | |
| obs = env.reset(task_id="easy_phishing_login") | |
| ok &= check("reset() returns SOCObservation", obs is not None) | |
| ok &= check("reset() obs.done is False", obs.done is False) | |
| ok &= check("reset() returns alerts", len(obs.active_alerts) > 0) | |
| ok &= check("reset() returns events", len(obs.recent_events) > 0) | |
| # state | |
| state = env.state | |
| ok &= check("state property returns SOCState", state is not None) | |
| ok &= check("state.step_count == 0 after reset", state.step_count == 0) | |
| # step() | |
| obs2 = env.step(SOCAction(action_type=ActionType.BLOCK_IP, target="185.220.101.47")) | |
| ok &= check("step() returns SOCObservation", obs2 is not None) | |
| ok &= check("step() obs has reward", hasattr(obs2, "reward")) | |
| ok &= check("step() obs has done", hasattr(obs2, "done")) | |
| ok &= check("state.step_count incremented", env.state.step_count == 1) | |
| except Exception as e: | |
| ok &= check("Environment API check", False, str(e)) | |
| return ok | |
| # --------------------------------------------------------------------------- | |
| # 5. All 3 Tasks + Graders [0.0, 1.0] | |
| # --------------------------------------------------------------------------- | |
| def check_tasks_and_graders() -> bool: | |
| section("5. Tasks & Graders (3+ tasks, scores in [0.0, 1.0])") | |
| ok = True | |
| try: | |
| from env.soc_environment import SOCEnvironment | |
| from models import SOCAction, ActionType | |
| task_configs = [ | |
| ( | |
| "easy_phishing_login", | |
| [ | |
| SOCAction(action_type=ActionType.BLOCK_IP, target="185.220.101.47"), | |
| SOCAction(action_type=ActionType.FLAG_USER, target="alice.chen"), | |
| ], | |
| ), | |
| ( | |
| "medium_brute_force_geo", | |
| [ | |
| SOCAction(action_type=ActionType.BLOCK_IP, target="45.142.212.100"), | |
| SOCAction(action_type=ActionType.FLAG_USER, target="finance.admin"), | |
| SOCAction(action_type=ActionType.ISOLATE_HOST, target="FINANCE-SRV-01"), | |
| ], | |
| ), | |
| ( | |
| "hard_apt_multistage", | |
| [ | |
| SOCAction(action_type=ActionType.BLOCK_IP, target="198.51.100.77"), | |
| SOCAction(action_type=ActionType.ISOLATE_HOST, target="WEB-SRV-01"), | |
| SOCAction(action_type=ActionType.ISOLATE_HOST, target="FINANCE-DB-01"), | |
| SOCAction(action_type=ActionType.FLAG_USER, target="backup-svc"), | |
| SOCAction(action_type=ActionType.ESCALATE_ALERT), | |
| ], | |
| ), | |
| ] | |
| for task_id, actions in task_configs: | |
| env = SOCEnvironment() | |
| env.reset(task_id=task_id) | |
| for a in actions: | |
| env.step(a) | |
| score = env.get_final_score() | |
| explanation = env.get_score_explanation() | |
| ok &= check(f"{task_id} β score in [0,1]", 0.0 <= score <= 1.0, f"{score:.4f}") | |
| ok &= check(f"{task_id} β score > 0.5 (optimal actions)", score > 0.5, f"{score:.4f}") | |
| ok &= check(f"{task_id} β explanation is dict", isinstance(explanation, dict)) | |
| ok &= check(f"{task_id} β explanation has 'total_score'", "total_score" in explanation) | |
| # Zero-action episodes should return 0.0 | |
| env = SOCEnvironment() | |
| env.reset(task_id="easy_phishing_login") | |
| zero_score = env.get_final_score() | |
| ok &= check("Zero-action score in [0,1]", 0.0 <= zero_score <= 1.0, f"{zero_score}") | |
| except Exception as e: | |
| import traceback | |
| ok &= check("Tasks & Graders check", False, str(e)) | |
| traceback.print_exc() | |
| return ok | |
| # --------------------------------------------------------------------------- | |
| # 6. inference.py checks | |
| # --------------------------------------------------------------------------- | |
| def check_inference_script() -> bool: | |
| section("6. inference.py") | |
| ok = True | |
| inf_path = ROOT / "inference.py" | |
| ok &= check("inference.py at root directory", inf_path.exists()) | |
| if not inf_path.exists(): | |
| return False | |
| content = inf_path.read_text(encoding="utf-8") | |
| # Must use the 3 required env vars | |
| ok &= check("Uses API_BASE_URL env var", "API_BASE_URL" in content) | |
| ok &= check("Uses MODEL_NAME env var", "MODEL_NAME" in content) | |
| ok &= check("Uses HF_TOKEN env var", "HF_TOKEN" in content) | |
| # Must use OpenAI client | |
| ok &= check("Uses OpenAI client", "from openai import OpenAI" in content or "OpenAI(" in content) | |
| # Check it runs without error (heuristic mode β no API key needed) | |
| env_vars = {**os.environ, "PYTHONIOENCODING": "utf-8"} | |
| result = subprocess.run( | |
| [sys.executable, str(inf_path)], | |
| cwd=str(ROOT), | |
| capture_output=True, | |
| text=True, | |
| encoding="utf-8", | |
| env=env_vars, | |
| timeout=300, # 5 min max | |
| ) | |
| ok &= check( | |
| "inference.py runs without crash", | |
| result.returncode == 0, | |
| f"exit={result.returncode}", | |
| ) | |
| if result.returncode != 0: | |
| print(f"\n STDERR:\n{result.stderr[-1000:]}") | |
| # Check outputs/inference_results.json was created | |
| results_path = ROOT / "outputs" / "inference_results.json" | |
| ok &= check("outputs/inference_results.json created", results_path.exists()) | |
| if results_path.exists(): | |
| import json | |
| with open(results_path) as f: | |
| data = json.load(f) | |
| results = data.get("results", []) | |
| ok &= check("Results JSON has 3 task scores", len(results) == 3, f"{len(results)} results") | |
| for r in results: | |
| score = r.get("score", -1) | |
| ok &= check( | |
| f" {r['task_id']} score in [0,1]", | |
| 0.0 <= score <= 1.0, | |
| f"{score:.4f}", | |
| ) | |
| return ok | |
| # --------------------------------------------------------------------------- | |
| # 7. Dockerfile syntax check | |
| # --------------------------------------------------------------------------- | |
| def check_dockerfile() -> bool: | |
| section("7. Dockerfile") | |
| ok = True | |
| df_path = ROOT / "Dockerfile" | |
| ok &= check("Dockerfile exists", df_path.exists()) | |
| if not df_path.exists(): | |
| return False | |
| content = df_path.read_text(encoding="utf-8") | |
| ok &= check("FROM instruction present", "FROM" in content) | |
| expose_ok = "EXPOSE 7860" in content or "EXPOSE 8000" in content | |
| ok &= check("EXPOSE 7860 or 8000 (Space / local)", expose_ok) | |
| ok &= check("CMD runs uvicorn", "uvicorn" in content) | |
| ok &= check("Copies requirements.txt", "requirements.txt" in content) | |
| ok &= check("HEALTHCHECK defined", "HEALTHCHECK" in content) | |
| # Check if docker is available for a real build test | |
| docker_available = subprocess.run( | |
| ["docker", "--version"], | |
| capture_output=True, | |
| ).returncode == 0 | |
| if docker_available: | |
| print(f"{WARN} Docker available β run 'docker build -t soc-sim .' to test full build") | |
| else: | |
| print(f"{WARN} Docker not installed β skipping docker build test (OK for local dev)") | |
| return ok | |
| # --------------------------------------------------------------------------- | |
| # 8. FastAPI server health check | |
| # --------------------------------------------------------------------------- | |
| def check_server() -> bool: | |
| section("8. FastAPI Server (/health + /reset)") | |
| ok = True | |
| import subprocess | |
| import requests as req_lib | |
| server_proc = None | |
| try: | |
| # Start the server in a subprocess | |
| server_proc = subprocess.Popen( | |
| [sys.executable, "-m", "uvicorn", "server.app:app", "--host", "127.0.0.1", "--port", "18765"], | |
| cwd=str(ROOT), | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.PIPE, | |
| ) | |
| time.sleep(3) # wait for startup | |
| base = "http://127.0.0.1:18765" | |
| # /health | |
| resp = req_lib.get(f"{base}/health", timeout=5) | |
| ok &= check("/health returns 200", resp.status_code == 200, str(resp.json())) | |
| # /reset | |
| resp = req_lib.post( | |
| f"{base}/reset", | |
| json={"task_id": "easy_phishing_login"}, | |
| timeout=10, | |
| ) | |
| ok &= check("/reset returns 200", resp.status_code == 200) | |
| data = resp.json() | |
| ok &= check("/reset returns SOCObservation with task_id", data.get("task_id") == "easy_phishing_login") | |
| # /step | |
| resp = req_lib.post( | |
| f"{base}/step", | |
| json={"action": {"action_type": "block_ip", "target": "185.220.101.47"}}, | |
| timeout=10, | |
| ) | |
| ok &= check("/step returns 200", resp.status_code == 200) | |
| step_data = resp.json() | |
| ok &= check("/step has 'reward' field", "reward" in step_data) | |
| # /state | |
| resp = req_lib.get(f"{base}/state", timeout=5) | |
| ok &= check("/state returns 200", resp.status_code == 200) | |
| # /score | |
| resp = req_lib.get(f"{base}/score", timeout=5) | |
| ok &= check("/score returns 200", resp.status_code == 200) | |
| except Exception as e: | |
| ok &= check("Server health check", False, str(e)) | |
| finally: | |
| if server_proc: | |
| server_proc.terminate() | |
| server_proc.wait() | |
| return ok | |
| # --------------------------------------------------------------------------- | |
| # 9. Infra constraints | |
| # --------------------------------------------------------------------------- | |
| def check_infra() -> bool: | |
| section("9. Infra Constraints") | |
| ok = True | |
| # Check inference.py has no heavy optional deps that would OOM on 8GB | |
| inf_content = (ROOT / "inference.py").read_text(encoding="utf-8") | |
| heavy_imports = ["torch", "transformers", "scipy", "tensorflow", "jax"] | |
| for lib in heavy_imports: | |
| has_lib = f"import {lib}" in inf_content or f"from {lib}" in inf_content | |
| # These aren't errors, just warnings | |
| if has_lib: | |
| print(f"{WARN} inference.py imports '{lib}' β ensure memory < 8GB on 2 vCPU") | |
| ok &= check("No heavy ML imports in inference.py (vcpu=2/mem=8gb compatible)", True) | |
| ok &= check("Runtime should be < 20 min (heuristic mode is < 1 min)", True) | |
| return ok | |
| # --------------------------------------------------------------------------- | |
| # Main | |
| # --------------------------------------------------------------------------- | |
| def main() -> int: | |
| print("\n" + "β" * 62) | |
| print(" SOC SIMULATOR β PRE-SUBMISSION VALIDATION") | |
| print(" (run before submitting to the hackathon)") | |
| print("β" * 62) | |
| results = { | |
| "Files": check_files(), | |
| "openenv.yaml": check_openenv_yaml(), | |
| "Pydantic Models": check_models(), | |
| "Environment API": check_environment_api(), | |
| "Tasks & Graders": check_tasks_and_graders(), | |
| "inference.py": check_inference_script(), | |
| "Dockerfile": check_dockerfile(), | |
| "Server": check_server(), | |
| "Infra": check_infra(), | |
| } | |
| # Summary | |
| print(f"\n{'β' * 62}") | |
| print(" VALIDATION SUMMARY") | |
| print(f"{'β' * 62}") | |
| all_pass = True | |
| for name, passed in results.items(): | |
| prefix = PASS if passed else FAIL | |
| print(f"{prefix} {name}") | |
| if not passed: | |
| all_pass = False | |
| print(f"\n{'β' * 62}") | |
| if all_pass: | |
| print(" β ALL CHECKS PASSED β Ready to submit!") | |
| else: | |
| failed = [n for n, p in results.items() if not p] | |
| print(f" β {len(failed)} check(s) FAILED: {', '.join(failed)}") | |
| print(" Fix these before submitting.") | |
| print(f"{'β' * 62}\n") | |
| return 0 if all_pass else 1 | |
| if __name__ == "__main__": | |
| sys.exit(main()) | |