diff --git a/.gitignore b/.gitignore index 284e270..939ce41 100644 --- a/.gitignore +++ b/.gitignore @@ -1,48 +1,9 @@ -# Python __pycache__/ *.py[cod] *$py.class -*.so -.Python -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ -*.egg-info/ +.pytest_cache/ +.venv/ +.env dist/ build/ - -# Security Analysis Results (user-generated) -*.json -*_results.txt -*_report.txt -*_audit.json -daily_check.json -weekly_audit.json -security_check.json - -# macOS -.DS_Store -.DS_Store? -._* -.Spotlight-V100 -.Trashes -ehthumbs.db -Thumbs.db - -# IDE -.vscode/ -.idea/ -*.swp -*.swo -*~ - -# Temporary files -*.tmp -*.temp -*.log - -# User configuration -config.local.py -.env \ No newline at end of file +*.egg-info/ \ No newline at end of file diff --git a/src/vpn_config_parser.py b/src/vpn_config_parser.py new file mode 100644 index 0000000..4b3dec4 --- /dev/null +++ b/src/vpn_config_parser.py @@ -0,0 +1,160 @@ +import subprocess +import re +from typing import Dict, Optional, List +import logging +import platform + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +class VPNConfigParser: + """ + A utility class for parsing VPN configuration details from system commands. + + Supports multiple platforms and VPN types with modular parsing strategies. + """ + + @staticmethod + def run_command(command: List[str]) -> str: + """ + Execute a system command and return its output. + + Args: + command (List[str]): Command to execute as a list of strings + + Returns: + str: Command output + + Raises: + subprocess.CalledProcessError: If command execution fails + """ + try: + result = subprocess.run( + command, + capture_output=True, + text=True, + check=True + ) + return result.stdout.strip() + except subprocess.CalledProcessError as e: + logger.error(f"Command execution failed: {e}") + raise + + @classmethod + def parse_macos_vpn_config(cls) -> Dict[str, Optional[str]]: + """ + Parse VPN configuration on macOS using system commands. + + Returns: + Dict[str, Optional[str]]: VPN configuration details + """ + try: + # Retrieve network service configurations + network_services_output = cls.run_command( + ["networksetup", "-listnetworkserviceorder"] + ) + + # Search for VPN-related services + vpn_services = re.findall( + r'\(\d+\)\s+(.*\(VPN\))', + network_services_output + ) + + # Detailed configuration parsing + config = { + "platform": "darwin", + "vpn_services": vpn_services, + "active_vpn": None, + "vpn_type": None + } + + # Check active VPN connections + try: + scutil_output = cls.run_command( + ["scutil", "--nc", "status"] + ) + active_vpns = [ + line.split(":")[0].strip() + for line in scutil_output.split("\n") + if "Connected" in line + ] + config["active_vpn"] = active_vpns[0] if active_vpns else None + except Exception as e: + logger.warning(f"Could not retrieve active VPN: {e}") + + return config + + except Exception as e: + logger.error(f"macOS VPN config parsing failed: {e}") + return { + "platform": "darwin", + "vpn_services": [], + "active_vpn": None, + "vpn_type": None + } + + @classmethod + def parse_linux_vpn_config(cls) -> Dict[str, Optional[str]]: + """ + Parse VPN configuration on Linux systems. + + Returns: + Dict[str, Optional[str]]: VPN configuration details + """ + try: + # Retrieve network interfaces + ip_output = cls.run_command(["ip", "tuntap", "show"]) + + # Look for VPN-related interfaces + vpn_interfaces = re.findall(r'(tun\d+)', ip_output) + + config = { + "platform": "linux", + "vpn_interfaces": vpn_interfaces, + "active_vpn": None, + "vpn_type": None + } + + # Check for active connections via routing table + try: + route_output = cls.run_command(["ip", "route", "show", "table", "all"]) + vpn_routes = [ + line for line in route_output.split("\n") + if "tun" in line + ] + config["active_vpn"] = vpn_routes[0] if vpn_routes else None + except Exception as e: + logger.warning(f"Could not retrieve active VPN routes: {e}") + + return config + + except Exception as e: + logger.error(f"Linux VPN config parsing failed: {e}") + return { + "platform": "linux", + "vpn_interfaces": [], + "active_vpn": None, + "vpn_type": None + } + + @classmethod + def detect_vpn_config(cls) -> Dict[str, Optional[str]]: + """ + Detect VPN configuration based on the current platform. + + Returns: + Dict[str, Optional[str]]: Parsed VPN configuration + """ + os_name = platform.system().lower() + + if os_name == "darwin": + return cls.parse_macos_vpn_config() + elif os_name == "linux": + return cls.parse_linux_vpn_config() + else: + logger.warning(f"Unsupported platform: {os_name}") + return { + "platform": os_name, + "active_vpn": None, + "vpn_type": None + } \ No newline at end of file diff --git a/tests/test_vpn_config_parser.py b/tests/test_vpn_config_parser.py new file mode 100644 index 0000000..c18fb65 --- /dev/null +++ b/tests/test_vpn_config_parser.py @@ -0,0 +1,54 @@ +import pytest +import platform +from unittest.mock import patch +from src.vpn_config_parser import VPNConfigParser + +def test_run_command_success(): + """Test successful command execution.""" + with patch('subprocess.run') as mock_run: + mock_run.return_value.stdout = "test output\n" + mock_run.return_value.returncode = 0 + + result = VPNConfigParser.run_command(["echo", "test"]) + assert result == "test output" + +def test_run_command_failure(): + """Test command execution failure.""" + with patch('subprocess.run') as mock_run: + mock_run.side_effect = Exception("Command failed") + + with pytest.raises(Exception): + VPNConfigParser.run_command(["invalid_command"]) + +@pytest.mark.skipif(platform.system() != "Darwin", reason="macOS specific test") +def test_macos_vpn_config_parsing(): + """Test VPN configuration parsing on macOS.""" + with patch.object(VPNConfigParser, 'run_command') as mock_run: + mock_run.side_effect = [ + "(1) VPN (Service)\n(2) Wi-Fi (Built-in)", + "Connected: VPN Service" + ] + + config = VPNConfigParser.parse_macos_vpn_config() + assert "VPN (Service)" in config["vpn_services"] + assert config["active_vpn"] is not None + +@pytest.mark.skipif(platform.system() != "Linux", reason="Linux specific test") +def test_linux_vpn_config_parsing(): + """Test VPN configuration parsing on Linux.""" + with patch.object(VPNConfigParser, 'run_command') as mock_run: + mock_run.side_effect = [ + "tun0: tun\ntun1: tun", + "default via 192.168.1.1 dev tun0" + ] + + config = VPNConfigParser.parse_linux_vpn_config() + assert len(config["vpn_interfaces"]) > 0 + assert config["active_vpn"] is not None + +def test_detect_vpn_config(): + """Test VPN configuration detection across platforms.""" + config = VPNConfigParser.detect_vpn_config() + assert "platform" in config + assert "active_vpn" in config + assert "vpn_type" in config \ No newline at end of file