pivotcheck 2.0.0__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.
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/PKG-INFO +1 -1
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/__init__.py +1 -1
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/analysis/explanation.py +28 -13
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/cli.py +30 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck.egg-info/PKG-INFO +1 -1
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/tests/test_intelligence_cli.py +99 -1
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/tests/test_operator_intelligence.py +29 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/LICENSE +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/README.md +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/__main__.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/analysis/__init__.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/analysis/comparison.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/analysis/correlation.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/analysis/evidence_gaps.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/analysis/gateway.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/analysis/graph.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/analysis/map_view.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/analysis/next_step.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/analysis/opsec.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/analysis/query.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/analysis/recommendation.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/analysis/summary.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/analysis/topology.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/analysis/transit_priority.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/checks/__init__.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/checks/context.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/checks/proxy.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/checks/resolver.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/checks/smb.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/checks/ssh.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/checks/tcp.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/checks/winrm.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/discovery/__init__.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/discovery/connections.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/discovery/dns.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/discovery/engine.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/discovery/interfaces.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/discovery/local.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/discovery/macos.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/discovery/neighbors.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/discovery/provider.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/discovery/remote.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/discovery/routes.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/discovery/ssh.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/discovery/windows.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/models/__init__.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/models/baseline.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/models/check.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/models/correlation.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/models/credentials.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/models/graph.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/models/network.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/models/opsec.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/models/proxy_check.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/models/result.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/models/session.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/models/smb_check.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/models/ssh_check.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/models/winrm_check.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/output/__init__.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/output/artifact.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/output/check_output.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/output/comparison.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/output/evidence_gaps.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/output/intelligence.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/output/json.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/output/map_view.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/output/next_step.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/output/opsec.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/output/proxy_check.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/output/smb_check.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/output/ssh_check.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/output/terminal.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/output/winrm_check.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/output/writer.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/storage/__init__.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/storage/baseline_store.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/utils/__init__.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/utils/credential_loader.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/utils/redaction.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/utils/system.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck/utils/validation.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck.egg-info/SOURCES.txt +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck.egg-info/dependency_links.txt +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck.egg-info/entry_points.txt +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck.egg-info/requires.txt +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pivotcheck.egg-info/top_level.txt +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/pyproject.toml +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/setup.cfg +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/tests/test_baseline_store.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/tests/test_baseline_workflow.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/tests/test_check_cli.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/tests/test_cli.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/tests/test_cli_contract.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/tests/test_comparison.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/tests/test_context.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/tests/test_correlation.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/tests/test_credentials.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/tests/test_determinism.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/tests/test_gateway.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/tests/test_graph.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/tests/test_intelligence_pure.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/tests/test_interfaces.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/tests/test_json_schema_audit.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/tests/test_map_view.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/tests/test_neighbors_dns.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/tests/test_network_safety.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/tests/test_next_step.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/tests/test_next_step_cli.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/tests/test_opsec.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/tests/test_output_encoding.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/tests/test_platform_collectors.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/tests/test_proxy_check.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/tests/test_proxy_check_cli.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/tests/test_proxy_protocol.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/tests/test_remote_session.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/tests/test_resolver.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/tests/test_routes.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/tests/test_session_provider.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/tests/test_side_effects.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/tests/test_smb_check.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/tests/test_smb_check_cli.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/tests/test_ssh_check.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/tests/test_ssh_check_cli.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/tests/test_ssh_check_integration.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/tests/test_ssh_cli.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/tests/test_ssh_provider.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/tests/test_tcp.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/tests/test_terminal.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/tests/test_topology.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/tests/test_transit_priority.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/tests/test_validation_context.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/tests/test_validation_context_cli.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/tests/test_winrm_check.py +0 -0
- {pivotcheck-2.0.0 → pivotcheck-2.0.2}/tests/test_winrm_check_cli.py +0 -0
|
@@ -47,9 +47,11 @@ def _finding(network: str, report: DiffReport) -> DiffFinding | None:
|
|
|
47
47
|
return None
|
|
48
48
|
|
|
49
49
|
|
|
50
|
-
def _reason(finding: DiffFinding | None) -> str:
|
|
50
|
+
def _reason(finding: DiffFinding | None, observed: bool = True) -> str:
|
|
51
51
|
if finding is None:
|
|
52
|
-
|
|
52
|
+
if observed:
|
|
53
|
+
return "Network observed in current discovery evidence."
|
|
54
|
+
return "Network not found in current discovery evidence."
|
|
53
55
|
reasons = {
|
|
54
56
|
"NEW_REACHABILITY": "Newly observed network not present in baseline.",
|
|
55
57
|
"EXPANDED_REACHABILITY": "Network coverage expanded relative to baseline.",
|
|
@@ -73,21 +75,34 @@ def explain_network(
|
|
|
73
75
|
If not, provides standalone explanation from current evidence only.
|
|
74
76
|
"""
|
|
75
77
|
canonical = str(ipaddress.ip_network(network, strict=False))
|
|
76
|
-
|
|
77
|
-
if report is not None:
|
|
78
|
-
finding = _finding(canonical, report)
|
|
79
|
-
classification = finding.classification if finding else "CURRENT_EVIDENCE"
|
|
80
|
-
reason = _reason(finding)
|
|
81
|
-
else:
|
|
82
|
-
# Standalone mode: no comparison context
|
|
83
|
-
classification = "CURRENT_EVIDENCE"
|
|
84
|
-
reason = "Network observed in current discovery evidence."
|
|
85
|
-
|
|
78
|
+
|
|
86
79
|
evidence = next(
|
|
87
|
-
(item for item in current.networks
|
|
80
|
+
(item for item in current.networks
|
|
88
81
|
if str(ipaddress.ip_network(item.cidr, strict=False)) == canonical),
|
|
89
82
|
None
|
|
90
83
|
)
|
|
84
|
+
observed = evidence is not None
|
|
85
|
+
|
|
86
|
+
if report is not None:
|
|
87
|
+
finding = _finding(canonical, report)
|
|
88
|
+
if finding is not None:
|
|
89
|
+
classification = finding.classification
|
|
90
|
+
reason = _reason(finding)
|
|
91
|
+
else:
|
|
92
|
+
# Comparison context covers a different scope than the current
|
|
93
|
+
# snapshot: classification follows what the CURRENT evidence
|
|
94
|
+
# actually shows, never an assumed observation.
|
|
95
|
+
classification = "CURRENT_EVIDENCE" if observed else "NOT_OBSERVED"
|
|
96
|
+
reason = _reason(None, observed)
|
|
97
|
+
else:
|
|
98
|
+
# Standalone mode: no comparison context.
|
|
99
|
+
if observed:
|
|
100
|
+
classification = "CURRENT_EVIDENCE"
|
|
101
|
+
reason = _reason(None, observed)
|
|
102
|
+
else:
|
|
103
|
+
classification = "NOT_OBSERVED"
|
|
104
|
+
reason = _reason(None, observed)
|
|
105
|
+
|
|
91
106
|
routes = _route_evidence(canonical, current)
|
|
92
107
|
|
|
93
108
|
# Get transit evidence if available
|
|
@@ -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
|
|
@@ -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."
|
|
@@ -63,3 +63,32 @@ def test_recommendations_are_deterministic_when_input_order_changes():
|
|
|
63
63
|
first = DiscoverySnapshot("h", "o", networks=(DiscoveredNetwork("192.168.0.0/24", NetworkOrigin.CONNECTED, Confidence.HIGH), DiscoveredNetwork("172.16.0.0/24", NetworkOrigin.CONNECTED, Confidence.HIGH)))
|
|
64
64
|
second = DiscoverySnapshot("h", "o", networks=tuple(reversed(first.networks)))
|
|
65
65
|
assert recommend(first, report) == recommend(second, report)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_explain_unobserved_network_is_not_reported_as_observed():
|
|
69
|
+
"""Stage 9 regression: explain() on a network absent from the current
|
|
70
|
+
snapshot must be classified NOT_OBSERVED - never CURRENT_EVIDENCE."""
|
|
71
|
+
snapshot = DiscoverySnapshot("host", "OS", networks=(
|
|
72
|
+
DiscoveredNetwork("172.16.0.0/24", NetworkOrigin.CONNECTED, Confidence.HIGH, "eth0"),
|
|
73
|
+
))
|
|
74
|
+
standalone = explain_network("10.9.9.0/24", snapshot)
|
|
75
|
+
assert standalone.classification == "NOT_OBSERVED"
|
|
76
|
+
assert "not found in current discovery evidence" in standalone.reason.lower()
|
|
77
|
+
assert standalone.origin is None and standalone.interface is None
|
|
78
|
+
|
|
79
|
+
with_report = explain_network(
|
|
80
|
+
"10.9.9.0/24", snapshot,
|
|
81
|
+
compare(perspective(BaselineNetwork("172.16.0.0/24", NetworkOrigin.CONNECTED, Confidence.HIGH)), perspective()),
|
|
82
|
+
)
|
|
83
|
+
assert with_report.classification == "NOT_OBSERVED"
|
|
84
|
+
assert "not found in current discovery evidence" in with_report.reason.lower()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_explain_observed_network_still_current_evidence():
|
|
88
|
+
snapshot = DiscoverySnapshot("host", "OS", networks=(
|
|
89
|
+
DiscoveredNetwork("172.16.0.0/24", NetworkOrigin.CONNECTED, Confidence.HIGH, "eth0"),
|
|
90
|
+
))
|
|
91
|
+
explanation = explain_network("172.16.0.0/24", snapshot)
|
|
92
|
+
assert explanation.classification == "CURRENT_EVIDENCE"
|
|
93
|
+
assert "observed in current discovery evidence" in explanation.reason.lower()
|
|
94
|
+
assert explanation.origin == "connected"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|