pivotcheck 2.0.1__tar.gz → 2.0.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/PKG-INFO +1 -1
  2. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/__init__.py +1 -1
  3. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/cli.py +30 -0
  4. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck.egg-info/PKG-INFO +1 -1
  5. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/tests/test_intelligence_cli.py +99 -1
  6. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/LICENSE +0 -0
  7. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/README.md +0 -0
  8. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/__main__.py +0 -0
  9. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/analysis/__init__.py +0 -0
  10. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/analysis/comparison.py +0 -0
  11. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/analysis/correlation.py +0 -0
  12. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/analysis/evidence_gaps.py +0 -0
  13. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/analysis/explanation.py +0 -0
  14. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/analysis/gateway.py +0 -0
  15. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/analysis/graph.py +0 -0
  16. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/analysis/map_view.py +0 -0
  17. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/analysis/next_step.py +0 -0
  18. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/analysis/opsec.py +0 -0
  19. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/analysis/query.py +0 -0
  20. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/analysis/recommendation.py +0 -0
  21. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/analysis/summary.py +0 -0
  22. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/analysis/topology.py +0 -0
  23. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/analysis/transit_priority.py +0 -0
  24. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/checks/__init__.py +0 -0
  25. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/checks/context.py +0 -0
  26. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/checks/proxy.py +0 -0
  27. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/checks/resolver.py +0 -0
  28. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/checks/smb.py +0 -0
  29. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/checks/ssh.py +0 -0
  30. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/checks/tcp.py +0 -0
  31. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/checks/winrm.py +0 -0
  32. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/discovery/__init__.py +0 -0
  33. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/discovery/connections.py +0 -0
  34. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/discovery/dns.py +0 -0
  35. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/discovery/engine.py +0 -0
  36. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/discovery/interfaces.py +0 -0
  37. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/discovery/local.py +0 -0
  38. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/discovery/macos.py +0 -0
  39. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/discovery/neighbors.py +0 -0
  40. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/discovery/provider.py +0 -0
  41. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/discovery/remote.py +0 -0
  42. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/discovery/routes.py +0 -0
  43. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/discovery/ssh.py +0 -0
  44. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/discovery/windows.py +0 -0
  45. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/models/__init__.py +0 -0
  46. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/models/baseline.py +0 -0
  47. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/models/check.py +0 -0
  48. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/models/correlation.py +0 -0
  49. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/models/credentials.py +0 -0
  50. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/models/graph.py +0 -0
  51. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/models/network.py +0 -0
  52. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/models/opsec.py +0 -0
  53. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/models/proxy_check.py +0 -0
  54. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/models/result.py +0 -0
  55. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/models/session.py +0 -0
  56. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/models/smb_check.py +0 -0
  57. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/models/ssh_check.py +0 -0
  58. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/models/winrm_check.py +0 -0
  59. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/output/__init__.py +0 -0
  60. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/output/artifact.py +0 -0
  61. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/output/check_output.py +0 -0
  62. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/output/comparison.py +0 -0
  63. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/output/evidence_gaps.py +0 -0
  64. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/output/intelligence.py +0 -0
  65. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/output/json.py +0 -0
  66. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/output/map_view.py +0 -0
  67. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/output/next_step.py +0 -0
  68. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/output/opsec.py +0 -0
  69. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/output/proxy_check.py +0 -0
  70. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/output/smb_check.py +0 -0
  71. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/output/ssh_check.py +0 -0
  72. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/output/terminal.py +0 -0
  73. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/output/winrm_check.py +0 -0
  74. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/output/writer.py +0 -0
  75. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/storage/__init__.py +0 -0
  76. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/storage/baseline_store.py +0 -0
  77. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/utils/__init__.py +0 -0
  78. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/utils/credential_loader.py +0 -0
  79. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/utils/redaction.py +0 -0
  80. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/utils/system.py +0 -0
  81. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck/utils/validation.py +0 -0
  82. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck.egg-info/SOURCES.txt +0 -0
  83. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck.egg-info/dependency_links.txt +0 -0
  84. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck.egg-info/entry_points.txt +0 -0
  85. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck.egg-info/requires.txt +0 -0
  86. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pivotcheck.egg-info/top_level.txt +0 -0
  87. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/pyproject.toml +0 -0
  88. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/setup.cfg +0 -0
  89. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/tests/test_baseline_store.py +0 -0
  90. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/tests/test_baseline_workflow.py +0 -0
  91. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/tests/test_check_cli.py +0 -0
  92. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/tests/test_cli.py +0 -0
  93. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/tests/test_cli_contract.py +0 -0
  94. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/tests/test_comparison.py +0 -0
  95. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/tests/test_context.py +0 -0
  96. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/tests/test_correlation.py +0 -0
  97. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/tests/test_credentials.py +0 -0
  98. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/tests/test_determinism.py +0 -0
  99. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/tests/test_gateway.py +0 -0
  100. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/tests/test_graph.py +0 -0
  101. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/tests/test_intelligence_pure.py +0 -0
  102. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/tests/test_interfaces.py +0 -0
  103. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/tests/test_json_schema_audit.py +0 -0
  104. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/tests/test_map_view.py +0 -0
  105. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/tests/test_neighbors_dns.py +0 -0
  106. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/tests/test_network_safety.py +0 -0
  107. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/tests/test_next_step.py +0 -0
  108. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/tests/test_next_step_cli.py +0 -0
  109. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/tests/test_operator_intelligence.py +0 -0
  110. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/tests/test_opsec.py +0 -0
  111. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/tests/test_output_encoding.py +0 -0
  112. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/tests/test_platform_collectors.py +0 -0
  113. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/tests/test_proxy_check.py +0 -0
  114. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/tests/test_proxy_check_cli.py +0 -0
  115. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/tests/test_proxy_protocol.py +0 -0
  116. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/tests/test_remote_session.py +0 -0
  117. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/tests/test_resolver.py +0 -0
  118. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/tests/test_routes.py +0 -0
  119. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/tests/test_session_provider.py +0 -0
  120. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/tests/test_side_effects.py +0 -0
  121. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/tests/test_smb_check.py +0 -0
  122. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/tests/test_smb_check_cli.py +0 -0
  123. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/tests/test_ssh_check.py +0 -0
  124. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/tests/test_ssh_check_cli.py +0 -0
  125. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/tests/test_ssh_check_integration.py +0 -0
  126. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/tests/test_ssh_cli.py +0 -0
  127. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/tests/test_ssh_provider.py +0 -0
  128. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/tests/test_tcp.py +0 -0
  129. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/tests/test_terminal.py +0 -0
  130. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/tests/test_topology.py +0 -0
  131. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/tests/test_transit_priority.py +0 -0
  132. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/tests/test_validation_context.py +0 -0
  133. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/tests/test_validation_context_cli.py +0 -0
  134. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/tests/test_winrm_check.py +0 -0
  135. {pivotcheck-2.0.1 → pivotcheck-2.0.2}/tests/test_winrm_check_cli.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pivotcheck
3
- Version: 2.0.1
3
+ Version: 2.0.2
4
4
  Summary: Passive network discovery and pivot path validation for authorized security assessments
5
5
  Author: PivotCheck contributors
6
6
  License: GPL-3.0-only
@@ -3,4 +3,4 @@
3
3
  Copyright (c) 2026 PivotCheck contributors. GPL-3.0-only licensed.
4
4
  """
5
5
 
6
- __version__ = "2.0.1"
6
+ __version__ = "2.0.2"
@@ -32,6 +32,7 @@ from __future__ import annotations
32
32
 
33
33
  import argparse
34
34
  import getpass
35
+ import ipaddress
35
36
  import os
36
37
  import re
37
38
  import sys
@@ -1674,8 +1675,33 @@ def _run_next(args: argparse.Namespace) -> int:
1674
1675
  return EXIT_OK
1675
1676
 
1676
1677
 
1678
+ def _validate_network_argument(network: str) -> int | None:
1679
+ """Validate a network CLI argument against the canonical CIDR model.
1680
+
1681
+ Returns ``None`` when the argument is valid, otherwise ``EXIT_USAGE``
1682
+ after printing the standard operator error. Validation is pure: invalid
1683
+ input fails before discovery (no filesystem, network, or subprocess
1684
+ side effects), and the original argument is passed through to analysis
1685
+ unchanged, so valid-path behavior is byte-identical.
1686
+ """
1687
+ try:
1688
+ ipaddress.ip_network(network, strict=False)
1689
+ except ValueError:
1690
+ print(f"[-] Invalid network argument: {network!r}.", file=sys.stderr)
1691
+ print(
1692
+ " Provide a CIDR network (e.g. 10.50.0.0/16) or an IP address.",
1693
+ file=sys.stderr,
1694
+ )
1695
+ return EXIT_USAGE
1696
+ return None
1697
+
1698
+
1677
1699
  def _run_gaps(args: argparse.Namespace) -> int:
1678
1700
  """Execute evidence gap analysis."""
1701
+ error = _validate_network_argument(args.network)
1702
+ if error is not None:
1703
+ return error
1704
+
1679
1705
  # Run discovery once
1680
1706
  try:
1681
1707
  snapshot = run_discovery()
@@ -1700,6 +1726,10 @@ def _run_gaps(args: argparse.Namespace) -> int:
1700
1726
 
1701
1727
  def _run_explain(args: argparse.Namespace) -> int:
1702
1728
  """Execute standalone network explanation."""
1729
+ error = _validate_network_argument(args.network)
1730
+ if error is not None:
1731
+ return error
1732
+
1703
1733
  # If the operator explicitly requested contextual baseline analysis,
1704
1734
  # load the baseline BEFORE any discovery or socket activity. A missing
1705
1735
  # or invalid baseline fails explicitly rather than silently performing
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pivotcheck
3
- Version: 2.0.1
3
+ Version: 2.0.2
4
4
  Summary: Passive network discovery and pivot path validation for authorized security assessments
5
5
  Author: PivotCheck contributors
6
6
  License: GPL-3.0-only
@@ -267,4 +267,102 @@ class TestDiscoverSummary:
267
267
  out = capsys.readouterr().out
268
268
  assert "Connected Coverage: 1" in out
269
269
  assert "Routed Coverage: 2" in out
270
- assert "Inferred Pivot Paths: 1" in out
270
+ assert "Inferred Pivot Paths: 1" in out
271
+
272
+
273
+ class TestNetworkArgumentValidation:
274
+ """Stage 9 DEFECT-002 regression: invalid network arguments must follow
275
+ the established CLI error contract (clean `[-]` message on stderr, no
276
+ Python traceback, EXIT_USAGE, no partial output, no side effects).
277
+
278
+ These tests fail against the defective 2.0.0 implementation, which
279
+ leaked an uncaught ValueError traceback from models/network.py and
280
+ exited 1.
281
+ """
282
+
283
+ BAD_NETWORKS = (
284
+ "not-a-cidr",
285
+ "999.999.1.0/24", # malformed IPv4 octets
286
+ "10.0.0.0/33", # invalid IPv4 prefix length
287
+ "10.0.0.0/120", # IPv6 prefix length applied to IPv4
288
+ "10.0.0.0/24/24", # double prefix
289
+ "2001:db8::/wxyz", # malformed IPv6 prefix
290
+ )
291
+
292
+ @pytest.fixture(autouse=True)
293
+ def fake_discovery(self, monkeypatch):
294
+ from pivotcheck import cli
295
+
296
+ monkeypatch.setattr(cli, "run_discovery", make_snapshot)
297
+
298
+ @pytest.mark.parametrize("bad", BAD_NETWORKS)
299
+ def test_gaps_invalid_network_is_usage_error(self, bad, capsys):
300
+ assert _run(["gaps", bad]) == EXIT_USAGE
301
+ err = capsys.readouterr().err
302
+ assert "Invalid network argument" in err
303
+ assert "Traceback" not in err
304
+ assert "ValueError" not in err
305
+
306
+ @pytest.mark.parametrize("bad", BAD_NETWORKS)
307
+ def test_explain_invalid_network_is_usage_error(self, bad, capsys):
308
+ assert _run(["explain", bad]) == EXIT_USAGE
309
+ err = capsys.readouterr().err
310
+ assert "Invalid network argument" in err
311
+ assert "Traceback" not in err
312
+
313
+ @pytest.mark.parametrize("bad", BAD_NETWORKS)
314
+ def test_invalid_network_json_path_has_no_partial_result(self, bad, capsys):
315
+ for command in ("gaps", "explain"):
316
+ assert _run([command, bad, "--json"]) == EXIT_USAGE
317
+ captured = capsys.readouterr()
318
+ # No partial JSON artifact may pretend the request succeeded.
319
+ assert captured.out.strip() == ""
320
+ assert "Traceback" not in captured.err
321
+
322
+ def test_invalid_network_fails_before_discovery(self, monkeypatch, capsys):
323
+ """Invalid input must produce zero side effects: discovery (and any
324
+ I/O behind it) must never run for an invalid network argument."""
325
+ from pivotcheck import cli
326
+
327
+ def _must_not_run(*_args, **_kwargs):
328
+ raise AssertionError("discovery must not run for invalid input")
329
+
330
+ monkeypatch.setattr(cli, "run_discovery", _must_not_run)
331
+ assert _run(["gaps", "not-a-cidr"]) == EXIT_USAGE
332
+ assert "Invalid network argument" in capsys.readouterr().err
333
+
334
+ def test_valid_cidr_behavior_unchanged(self, capsys):
335
+ assert _run(["gaps", "10.50.0.0/16"]) == EXIT_OK
336
+ out = capsys.readouterr().out
337
+ assert "10.50.0.0/16" in out
338
+ assert "EVIDENCE GAP" in out
339
+
340
+ def test_bare_ip_still_valid(self, capsys):
341
+ assert _run(["explain", "10.50.5.1"]) == EXIT_OK
342
+ assert "10.50.0.0/16" in capsys.readouterr().out
343
+
344
+ def test_host_bits_set_still_accepted(self, capsys):
345
+ # strict=False semantics are preserved: host bits are normalized,
346
+ # exactly as in 2.0.0.
347
+ assert _run(["gaps", "10.50.5.1/16"]) == EXIT_OK
348
+
349
+ def test_ipv6_cidr_still_valid(self, capsys):
350
+ assert _run(["explain", "2001:db8:a::/48"]) == EXIT_OK
351
+ out = capsys.readouterr().out
352
+ assert "2001:db8:a::/48" in out
353
+ assert "CURRENT_EVIDENCE" in out
354
+
355
+ def test_explain_unobserved_network_is_not_observed(self, capsys):
356
+ """Stage 9 DEFECT-001 regression at the CLI boundary: absence of
357
+ observation must never be promoted to CURRENT_EVIDENCE. Fails
358
+ against 2.0.0, which reported CURRENT_EVIDENCE here."""
359
+ assert _run(["explain", "192.168.99.0/24"]) == EXIT_OK
360
+ out = capsys.readouterr().out
361
+ assert "NOT_OBSERVED" in out
362
+ assert "CURRENT_EVIDENCE" not in out
363
+
364
+ def test_explain_unobserved_network_json_classification(self, capsys):
365
+ assert _run(["explain", "192.168.99.0/24", "--json"]) == EXIT_OK
366
+ payload = json.loads(capsys.readouterr().out)
367
+ assert payload["classification"] == "NOT_OBSERVED"
368
+ assert payload["reason"] == "Network not found in current discovery evidence."
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes