Lars Talian commited on
Commit
6f0f018
·
unverified ·
1 Parent(s): c75d1f7

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
- logger.warning(
47
- "exploitability: golden path step %d has no expect_in_stdout"
48
- "output not validated",
49
- step.step,
50
  )
51
- unvalidated_steps.append(step.step)
 
 
 
 
 
 
 
 
 
 
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
  # ---------------------------------------------------------------------------