Spaces:
Runtime error
Runtime error
Lars Talian commited on
Harden exploitability check expectations (#84)
Browse files
src/open_range/validator/exploitability.py
CHANGED
|
@@ -15,6 +15,16 @@ _META_COMMANDS = {"submit_flag", "submit_evidence", "submit_finding", "auth", "l
|
|
| 15 |
class ExploitabilityCheck:
|
| 16 |
"""Execute every golden-path step and verify ``expect_in_stdout`` appears."""
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
async def check(self, snapshot: SnapshotSpec, containers: ContainerSet) -> CheckResult:
|
| 19 |
if not snapshot.golden_path:
|
| 20 |
return CheckResult(
|
|
@@ -43,12 +53,20 @@ class ExploitabilityCheck:
|
|
| 43 |
|
| 44 |
expected = step.expect_in_stdout
|
| 45 |
if not expected:
|
| 46 |
-
|
| 47 |
-
"
|
| 48 |
-
"output not validated",
|
| 49 |
-
step.step,
|
| 50 |
)
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
elif expected not in output:
|
| 53 |
failed_steps.append({
|
| 54 |
"step": step.step,
|
|
@@ -71,6 +89,7 @@ class ExploitabilityCheck:
|
|
| 71 |
"unvalidated_steps": unvalidated_steps,
|
| 72 |
"issues": issues,
|
| 73 |
"total_steps": len(snapshot.golden_path),
|
|
|
|
| 74 |
},
|
| 75 |
error="" if passed else f"{len(failed_steps)} golden-path step(s) failed",
|
| 76 |
)
|
|
|
|
| 15 |
class ExploitabilityCheck:
|
| 16 |
"""Execute every golden-path step and verify ``expect_in_stdout`` appears."""
|
| 17 |
|
| 18 |
+
def __init__(self, *, require_expectation: bool = True) -> None:
|
| 19 |
+
"""Create an exploitability check.
|
| 20 |
+
|
| 21 |
+
Args:
|
| 22 |
+
require_expectation: When ``True`` (default), every non-meta golden
|
| 23 |
+
path step must define ``expect_in_stdout``. Missing expectations
|
| 24 |
+
are treated as validation failures.
|
| 25 |
+
"""
|
| 26 |
+
self.require_expectation = require_expectation
|
| 27 |
+
|
| 28 |
async def check(self, snapshot: SnapshotSpec, containers: ContainerSet) -> CheckResult:
|
| 29 |
if not snapshot.golden_path:
|
| 30 |
return CheckResult(
|
|
|
|
| 53 |
|
| 54 |
expected = step.expect_in_stdout
|
| 55 |
if not expected:
|
| 56 |
+
message = (
|
| 57 |
+
f"golden path step {step.step} has no expect_in_stdout"
|
|
|
|
|
|
|
| 58 |
)
|
| 59 |
+
if self.require_expectation:
|
| 60 |
+
failed_steps.append({
|
| 61 |
+
"step": step.step,
|
| 62 |
+
"error": message,
|
| 63 |
+
})
|
| 64 |
+
else:
|
| 65 |
+
logger.warning(
|
| 66 |
+
"exploitability: %s — output not validated",
|
| 67 |
+
message,
|
| 68 |
+
)
|
| 69 |
+
unvalidated_steps.append(step.step)
|
| 70 |
elif expected not in output:
|
| 71 |
failed_steps.append({
|
| 72 |
"step": step.step,
|
|
|
|
| 89 |
"unvalidated_steps": unvalidated_steps,
|
| 90 |
"issues": issues,
|
| 91 |
"total_steps": len(snapshot.golden_path),
|
| 92 |
+
"require_expectation": self.require_expectation,
|
| 93 |
},
|
| 94 |
error="" if passed else f"{len(failed_steps)} golden-path step(s) failed",
|
| 95 |
)
|
tests/test_validator.py
CHANGED
|
@@ -336,6 +336,42 @@ async def test_exploitability_skips_meta_commands(mock_containers):
|
|
| 336 |
assert result.details["skipped_steps"] == [2]
|
| 337 |
|
| 338 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 339 |
# ---------------------------------------------------------------------------
|
| 340 |
# Check 3: Patchability
|
| 341 |
# ---------------------------------------------------------------------------
|
|
|
|
| 336 |
assert result.details["skipped_steps"] == [2]
|
| 337 |
|
| 338 |
|
| 339 |
+
@pytest.mark.asyncio
|
| 340 |
+
async def test_exploitability_fails_when_expectation_missing_in_strict_mode(mock_containers):
|
| 341 |
+
from open_range.validator.exploitability import ExploitabilityCheck
|
| 342 |
+
|
| 343 |
+
spec = SnapshotSpec(
|
| 344 |
+
golden_path=[
|
| 345 |
+
GoldenPathStep(step=1, command="curl http://web/", expect_in_stdout=""),
|
| 346 |
+
],
|
| 347 |
+
)
|
| 348 |
+
mock_containers.exec_results[("attacker", "curl http://web/")] = "Welcome"
|
| 349 |
+
|
| 350 |
+
result = await ExploitabilityCheck().check(spec, mock_containers)
|
| 351 |
+
assert result.passed is False
|
| 352 |
+
assert result.details["require_expectation"] is True
|
| 353 |
+
assert result.details["failed_steps"][0]["error"] == (
|
| 354 |
+
"golden path step 1 has no expect_in_stdout"
|
| 355 |
+
)
|
| 356 |
+
|
| 357 |
+
|
| 358 |
+
@pytest.mark.asyncio
|
| 359 |
+
async def test_exploitability_allows_missing_expectation_in_lenient_mode(mock_containers):
|
| 360 |
+
from open_range.validator.exploitability import ExploitabilityCheck
|
| 361 |
+
|
| 362 |
+
spec = SnapshotSpec(
|
| 363 |
+
golden_path=[
|
| 364 |
+
GoldenPathStep(step=1, command="curl http://web/", expect_in_stdout=""),
|
| 365 |
+
],
|
| 366 |
+
)
|
| 367 |
+
mock_containers.exec_results[("attacker", "curl http://web/")] = "Welcome"
|
| 368 |
+
|
| 369 |
+
result = await ExploitabilityCheck(require_expectation=False).check(spec, mock_containers)
|
| 370 |
+
assert result.passed is True
|
| 371 |
+
assert result.details["require_expectation"] is False
|
| 372 |
+
assert result.details["unvalidated_steps"] == [1]
|
| 373 |
+
|
| 374 |
+
|
| 375 |
# ---------------------------------------------------------------------------
|
| 376 |
# Check 3: Patchability
|
| 377 |
# ---------------------------------------------------------------------------
|