Spaces:
Runtime error
Runtime error
Merge remote-tracking branch 'origin/main' into codex/issue-77-20260308
Browse files# Conflicts:
# Dockerfile
# README.md
# src/open_range/server/runtime.py
- Dockerfile +2 -4
- README.md +11 -6
- src/open_range/server/runtime.py +65 -18
- src/open_range/validator/isolation.py +8 -2
- tests/test_runtime.py +58 -0
- tests/test_validator.py +64 -1
Dockerfile
CHANGED
|
@@ -86,10 +86,8 @@ ENV PYTHONPATH="/app/env/src:/app/env"
|
|
| 86 |
ENV OPENRANGE_EXECUTION_MODE=subprocess
|
| 87 |
# Enable the managed runtime so reset() boots real services from the manifest
|
| 88 |
ENV OPENRANGE_RUNTIME_MANIFEST=manifests/tier1_basic.yaml
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
ENV OPENRANGE_RUNTIME_VALIDATOR_PROFILE=offline
|
| 92 |
-
ENV OPENRANGE_ALLOW_OFFLINE_ADMISSION=1
|
| 93 |
ENV OPENRANGE_SNAPSHOT_POOL_SIZE=1
|
| 94 |
# Enable the OpenEnv Gradio web interface at /web
|
| 95 |
ENV ENABLE_WEB_INTERFACE=true
|
|
|
|
| 86 |
ENV OPENRANGE_EXECUTION_MODE=subprocess
|
| 87 |
# Enable the managed runtime so reset() boots real services from the manifest
|
| 88 |
ENV OPENRANGE_RUNTIME_MANIFEST=manifests/tier1_basic.yaml
|
| 89 |
+
ENV OPENRANGE_RUNTIME_VALIDATOR_PROFILE=training
|
| 90 |
+
ENV OPENRANGE_ENABLE_LIVE_ADMISSION=1
|
|
|
|
|
|
|
| 91 |
ENV OPENRANGE_SNAPSHOT_POOL_SIZE=1
|
| 92 |
# Enable the OpenEnv Gradio web interface at /web
|
| 93 |
ENV ENABLE_WEB_INTERFACE=true
|
README.md
CHANGED
|
@@ -100,14 +100,19 @@ uv run pytest tests/ -v --tb=short
|
|
| 100 |
|
| 101 |
The deployed package exposes the standard OpenEnv `reset()`, `step()`, and `state()` contract through `server.app:app`, which is the entrypoint referenced by `openenv.yaml`.
|
| 102 |
|
| 103 |
-
**Validator** — Admission gate for candidate snapshots.
|
| 104 |
|
| 105 |
-
|
| 106 |
-
|-------------------|-----------------|-----------------------|--------------|
|
| 107 |
-
| `training` (default) | Graph + structural + task + build/boot + exploitability + patchability + evidence + reward grounding + isolation + difficulty + NPC consistency + realism review | Strict live/container-backed admission | Managed/production runtime |
|
| 108 |
-
| `offline` | Graph + structural + task feasibility only | No live exploit/patch/evidence/reward validation | Local fallback only (must set `OPENRANGE_ALLOW_OFFLINE_ADMISSION=1`) |
|
| 109 |
|
| 110 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
|
| 112 |
**Environment** — `RangeEnvironment(Environment)` following the OpenEnv contract. `reset()` asks the shared runtime for a frozen admitted snapshot. `step(action)` routes commands to the appropriate container — Red runs on the attacker box, Blue runs on the SIEM. No artificial command allowlists; the container's installed tools are the constraint.
|
| 113 |
|
|
|
|
| 100 |
|
| 101 |
The deployed package exposes the standard OpenEnv `reset()`, `step()`, and `state()` contract through `server.app:app`, which is the entrypoint referenced by `openenv.yaml`.
|
| 102 |
|
| 103 |
+
**Validator** — Admission gate for candidate snapshots. The shipped runtime enforces manifest compliance plus graph-native checks such as graph consistency, path solvability, evidence sufficiency, and reward grounding before structural/task checks. With the `training` profile, the runtime boots rendered bundles, applies payload files, constructs a real `ContainerSet`, and runs live build/exploit/patch/evidence/reward/isolation/difficulty/NPC/realism checks before admission.
|
| 104 |
|
| 105 |
+
Validator profile matrix:
|
|
|
|
|
|
|
|
|
|
| 106 |
|
| 107 |
+
| Profile | Checks | Guarantees |
|
| 108 |
+
|---------|--------|------------|
|
| 109 |
+
| `offline` | Graph + structural/task checks only (no live containers) | Fast static admission only; no live exploitability/patchability guarantee |
|
| 110 |
+
| `training` | `offline` checks + live/container-backed checks | Full admission guarantees for managed training/runtime use |
|
| 111 |
+
|
| 112 |
+
Managed runtime defaults and safety behavior:
|
| 113 |
+
- `OPENRANGE_RUNTIME_VALIDATOR_PROFILE` defaults to `training`.
|
| 114 |
+
- `OPENRANGE_ENABLE_LIVE_ADMISSION` defaults to `1`.
|
| 115 |
+
- If managed runtime is configured non-live (`offline` profile and/or live admission disabled), startup raises an error unless you explicitly opt out with `OPENRANGE_ALLOW_NON_LIVE_ADMISSION=1` (legacy alias: `OPENRANGE_ALLOW_OFFLINE_ADMISSION=1`), in which case a warning is emitted.
|
| 116 |
|
| 117 |
**Environment** — `RangeEnvironment(Environment)` following the OpenEnv contract. `reset()` asks the shared runtime for a frozen admitted snapshot. `step(action)` routes commands to the appropriate container — Red runs on the attacker box, Blue runs on the SIEM. No artificial command allowlists; the container's installed tools are the constraint.
|
| 118 |
|
src/open_range/server/runtime.py
CHANGED
|
@@ -59,7 +59,11 @@ from open_range.validator.validator import ValidationResult, ValidatorGate
|
|
| 59 |
logger = logging.getLogger(__name__)
|
| 60 |
|
| 61 |
_DEFAULT_MANIFEST = ("manifests", "tier1_basic.yaml")
|
| 62 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
_VALIDATOR_PROFILE_ALIASES = {
|
| 64 |
"light": "offline",
|
| 65 |
"static": "offline",
|
|
@@ -67,7 +71,6 @@ _VALIDATOR_PROFILE_ALIASES = {
|
|
| 67 |
"strict": "training",
|
| 68 |
}
|
| 69 |
_LIVE_VALIDATOR_PROFILES = {"training"}
|
| 70 |
-
_ALLOW_OFFLINE_ADMISSION_ENV = "OPENRANGE_ALLOW_OFFLINE_ADMISSION"
|
| 71 |
_PERSISTED_SNAPSHOT_VALIDATION_ALIASES = {
|
| 72 |
"none": "trust",
|
| 73 |
"disabled": "trust",
|
|
@@ -91,6 +94,21 @@ def _env_int(name: str, default: int) -> int:
|
|
| 91 |
return int(raw)
|
| 92 |
|
| 93 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
def _candidate_roots() -> list[Path]:
|
| 95 |
roots: list[Path] = []
|
| 96 |
cwd = Path.cwd()
|
|
@@ -377,6 +395,19 @@ def _build_validator(profile: str, manifest: dict[str, Any]) -> ValidatorGate:
|
|
| 377 |
)
|
| 378 |
|
| 379 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 380 |
def _default_live_validator(*, include_patchability: bool = False) -> ValidatorGate:
|
| 381 |
checks = [
|
| 382 |
BuildBootCheck(),
|
|
@@ -428,7 +459,7 @@ class ManagedSnapshotRuntime:
|
|
| 428 |
self.mutation_policy = mutation_policy or PopulationMutationPolicy()
|
| 429 |
self.mutator = Mutator(self.builder, policy=self.mutation_policy)
|
| 430 |
self.allow_insecure_offline_profile = (
|
| 431 |
-
|
| 432 |
if allow_insecure_offline_profile is None
|
| 433 |
else bool(allow_insecure_offline_profile)
|
| 434 |
)
|
|
@@ -474,27 +505,42 @@ class ManagedSnapshotRuntime:
|
|
| 474 |
|
| 475 |
@classmethod
|
| 476 |
def from_env(cls) -> "ManagedSnapshotRuntime":
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 477 |
return cls(
|
| 478 |
manifest_path=os.getenv("OPENRANGE_RUNTIME_MANIFEST"),
|
| 479 |
store_dir=os.getenv("OPENRANGE_SNAPSHOT_DIR"),
|
| 480 |
-
validator_profile=
|
| 481 |
-
|
| 482 |
-
_DEFAULT_VALIDATOR_PROFILE,
|
| 483 |
-
),
|
| 484 |
-
allow_insecure_offline_profile=_env_flag(
|
| 485 |
-
_ALLOW_OFFLINE_ADMISSION_ENV,
|
| 486 |
-
default=False,
|
| 487 |
-
),
|
| 488 |
pool_size=_env_int("OPENRANGE_SNAPSHOT_POOL_SIZE", 3),
|
| 489 |
selection_strategy=os.getenv("OPENRANGE_SNAPSHOT_SELECTION", "random"),
|
| 490 |
parent_selection_strategy=os.getenv("OPENRANGE_PARENT_SELECTION", "policy"),
|
| 491 |
refill_enabled=_env_flag("OPENRANGE_ENABLE_MANAGED_REFILL", default=False),
|
| 492 |
refill_interval_s=float(os.getenv("OPENRANGE_REFILL_INTERVAL_S", "2.0")),
|
| 493 |
generation_retries=_env_int("OPENRANGE_GENERATION_RETRIES", 3),
|
| 494 |
-
live_admission_enabled=
|
| 495 |
-
"OPENRANGE_ENABLE_LIVE_ADMISSION",
|
| 496 |
-
default=False,
|
| 497 |
-
),
|
| 498 |
teardown_booted_projects=not _env_flag(
|
| 499 |
"OPENRANGE_KEEP_BOOTED_VALIDATION_STACKS",
|
| 500 |
default=False,
|
|
@@ -523,12 +569,13 @@ class ManagedSnapshotRuntime:
|
|
| 523 |
raise RuntimeError(
|
| 524 |
warning
|
| 525 |
+ " Set OPENRANGE_RUNTIME_VALIDATOR_PROFILE=training for strict admission, "
|
| 526 |
-
+
|
|
|
|
|
|
|
| 527 |
)
|
| 528 |
logger.warning(
|
| 529 |
-
"%s Running with explicit opt-out
|
| 530 |
warning,
|
| 531 |
-
_ALLOW_OFFLINE_ADMISSION_ENV,
|
| 532 |
)
|
| 533 |
|
| 534 |
@staticmethod
|
|
|
|
| 59 |
logger = logging.getLogger(__name__)
|
| 60 |
|
| 61 |
_DEFAULT_MANIFEST = ("manifests", "tier1_basic.yaml")
|
| 62 |
+
_DEFAULT_MANAGED_VALIDATOR_PROFILE = "training"
|
| 63 |
+
_DEFAULT_MANAGED_LIVE_ADMISSION = True
|
| 64 |
+
_ALLOW_NON_LIVE_ADMISSION_ENV = "OPENRANGE_ALLOW_NON_LIVE_ADMISSION"
|
| 65 |
+
_ALLOW_OFFLINE_ADMISSION_ENV = "OPENRANGE_ALLOW_OFFLINE_ADMISSION"
|
| 66 |
+
_DEFAULT_VALIDATOR_PROFILE = _DEFAULT_MANAGED_VALIDATOR_PROFILE
|
| 67 |
_VALIDATOR_PROFILE_ALIASES = {
|
| 68 |
"light": "offline",
|
| 69 |
"static": "offline",
|
|
|
|
| 71 |
"strict": "training",
|
| 72 |
}
|
| 73 |
_LIVE_VALIDATOR_PROFILES = {"training"}
|
|
|
|
| 74 |
_PERSISTED_SNAPSHOT_VALIDATION_ALIASES = {
|
| 75 |
"none": "trust",
|
| 76 |
"disabled": "trust",
|
|
|
|
| 94 |
return int(raw)
|
| 95 |
|
| 96 |
|
| 97 |
+
def _non_live_opt_out_enabled() -> bool:
|
| 98 |
+
return _env_flag(_ALLOW_NON_LIVE_ADMISSION_ENV, default=False) or _env_flag(
|
| 99 |
+
_ALLOW_OFFLINE_ADMISSION_ENV,
|
| 100 |
+
default=False,
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def _non_live_opt_out_env_name() -> str | None:
|
| 105 |
+
if _env_flag(_ALLOW_NON_LIVE_ADMISSION_ENV, default=False):
|
| 106 |
+
return _ALLOW_NON_LIVE_ADMISSION_ENV
|
| 107 |
+
if _env_flag(_ALLOW_OFFLINE_ADMISSION_ENV, default=False):
|
| 108 |
+
return _ALLOW_OFFLINE_ADMISSION_ENV
|
| 109 |
+
return None
|
| 110 |
+
|
| 111 |
+
|
| 112 |
def _candidate_roots() -> list[Path]:
|
| 113 |
roots: list[Path] = []
|
| 114 |
cwd = Path.cwd()
|
|
|
|
| 395 |
)
|
| 396 |
|
| 397 |
|
| 398 |
+
def _strict_admission_enabled(profile: str, live_admission_enabled: bool) -> bool:
|
| 399 |
+
return profile in _LIVE_VALIDATOR_PROFILES and live_admission_enabled
|
| 400 |
+
|
| 401 |
+
|
| 402 |
+
def _managed_admission_failure_message(profile: str, live_admission_enabled: bool) -> str:
|
| 403 |
+
return (
|
| 404 |
+
"Managed runtime requires strict live admission "
|
| 405 |
+
f"(validator_profile='training', live_admission_enabled=1). "
|
| 406 |
+
f"Current configuration: validator_profile={profile!r}, "
|
| 407 |
+
f"live_admission_enabled={live_admission_enabled!r}."
|
| 408 |
+
)
|
| 409 |
+
|
| 410 |
+
|
| 411 |
def _default_live_validator(*, include_patchability: bool = False) -> ValidatorGate:
|
| 412 |
checks = [
|
| 413 |
BuildBootCheck(),
|
|
|
|
| 459 |
self.mutation_policy = mutation_policy or PopulationMutationPolicy()
|
| 460 |
self.mutator = Mutator(self.builder, policy=self.mutation_policy)
|
| 461 |
self.allow_insecure_offline_profile = (
|
| 462 |
+
_non_live_opt_out_enabled()
|
| 463 |
if allow_insecure_offline_profile is None
|
| 464 |
else bool(allow_insecure_offline_profile)
|
| 465 |
)
|
|
|
|
| 505 |
|
| 506 |
@classmethod
|
| 507 |
def from_env(cls) -> "ManagedSnapshotRuntime":
|
| 508 |
+
profile = _normalize_validator_profile(
|
| 509 |
+
os.getenv(
|
| 510 |
+
"OPENRANGE_RUNTIME_VALIDATOR_PROFILE",
|
| 511 |
+
_DEFAULT_MANAGED_VALIDATOR_PROFILE,
|
| 512 |
+
)
|
| 513 |
+
)
|
| 514 |
+
live_admission_enabled = _env_flag(
|
| 515 |
+
"OPENRANGE_ENABLE_LIVE_ADMISSION",
|
| 516 |
+
default=_DEFAULT_MANAGED_LIVE_ADMISSION,
|
| 517 |
+
)
|
| 518 |
+
if not _strict_admission_enabled(profile, live_admission_enabled):
|
| 519 |
+
message = _managed_admission_failure_message(profile, live_admission_enabled)
|
| 520 |
+
opt_out_env = _non_live_opt_out_env_name()
|
| 521 |
+
if opt_out_env:
|
| 522 |
+
logger.warning(
|
| 523 |
+
"%s Explicit opt-out enabled via %s=1.",
|
| 524 |
+
message,
|
| 525 |
+
opt_out_env,
|
| 526 |
+
)
|
| 527 |
+
else:
|
| 528 |
+
raise RuntimeError(
|
| 529 |
+
f"{message} Set {_ALLOW_NON_LIVE_ADMISSION_ENV}=1 to explicitly opt out."
|
| 530 |
+
)
|
| 531 |
+
|
| 532 |
return cls(
|
| 533 |
manifest_path=os.getenv("OPENRANGE_RUNTIME_MANIFEST"),
|
| 534 |
store_dir=os.getenv("OPENRANGE_SNAPSHOT_DIR"),
|
| 535 |
+
validator_profile=profile,
|
| 536 |
+
allow_insecure_offline_profile=_non_live_opt_out_enabled(),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 537 |
pool_size=_env_int("OPENRANGE_SNAPSHOT_POOL_SIZE", 3),
|
| 538 |
selection_strategy=os.getenv("OPENRANGE_SNAPSHOT_SELECTION", "random"),
|
| 539 |
parent_selection_strategy=os.getenv("OPENRANGE_PARENT_SELECTION", "policy"),
|
| 540 |
refill_enabled=_env_flag("OPENRANGE_ENABLE_MANAGED_REFILL", default=False),
|
| 541 |
refill_interval_s=float(os.getenv("OPENRANGE_REFILL_INTERVAL_S", "2.0")),
|
| 542 |
generation_retries=_env_int("OPENRANGE_GENERATION_RETRIES", 3),
|
| 543 |
+
live_admission_enabled=live_admission_enabled,
|
|
|
|
|
|
|
|
|
|
| 544 |
teardown_booted_projects=not _env_flag(
|
| 545 |
"OPENRANGE_KEEP_BOOTED_VALIDATION_STACKS",
|
| 546 |
default=False,
|
|
|
|
| 569 |
raise RuntimeError(
|
| 570 |
warning
|
| 571 |
+ " Set OPENRANGE_RUNTIME_VALIDATOR_PROFILE=training for strict admission, "
|
| 572 |
+
+ "or set OPENRANGE_ALLOW_NON_LIVE_ADMISSION=1 "
|
| 573 |
+
+ "(legacy alias: OPENRANGE_ALLOW_OFFLINE_ADMISSION=1) "
|
| 574 |
+
+ "to explicitly opt out."
|
| 575 |
)
|
| 576 |
logger.warning(
|
| 577 |
+
"%s Running with explicit opt-out.",
|
| 578 |
warning,
|
|
|
|
| 579 |
)
|
| 580 |
|
| 581 |
@staticmethod
|
src/open_range/validator/isolation.py
CHANGED
|
@@ -2,6 +2,8 @@
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
|
|
|
|
|
|
| 5 |
from open_range.protocols import CheckResult, ContainerSet, SnapshotSpec
|
| 6 |
|
| 7 |
# Common service ports to probe for zone isolation violations.
|
|
@@ -33,10 +35,14 @@ class IsolationCheck:
|
|
| 33 |
open_ports: list[int] = []
|
| 34 |
for port in _PROBE_PORTS:
|
| 35 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
output = await containers.exec(
|
| 37 |
attacker_host,
|
| 38 |
-
|
| 39 |
-
f"2>/dev/null && echo OPEN || echo CLOSED",
|
| 40 |
)
|
| 41 |
if "OPEN" in output:
|
| 42 |
open_ports.append(port)
|
|
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
+
import shlex
|
| 6 |
+
|
| 7 |
from open_range.protocols import CheckResult, ContainerSet, SnapshotSpec
|
| 8 |
|
| 9 |
# Common service ports to probe for zone isolation violations.
|
|
|
|
| 35 |
open_ports: list[int] = []
|
| 36 |
for port in _PROBE_PORTS:
|
| 37 |
try:
|
| 38 |
+
probe_cmd = (
|
| 39 |
+
"timeout 2 bash -lc 'echo > /dev/tcp/\"$1\"/\"$2\"' _ "
|
| 40 |
+
f"{shlex.quote(target_name)} {shlex.quote(str(port))} "
|
| 41 |
+
"2>/dev/null && echo OPEN || echo CLOSED"
|
| 42 |
+
)
|
| 43 |
output = await containers.exec(
|
| 44 |
attacker_host,
|
| 45 |
+
probe_cmd,
|
|
|
|
| 46 |
)
|
| 47 |
if "OPEN" in output:
|
| 48 |
open_ports.append(port)
|
tests/test_runtime.py
CHANGED
|
@@ -3,9 +3,11 @@
|
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
import json
|
|
|
|
| 6 |
from pathlib import Path
|
| 7 |
|
| 8 |
import pytest
|
|
|
|
| 9 |
|
| 10 |
from open_range.protocols import CheckResult, ContainerSet, SnapshotSpec
|
| 11 |
from open_range.server.compose_runner import BootedSnapshotProject
|
|
@@ -15,6 +17,62 @@ from open_range.validator.validator import ValidationResult
|
|
| 15 |
|
| 16 |
|
| 17 |
class TestManagedSnapshotRuntime:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
def test_offline_validator_profile_includes_static_checks(self, tier1_manifest, tmp_path):
|
| 19 |
runtime = ManagedSnapshotRuntime(
|
| 20 |
manifest=tier1_manifest,
|
|
|
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
import json
|
| 6 |
+
import logging
|
| 7 |
from pathlib import Path
|
| 8 |
|
| 9 |
import pytest
|
| 10 |
+
import yaml
|
| 11 |
|
| 12 |
from open_range.protocols import CheckResult, ContainerSet, SnapshotSpec
|
| 13 |
from open_range.server.compose_runner import BootedSnapshotProject
|
|
|
|
| 17 |
|
| 18 |
|
| 19 |
class TestManagedSnapshotRuntime:
|
| 20 |
+
def test_from_env_defaults_to_training_and_live(self, tier1_manifest, tmp_path, monkeypatch):
|
| 21 |
+
manifest_path = tmp_path / "manifest.yaml"
|
| 22 |
+
manifest_path.write_text(yaml.safe_dump(tier1_manifest), encoding="utf-8")
|
| 23 |
+
|
| 24 |
+
monkeypatch.setenv("OPENRANGE_RUNTIME_MANIFEST", str(manifest_path))
|
| 25 |
+
monkeypatch.setenv("OPENRANGE_SNAPSHOT_DIR", str(tmp_path / "snapshots"))
|
| 26 |
+
monkeypatch.delenv("OPENRANGE_RUNTIME_VALIDATOR_PROFILE", raising=False)
|
| 27 |
+
monkeypatch.delenv("OPENRANGE_ENABLE_LIVE_ADMISSION", raising=False)
|
| 28 |
+
monkeypatch.delenv("OPENRANGE_ALLOW_NON_LIVE_ADMISSION", raising=False)
|
| 29 |
+
|
| 30 |
+
runtime = ManagedSnapshotRuntime.from_env()
|
| 31 |
+
assert runtime.validator_profile == "training"
|
| 32 |
+
assert runtime.live_admission_enabled is True
|
| 33 |
+
|
| 34 |
+
def test_from_env_rejects_non_live_without_explicit_opt_out(
|
| 35 |
+
self,
|
| 36 |
+
tier1_manifest,
|
| 37 |
+
tmp_path,
|
| 38 |
+
monkeypatch,
|
| 39 |
+
):
|
| 40 |
+
manifest_path = tmp_path / "manifest.yaml"
|
| 41 |
+
manifest_path.write_text(yaml.safe_dump(tier1_manifest), encoding="utf-8")
|
| 42 |
+
|
| 43 |
+
monkeypatch.setenv("OPENRANGE_RUNTIME_MANIFEST", str(manifest_path))
|
| 44 |
+
monkeypatch.setenv("OPENRANGE_SNAPSHOT_DIR", str(tmp_path / "snapshots"))
|
| 45 |
+
monkeypatch.setenv("OPENRANGE_RUNTIME_VALIDATOR_PROFILE", "offline")
|
| 46 |
+
monkeypatch.setenv("OPENRANGE_ENABLE_LIVE_ADMISSION", "0")
|
| 47 |
+
monkeypatch.delenv("OPENRANGE_ALLOW_NON_LIVE_ADMISSION", raising=False)
|
| 48 |
+
|
| 49 |
+
with pytest.raises(RuntimeError, match="OPENRANGE_ALLOW_NON_LIVE_ADMISSION=1"):
|
| 50 |
+
ManagedSnapshotRuntime.from_env()
|
| 51 |
+
|
| 52 |
+
def test_from_env_allows_non_live_with_explicit_opt_out_warning(
|
| 53 |
+
self,
|
| 54 |
+
tier1_manifest,
|
| 55 |
+
tmp_path,
|
| 56 |
+
monkeypatch,
|
| 57 |
+
caplog,
|
| 58 |
+
):
|
| 59 |
+
manifest_path = tmp_path / "manifest.yaml"
|
| 60 |
+
manifest_path.write_text(yaml.safe_dump(tier1_manifest), encoding="utf-8")
|
| 61 |
+
|
| 62 |
+
monkeypatch.setenv("OPENRANGE_RUNTIME_MANIFEST", str(manifest_path))
|
| 63 |
+
monkeypatch.setenv("OPENRANGE_SNAPSHOT_DIR", str(tmp_path / "snapshots"))
|
| 64 |
+
monkeypatch.setenv("OPENRANGE_RUNTIME_VALIDATOR_PROFILE", "offline")
|
| 65 |
+
monkeypatch.setenv("OPENRANGE_ENABLE_LIVE_ADMISSION", "0")
|
| 66 |
+
monkeypatch.setenv("OPENRANGE_ALLOW_NON_LIVE_ADMISSION", "1")
|
| 67 |
+
|
| 68 |
+
with caplog.at_level(logging.WARNING):
|
| 69 |
+
runtime = ManagedSnapshotRuntime.from_env()
|
| 70 |
+
|
| 71 |
+
assert runtime.validator_profile == "offline"
|
| 72 |
+
assert runtime.live_admission_enabled is False
|
| 73 |
+
assert "strict live admission" in caplog.text
|
| 74 |
+
assert "OPENRANGE_ALLOW_NON_LIVE_ADMISSION=1" in caplog.text
|
| 75 |
+
|
| 76 |
def test_offline_validator_profile_includes_static_checks(self, tier1_manifest, tmp_path):
|
| 77 |
runtime = ManagedSnapshotRuntime(
|
| 78 |
manifest=tier1_manifest,
|
tests/test_validator.py
CHANGED
|
@@ -8,6 +8,7 @@ import pytest
|
|
| 8 |
from open_range.protocols import (
|
| 9 |
CheckResult,
|
| 10 |
EvidenceItem,
|
|
|
|
| 11 |
ExploitStep,
|
| 12 |
FlagSpec,
|
| 13 |
GoldenPathStep,
|
|
@@ -795,6 +796,36 @@ async def test_evidence_fails_on_nonzero_exit_even_when_output_present(mock_cont
|
|
| 795 |
assert result.details["missing"][0]["location"] == "siem:/var/log/test.log"
|
| 796 |
|
| 797 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 798 |
# ---------------------------------------------------------------------------
|
| 799 |
# Check 5: Reward grounding
|
| 800 |
# ---------------------------------------------------------------------------
|
|
@@ -1053,7 +1084,7 @@ async def test_isolation_fails_on_non_ssh_port(mock_containers):
|
|
| 1053 |
# Only port 3306 is OPEN; everything else CLOSED.
|
| 1054 |
async def exec_side_effect(container, cmd, **kwargs):
|
| 1055 |
if container == "attacker" and "/dev/tcp/" in cmd:
|
| 1056 |
-
if "
|
| 1057 |
return "OPEN"
|
| 1058 |
return "CLOSED"
|
| 1059 |
return ""
|
|
@@ -1065,6 +1096,38 @@ async def test_isolation_fails_on_non_ssh_port(mock_containers):
|
|
| 1065 |
assert "db" in result.error
|
| 1066 |
|
| 1067 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1068 |
# ---------------------------------------------------------------------------
|
| 1069 |
# Check 7: Task feasibility
|
| 1070 |
# ---------------------------------------------------------------------------
|
|
|
|
| 8 |
from open_range.protocols import (
|
| 9 |
CheckResult,
|
| 10 |
EvidenceItem,
|
| 11 |
+
ExecResult,
|
| 12 |
ExploitStep,
|
| 13 |
FlagSpec,
|
| 14 |
GoldenPathStep,
|
|
|
|
| 796 |
assert result.details["missing"][0]["location"] == "siem:/var/log/test.log"
|
| 797 |
|
| 798 |
|
| 799 |
+
@pytest.mark.asyncio
|
| 800 |
+
async def test_evidence_quotes_pattern_and_location_path():
|
| 801 |
+
"""Evidence grep command must quote pattern and path from snapshot content."""
|
| 802 |
+
import shlex
|
| 803 |
+
|
| 804 |
+
from open_range.validator.evidence import EvidenceCheck
|
| 805 |
+
|
| 806 |
+
class RecordingContainers:
|
| 807 |
+
def __init__(self) -> None:
|
| 808 |
+
self.calls: list[tuple[str, str]] = []
|
| 809 |
+
|
| 810 |
+
async def exec_run(self, container: str, cmd: str, **kwargs) -> ExecResult:
|
| 811 |
+
self.calls.append((container, cmd))
|
| 812 |
+
return ExecResult(stdout="1", exit_code=0)
|
| 813 |
+
|
| 814 |
+
containers = RecordingContainers()
|
| 815 |
+
pattern = "ERR'; touch /tmp/pwn #"
|
| 816 |
+
path = "/var/log/app; echo PWNED"
|
| 817 |
+
spec = SnapshotSpec(
|
| 818 |
+
evidence_spec=[
|
| 819 |
+
EvidenceItem(type="log_entry", location=f"siem:{path}", pattern=pattern),
|
| 820 |
+
],
|
| 821 |
+
)
|
| 822 |
+
|
| 823 |
+
result = await EvidenceCheck().check(spec, containers) # type: ignore[arg-type]
|
| 824 |
+
assert result.passed is True
|
| 825 |
+
assert containers.calls
|
| 826 |
+
assert containers.calls[0][1] == f"grep -c {shlex.quote(pattern)} {shlex.quote(path)}"
|
| 827 |
+
|
| 828 |
+
|
| 829 |
# ---------------------------------------------------------------------------
|
| 830 |
# Check 5: Reward grounding
|
| 831 |
# ---------------------------------------------------------------------------
|
|
|
|
| 1084 |
# Only port 3306 is OPEN; everything else CLOSED.
|
| 1085 |
async def exec_side_effect(container, cmd, **kwargs):
|
| 1086 |
if container == "attacker" and "/dev/tcp/" in cmd:
|
| 1087 |
+
if " 3306 " in cmd:
|
| 1088 |
return "OPEN"
|
| 1089 |
return "CLOSED"
|
| 1090 |
return ""
|
|
|
|
| 1096 |
assert "db" in result.error
|
| 1097 |
|
| 1098 |
|
| 1099 |
+
@pytest.mark.asyncio
|
| 1100 |
+
async def test_isolation_uses_argument_safe_tcp_probe_for_target_name():
|
| 1101 |
+
"""Target names are passed as positional args, not interpolated into script."""
|
| 1102 |
+
from open_range.validator.isolation import IsolationCheck
|
| 1103 |
+
|
| 1104 |
+
class RecordingContainers:
|
| 1105 |
+
def __init__(self) -> None:
|
| 1106 |
+
self.calls: list[tuple[str, str]] = []
|
| 1107 |
+
|
| 1108 |
+
async def exec(self, container: str, cmd: str, **kwargs) -> str:
|
| 1109 |
+
self.calls.append((container, cmd))
|
| 1110 |
+
return "CLOSED"
|
| 1111 |
+
|
| 1112 |
+
containers = RecordingContainers()
|
| 1113 |
+
target = "db'; touch /tmp/pwn #"
|
| 1114 |
+
spec = SnapshotSpec(
|
| 1115 |
+
topology={"hosts": ["attacker", "db"], "zones": {"internal": [target]}},
|
| 1116 |
+
flags=[],
|
| 1117 |
+
golden_path=[],
|
| 1118 |
+
task=TaskSpec(red_briefing="Go.", blue_briefing="Watch."),
|
| 1119 |
+
)
|
| 1120 |
+
|
| 1121 |
+
result = await IsolationCheck().check(spec, containers) # type: ignore[arg-type]
|
| 1122 |
+
assert result.passed is True
|
| 1123 |
+
assert containers.calls
|
| 1124 |
+
first_cmd = containers.calls[0][1]
|
| 1125 |
+
script_part, _, arg_part = first_cmd.partition(" _ ")
|
| 1126 |
+
assert "bash -lc 'echo > /dev/tcp/\"$1\"/\"$2\"'" in script_part
|
| 1127 |
+
assert "touch /tmp/pwn" not in script_part
|
| 1128 |
+
assert "touch /tmp/pwn" in arg_part
|
| 1129 |
+
|
| 1130 |
+
|
| 1131 |
# ---------------------------------------------------------------------------
|
| 1132 |
# Check 7: Task feasibility
|
| 1133 |
# ---------------------------------------------------------------------------
|