vlan-probe 0.1.0__py3-none-any.whl

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.
vlan_probe/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ """
2
+ VLAN Isolation & Network Permission Probe Tool.
3
+
4
+ Probes target VLAN subnets, IPs, and ports from the host environment to verify
5
+ isolation policies and detect unauthorized inter-VLAN access violations.
6
+ """
7
+
8
+ __version__ = "0.1.0"
9
+ __all__ = ["probe_target", "get_local_ips"]
10
+
11
+ from .probe import get_local_ips, probe_target
vlan_probe/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Module entrypoint for vlan_probe package."""
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
vlan_probe/cli.py ADDED
@@ -0,0 +1,159 @@
1
+ """CLI interface for VLAN probe tool."""
2
+
3
+ import argparse
4
+ import datetime
5
+ import json
6
+ import os
7
+ import sys
8
+
9
+ from .config import DEFAULT_CONFIG_PATH, load_config
10
+ from .probe import get_local_ips, probe_target
11
+
12
+ # ANSI color codes for interactive (TTY) output.
13
+ _COLORS = {
14
+ "reset": "\033[0m",
15
+ "bold": "\033[1m",
16
+ "red": "\033[31m",
17
+ "green": "\033[32m",
18
+ "yellow": "\033[33m",
19
+ "cyan": "\033[36m",
20
+ }
21
+
22
+
23
+ def colorize(text: str, color: str) -> str:
24
+ """Apply ANSI color to text if color is enabled."""
25
+ if not color:
26
+ return text
27
+ return f"{_COLORS[color]}{text}{_COLORS['reset']}"
28
+
29
+
30
+ def may_colorize() -> bool:
31
+ """Check if output should be colorized."""
32
+ return sys.stdout.isatty() and not os.environ.get("NO_COLOR")
33
+
34
+
35
+ def resolve_color_mode(mode: str) -> bool:
36
+ """Resolve color mode setting to boolean."""
37
+ if mode == "always":
38
+ return True
39
+ if mode == "never":
40
+ return False
41
+ return may_colorize()
42
+
43
+
44
+ def colorize_json_statuses(line: str, color: bool) -> str:
45
+ """Colorize JSON status values in output."""
46
+ if not color:
47
+ return line
48
+ line = line.replace('"status": "PASS"', f'"status": "{colorize("PASS", "green")}"')
49
+ line = line.replace('"status": "FAIL"', f'"status": "{colorize("FAIL", "red")}"')
50
+ return line
51
+
52
+
53
+ def main() -> None:
54
+ """Main CLI entry point."""
55
+ parser = argparse.ArgumentParser(description="Probe VLAN network access and verify isolation permissions.")
56
+ parser.add_argument("-c", "--config", default=DEFAULT_CONFIG_PATH, help="Path to config JSON file")
57
+ parser.add_argument(
58
+ "-f",
59
+ "--format",
60
+ choices=["ndjson", "json", "table"],
61
+ default="ndjson",
62
+ help="Output format (default: ndjson).",
63
+ )
64
+ parser.add_argument(
65
+ "-t",
66
+ "--timeout",
67
+ type=float,
68
+ default=2.0,
69
+ help="Socket connection timeout in seconds",
70
+ )
71
+ parser.add_argument(
72
+ "-s",
73
+ "--strict",
74
+ action="store_true",
75
+ help="Exit with code 1 if any access violation / test failure occurs",
76
+ )
77
+ parser.add_argument(
78
+ "--color",
79
+ choices=["auto", "always", "never"],
80
+ default="auto",
81
+ help="Colorize table output. auto = on when stdout is a TTY",
82
+ )
83
+ args = parser.parse_args()
84
+ color = resolve_color_mode(args.color)
85
+
86
+ targets = load_config(args.config)
87
+
88
+ results = []
89
+ violations = []
90
+ local_ips = get_local_ips()
91
+
92
+ for target in targets:
93
+ res = probe_target(target, timeout=args.timeout, local_ips=local_ips)
94
+ results.append(res)
95
+ if res["status"] == "FAIL":
96
+ violations.append(res)
97
+
98
+ if args.format == "ndjson":
99
+ print(colorize_json_statuses(json.dumps(res), color))
100
+
101
+ if args.format == "json":
102
+ summary = {
103
+ "timestamp": datetime.datetime.now(datetime.timezone.utc).isoformat(),
104
+ "total_probed": len(results),
105
+ "passed": len(results) - len(violations),
106
+ "failed": len(violations),
107
+ "violations": [
108
+ {
109
+ "vlan": v["target_vlan"],
110
+ "target": v["target_name"],
111
+ "ip": v["target_ip"],
112
+ "port": v["port"],
113
+ "error": v["error"],
114
+ }
115
+ for v in violations
116
+ ],
117
+ "results": results,
118
+ }
119
+ print(colorize_json_statuses(json.dumps(summary, indent=2), color))
120
+
121
+ elif args.format == "table":
122
+ if color:
123
+ header = (
124
+ f"{colorize('VLAN', 'cyan'):<12} "
125
+ f"{colorize('TARGET', 'cyan'):<30} "
126
+ f"{colorize('ENDPOINT', 'cyan'):<22} "
127
+ f"{colorize('STATUS', 'cyan'):<8} "
128
+ f"{colorize('DETAILS', 'cyan')}"
129
+ )
130
+ divider = colorize("-" * 90, "cyan")
131
+ else:
132
+ header = f"{'VLAN':<12} {'TARGET':<30} {'ENDPOINT':<22} {'STATUS':<8} {'DETAILS'}"
133
+ divider = "-" * 90
134
+ print(header)
135
+ print(divider)
136
+ for r in results:
137
+ endpoint = f"{r['target_ip']}:{r['port']} ({r['protocol']})"
138
+ details = r["error"] if r["error"] else "OK"
139
+ status = r["status"]
140
+ if color:
141
+ status_color = "red" if status == "FAIL" else "green"
142
+ details_color = "red" if status == "FAIL" else "green"
143
+ status = colorize(str(status), status_color)
144
+ details = colorize(str(details), details_color)
145
+ print(f"{r['target_vlan']:<12} {r['target_name']:<30} {endpoint:<22} {status:<8} {details}")
146
+
147
+ if args.strict and violations:
148
+ head = f"{len(violations)} unauthorized connection(s) detected!"
149
+ if color:
150
+ head = colorize(colorize("VLAN ISOLATION VIOLATION FAILURE:", "bold") + " " + head, "red")
151
+ sys.stderr.write(f"\n🚨 {head}\n")
152
+ for v in violations:
153
+ line = f" - {v['error']}"
154
+ sys.stderr.write((colorize(line, "red") if color else line) + "\n")
155
+ sys.exit(1)
156
+
157
+
158
+ if __name__ == "__main__":
159
+ main()
vlan_probe/config.py ADDED
@@ -0,0 +1,64 @@
1
+ """Configuration loading for vlan_probe."""
2
+
3
+ import sys
4
+ from typing import Any, Dict, List
5
+
6
+ try:
7
+ import tomllib # Python 3.11+
8
+ except ImportError:
9
+ try:
10
+ import tomli as tomllib # Fallback for Python < 3.11
11
+ except ImportError:
12
+ tomllib = None
13
+
14
+ DEFAULT_CONFIG_PATH = "/etc/vlan_probe.toml"
15
+
16
+
17
+ def load_config(config_path: str) -> List[Dict[str, Any]]:
18
+ """
19
+ Load and parse configuration file.
20
+
21
+ Supports TOML and JSON formats based on file extension.
22
+
23
+ Args:
24
+ config_path: Path to configuration file
25
+
26
+ Returns:
27
+ List of target dictionaries
28
+
29
+ Raises:
30
+ SystemExit: On configuration loading or parsing errors
31
+ """
32
+ try:
33
+ if config_path.endswith(".toml"):
34
+ if tomllib is None:
35
+ sys.stderr.write(
36
+ "Error: TOML support requires 'tomli' package for Python < 3.11. Install with: pip install tomli\n"
37
+ )
38
+ sys.exit(2)
39
+ with open(config_path, "rb") as f:
40
+ config_data = tomllib.load(f)
41
+ else:
42
+ # Fallback to JSON for other extensions
43
+ import json
44
+
45
+ with open(config_path, "r", encoding="utf-8") as f:
46
+ config_data = json.load(f)
47
+ except FileNotFoundError:
48
+ sys.stderr.write(f"Error: Config file not found: {config_path}\n")
49
+ sys.exit(2)
50
+ except Exception as e:
51
+ sys.stderr.write(f"Error loading config file '{config_path}': {e}\n")
52
+ sys.exit(2)
53
+
54
+ # Extract targets from config (support nested structure)
55
+ if isinstance(config_data, dict):
56
+ targets = config_data.get("targets", [])
57
+ else:
58
+ targets = config_data if isinstance(config_data, list) else []
59
+
60
+ if not isinstance(targets, list):
61
+ sys.stderr.write("Error: 'targets' in config must be a list\n")
62
+ sys.exit(2)
63
+
64
+ return targets
vlan_probe/probe.py ADDED
@@ -0,0 +1,125 @@
1
+ """Core VLAN probe functionality."""
2
+
3
+ import datetime
4
+ import socket
5
+ import subprocess
6
+ import time
7
+ from typing import Dict, Optional, Set
8
+
9
+ DEFAULT_TIMEOUT = 2.0
10
+
11
+
12
+ def get_local_ips() -> Set[str]:
13
+ """Get all local IP addresses on this host."""
14
+ ips: Set[str] = {"127.0.0.1"}
15
+ try:
16
+ out = subprocess.check_output(["ip", "-o", "-4", "addr", "show"], text=True, timeout=2)
17
+ for line in out.splitlines():
18
+ parts = line.split()
19
+ if len(parts) >= 4:
20
+ ip = parts[3].split("/")[0]
21
+ ips.add(ip)
22
+ except Exception:
23
+ try:
24
+ hostname = socket.gethostname()
25
+ for ip in socket.gethostbyname_ex(hostname)[2]:
26
+ ips.add(ip)
27
+ except Exception:
28
+ pass
29
+ return ips
30
+
31
+
32
+ def probe_target(
33
+ target: Dict[str, object], timeout: float = DEFAULT_TIMEOUT, local_ips: Optional[Set[str]] = None
34
+ ) -> Dict[str, object]:
35
+ """
36
+ Probe a single target to verify VLAN access permissions.
37
+
38
+ Args:
39
+ target: Dict with keys: name, vlan, ip, port, protocol, expected_blocked
40
+ timeout: Socket timeout in seconds
41
+ local_ips: Set of local IPs (auto-detected if None)
42
+
43
+ Returns:
44
+ Dict with probe result including status, latency, and error details
45
+ """
46
+ if local_ips is None:
47
+ local_ips = get_local_ips()
48
+
49
+ name = str(target.get("name", "Unknown Target"))
50
+ vlan = str(target.get("vlan", "Unknown VLAN"))
51
+ ip = str(target.get("ip"))
52
+ port_raw = target.get("port", 80)
53
+ port = int(port_raw) if isinstance(port_raw, (int, str)) else 80
54
+ protocol = str(target.get("protocol", "tcp")).lower()
55
+ expected_blocked = bool(target.get("expected_blocked", True))
56
+
57
+ is_self = ip in local_ips
58
+
59
+ start_time = time.time()
60
+ reachable = False
61
+ error_msg: Optional[str] = None
62
+
63
+ if protocol == "tcp":
64
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
65
+ sock.settimeout(timeout)
66
+ try:
67
+ sock.connect((ip, port))
68
+ reachable = True
69
+ sock.close()
70
+ except (socket.timeout, ConnectionRefusedError, OSError):
71
+ reachable = False
72
+ elif protocol == "udp":
73
+ sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
74
+ sock.settimeout(timeout)
75
+ try:
76
+ if port == 53:
77
+ dns_query = b"\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x07example\x03com\x00\x00\x01\x00\x01"
78
+ sock.sendto(dns_query, (ip, port))
79
+ else:
80
+ sock.sendto(b"\x00", (ip, port))
81
+ sock.recvfrom(1024)
82
+ reachable = True
83
+ except socket.timeout:
84
+ reachable = False
85
+ except Exception:
86
+ reachable = False
87
+ finally:
88
+ sock.close()
89
+
90
+ latency_ms = round((time.time() - start_time) * 1000, 2)
91
+
92
+ if is_self:
93
+ passed = True
94
+ error_details: Optional[str] = (
95
+ f"EXEMPT_SELF_HOST: Target {ip}:{port} ({name}) is the local interface of the probing host"
96
+ )
97
+ elif expected_blocked:
98
+ passed = not reachable
99
+ if reachable:
100
+ error_details = (
101
+ f"UNAUTHORIZED_CONNECTIVITY_VIOLATION: Host can connect outside to "
102
+ f"restricted VLAN '{vlan}' at {ip}:{port} ({name})"
103
+ )
104
+ else:
105
+ error_details = None
106
+ else:
107
+ passed = reachable
108
+ if not passed:
109
+ error_details = f"EXPECTED_CONNECTIVITY_FAILED: Failed to connect to {name} ({ip}:{port})"
110
+ else:
111
+ error_details = None
112
+
113
+ return {
114
+ "timestamp": datetime.datetime.now(datetime.timezone.utc).isoformat(),
115
+ "target_name": name,
116
+ "target_vlan": vlan,
117
+ "target_ip": ip,
118
+ "port": port,
119
+ "protocol": protocol,
120
+ "reachable": reachable,
121
+ "expected_blocked": expected_blocked,
122
+ "status": "PASS" if passed else "FAIL",
123
+ "latency_ms": latency_ms,
124
+ "error": error_details or error_msg,
125
+ }
@@ -0,0 +1,142 @@
1
+ Metadata-Version: 2.5
2
+ Name: vlan-probe
3
+ Version: 0.1.0
4
+ Summary: VLAN Isolation & Network Permission Probe Tool
5
+ Project-URL: Homepage, https://github.com/hellqvio86/vlan-probe
6
+ Project-URL: Repository, https://github.com/hellqvio86/vlan-probe
7
+ Author: hellqvio
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Requires-Python: >=3.10
15
+ Requires-Dist: tomli>=1.2.0; python_version < '3.11'
16
+ Description-Content-Type: text/markdown
17
+
18
+ # vlan-probe
19
+
20
+ VLAN Isolation & Network Permission Probe Tool — standalone project extracted
21
+ from the Ansible role in ansible-home-baseline.
22
+
23
+ `vlan-probe` is a command-line tool. Install it with [pipx](https://pipx.pypa.io/)
24
+ so it runs in an isolated environment and the `vlan-probe` command is available
25
+ on your `PATH`.
26
+
27
+ ## Requirements
28
+
29
+ - Python 3.10 or newer
30
+
31
+ ## Installation
32
+
33
+ ### Install with pipx (recommended)
34
+
35
+ ```bash
36
+ pipx install vlan-probe
37
+ ```
38
+
39
+ Upgrade to a newer release:
40
+
41
+ ```bash
42
+ pipx upgrade vlan-probe
43
+ ```
44
+
45
+ If you do not already have pipx, install it first (macOS/Homebrew, Debian/Ubuntu
46
+ or Arch examples; see the [pipx docs](https://pipx.pypa.io/) for other systems):
47
+
48
+ ```bash
49
+ brew install pipx && pipx ensurepath
50
+ # or
51
+ apt install pipx && pipx ensurepath
52
+ # or
53
+ pacman -S python-pipx && pipx ensurepath
54
+ ```
55
+
56
+ ### Install from source
57
+
58
+ ```bash
59
+ # with pipx
60
+ pipx install .
61
+
62
+ # with uv (alternative)
63
+ uv tool install .
64
+
65
+ # or install directly into your environment
66
+ pip install .
67
+ ```
68
+
69
+ ## Usage
70
+
71
+ 1. Install or copy to a host.
72
+ 2. Create a TOML config at `/etc/vlan_probe.toml` or pass `-c` to point to a different file.
73
+ 3. Run `vlan-probe -f table` or `python -m vlan_probe -f table`.
74
+
75
+ ### Configuration
76
+
77
+ Configuration uses TOML format for easy human readability and simplicity:
78
+
79
+ ```toml
80
+ [[targets]]
81
+ name = "Internal - Device A SSH"
82
+ vlan = "Internal"
83
+ ip = "192.168.1.10"
84
+ port = 22
85
+ protocol = "tcp"
86
+ expected_blocked = true
87
+
88
+ [[targets]]
89
+ name = "Internal - Gateway HTTP"
90
+ vlan = "Internal"
91
+ ip = "192.168.2.1"
92
+ port = 80
93
+ protocol = "tcp"
94
+ expected_blocked = true
95
+
96
+ [[targets]]
97
+ name = "External - Public DNS"
98
+ vlan = "External"
99
+ ip = "8.8.8.8"
100
+ port = 53
101
+ protocol = "udp"
102
+ expected_blocked = false
103
+ ```
104
+
105
+ ### Options
106
+
107
+ ```
108
+ -c, --config PATH Path to config TOML file (default: /etc/vlan_probe.toml)
109
+ -f, --format FORMAT Output format: ndjson, json, or table (default: ndjson)
110
+ -t, --timeout SECONDS Socket connection timeout (default: 2.0)
111
+ -s, --strict Exit with code 1 if any violations occur
112
+ --color [auto|always|never] Colorize output (default: auto)
113
+ ```
114
+
115
+ ### Examples
116
+
117
+ ```bash
118
+ # Display as table
119
+ vlan-probe -c ./vlan_probe.toml -f table
120
+
121
+ # Output JSON
122
+ vlan-probe -f json
123
+
124
+ # Strict mode with custom timeout
125
+ vlan-probe -s -t 5.0
126
+
127
+ # No color output
128
+ vlan-probe -f table --color never
129
+ ```
130
+
131
+ ## Development
132
+
133
+ ```bash
134
+ uv sync # install dev dependencies
135
+ uv run pytest # run tests
136
+ uv run ruff check . && uv run ruff format --check . # lint
137
+ uv run mypy src # type check
138
+ ```
139
+
140
+ ## License
141
+
142
+ MIT (see LICENSE)
@@ -0,0 +1,10 @@
1
+ vlan_probe/__init__.py,sha256=TCq8CZROxw24wS8sbZ3AlBScNmnczCGuupp1eVjcSCg,324
2
+ vlan_probe/__main__.py,sha256=mh23umaaV-HntetBr-saUihxqaBgsGDtGxB3gSczPNc,110
3
+ vlan_probe/cli.py,sha256=T6hYHE9mHUDp5XHpduf0hp-pGvmohfqzRhqjgzQCr2A,5138
4
+ vlan_probe/config.py,sha256=LMz9Z3jH96Eo8K5UyEmzTVmhu0SP0Cmg_Y9-UiyOjws,1883
5
+ vlan_probe/probe.py,sha256=Z__1edtn5YPOBXhNOs1GW_7f466rBt5LI_ojTlaDGWI,4016
6
+ vlan_probe-0.1.0.dist-info/METADATA,sha256=7K-un1h3oQIsFxi5Vv9FaZEaRpGkgpTdGrCp75UCLRA,3137
7
+ vlan_probe-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
8
+ vlan_probe-0.1.0.dist-info/entry_points.txt,sha256=raJAYMaxPIKuMakWaQHAHYlYOlC0hLXl2JCaeN_cfVE,51
9
+ vlan_probe-0.1.0.dist-info/licenses/LICENSE,sha256=dh_ozxjkgzEj63HxBsSRHCaa1FugZLJaAO4AuzPSdWY,1065
10
+ vlan_probe-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ vlan-probe = vlan_probe.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 hellqvio
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.