415 lines
15 KiB
Python
415 lines
15 KiB
Python
"""Tests for ALL module categories — import, BaseModule subclass, and attribute validation.
|
|
|
|
Covers: passive (17), active (9), intel (9), connectivity (8).
|
|
Stealth modules are already covered by test_stealth_modules.py.
|
|
"""
|
|
|
|
import importlib
|
|
|
|
import pytest
|
|
|
|
from modules.base import BaseModule
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Passive modules (17)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
PASSIVE_MODULE_DOTTED = [
|
|
"modules.passive.packet_capture",
|
|
"modules.passive.dns_logger",
|
|
"modules.passive.tls_sni_extractor",
|
|
"modules.passive.credential_sniffer",
|
|
"modules.passive.kerberos_harvester",
|
|
"modules.passive.host_discovery",
|
|
"modules.passive.os_fingerprint",
|
|
"modules.passive.traffic_analyzer",
|
|
"modules.passive.vlan_discovery",
|
|
"modules.passive.network_mapper",
|
|
"modules.passive.auth_flow_tracker",
|
|
"modules.passive.smb_monitor",
|
|
"modules.passive.cloud_token_harvester",
|
|
"modules.passive.ldap_harvester",
|
|
"modules.passive.rdp_monitor",
|
|
"modules.passive.quic_analyzer",
|
|
"modules.passive.db_interceptor",
|
|
]
|
|
|
|
PASSIVE_CLASS_NAMES = [
|
|
"PacketCapture",
|
|
"DNSLogger",
|
|
"TLSSNIExtractor",
|
|
"CredentialSniffer",
|
|
"KerberosHarvester",
|
|
"HostDiscovery",
|
|
"OSFingerprint",
|
|
"TrafficAnalyzer",
|
|
"VLANDiscovery",
|
|
"NetworkMapper",
|
|
"AuthFlowTracker",
|
|
"SMBMonitor",
|
|
"CloudTokenHarvester",
|
|
"LDAPHarvester",
|
|
"RDPMonitor",
|
|
"QUICAnalyzer",
|
|
"DBInterceptor",
|
|
]
|
|
|
|
|
|
class TestPassiveModulesImportable:
|
|
|
|
@pytest.mark.parametrize("dotted_path", PASSIVE_MODULE_DOTTED)
|
|
def test_passive_module_importable(self, dotted_path):
|
|
"""Each passive module file can be imported without errors."""
|
|
mod = importlib.import_module(dotted_path)
|
|
assert mod is not None
|
|
|
|
|
|
class TestPassiveModulesAreBaseModule:
|
|
|
|
@pytest.mark.parametrize("class_name", PASSIVE_CLASS_NAMES)
|
|
def test_passive_modules_are_base_module(self, class_name):
|
|
"""Each passive module class is a subclass of BaseModule."""
|
|
import modules.passive as passive_pkg
|
|
cls = getattr(passive_pkg, class_name)
|
|
assert issubclass(cls, BaseModule), f"{class_name} is not a BaseModule subclass"
|
|
|
|
|
|
class TestPassiveModuleAttributes:
|
|
|
|
@pytest.mark.parametrize("class_name", PASSIVE_CLASS_NAMES)
|
|
def test_passive_module_attributes(self, class_name):
|
|
"""Each passive module has required class attributes."""
|
|
import modules.passive as passive_pkg
|
|
cls = getattr(passive_pkg, class_name)
|
|
|
|
assert hasattr(cls, "name") and cls.name != "unnamed"
|
|
assert hasattr(cls, "module_type") and cls.module_type == "passive"
|
|
assert hasattr(cls, "priority") and isinstance(cls.priority, int)
|
|
assert hasattr(cls, "dependencies") and isinstance(cls.dependencies, list)
|
|
assert hasattr(cls, "requires_root")
|
|
|
|
for method_name in ("start", "stop", "status", "configure"):
|
|
assert callable(getattr(cls, method_name, None))
|
|
|
|
|
|
class TestPassiveModuleInstantiate:
|
|
|
|
@pytest.mark.parametrize("class_name", PASSIVE_CLASS_NAMES)
|
|
def test_passive_module_instantiate(self, class_name, mock_bus, mock_state, mock_config):
|
|
"""Each passive module can be instantiated with mock bus/state/config."""
|
|
import modules.passive as passive_pkg
|
|
cls = getattr(passive_pkg, class_name)
|
|
instance = cls(bus=mock_bus, state=mock_state, config=mock_config)
|
|
assert instance is not None
|
|
assert instance._running is False
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Active modules (9)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
ACTIVE_MODULE_DOTTED = [
|
|
"modules.active.bettercap_mgr",
|
|
"modules.active.arp_spoof",
|
|
"modules.active.dns_poison",
|
|
"modules.active.dhcp_spoof",
|
|
"modules.active.evil_twin",
|
|
"modules.active.ipv6_slaac",
|
|
"modules.active.responder_mgr",
|
|
"modules.active.mitmproxy_mgr",
|
|
"modules.active.ntlm_relay",
|
|
]
|
|
|
|
ACTIVE_CLASS_NAMES = [
|
|
"BettercapManager",
|
|
"ARPSpoof",
|
|
"DNSPoison",
|
|
"DHCPSpoof",
|
|
"EvilTwin",
|
|
"IPv6SLAAC",
|
|
"ResponderManager",
|
|
"MitmproxyManager",
|
|
"NTLMRelay",
|
|
]
|
|
|
|
|
|
class TestActiveModulesImportable:
|
|
|
|
@pytest.mark.parametrize("dotted_path", ACTIVE_MODULE_DOTTED)
|
|
def test_active_module_importable(self, dotted_path):
|
|
"""Each active module file can be imported without errors."""
|
|
mod = importlib.import_module(dotted_path)
|
|
assert mod is not None
|
|
|
|
|
|
class TestActiveModulesAreBaseModule:
|
|
|
|
@pytest.mark.parametrize("class_name", ACTIVE_CLASS_NAMES)
|
|
def test_active_modules_are_base_module(self, class_name):
|
|
"""Each active module class is a subclass of BaseModule."""
|
|
import modules.active as active_pkg
|
|
cls = getattr(active_pkg, class_name)
|
|
assert issubclass(cls, BaseModule), f"{class_name} is not a BaseModule subclass"
|
|
|
|
|
|
class TestActiveModuleAttributes:
|
|
|
|
@pytest.mark.parametrize("class_name", ACTIVE_CLASS_NAMES)
|
|
def test_active_module_attributes(self, class_name):
|
|
"""Each active module has required class attributes."""
|
|
import modules.active as active_pkg
|
|
cls = getattr(active_pkg, class_name)
|
|
|
|
assert hasattr(cls, "name") and cls.name != "unnamed"
|
|
assert hasattr(cls, "module_type") and cls.module_type == "active"
|
|
assert hasattr(cls, "priority") and isinstance(cls.priority, int)
|
|
assert hasattr(cls, "dependencies") and isinstance(cls.dependencies, list)
|
|
assert hasattr(cls, "requires_root")
|
|
|
|
for method_name in ("start", "stop", "status", "configure"):
|
|
assert callable(getattr(cls, method_name, None))
|
|
|
|
|
|
class TestActiveModuleInstantiate:
|
|
|
|
@pytest.mark.parametrize("class_name", ACTIVE_CLASS_NAMES)
|
|
def test_active_module_instantiate(self, class_name, mock_bus, mock_state, mock_config):
|
|
"""Each active module can be instantiated with mock bus/state/config."""
|
|
import modules.active as active_pkg
|
|
cls = getattr(active_pkg, class_name)
|
|
instance = cls(bus=mock_bus, state=mock_state, config=mock_config)
|
|
assert instance is not None
|
|
assert instance._running is False
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Intel modules (9)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
INTEL_MODULE_DOTTED = [
|
|
"modules.intel.credential_db",
|
|
"modules.intel.topology_mapper",
|
|
"modules.intel.net_intel",
|
|
"modules.intel.user_timeline",
|
|
"modules.intel.supply_chain_detect",
|
|
"modules.intel.change_detector",
|
|
"modules.intel.security_posture",
|
|
"modules.intel.operator_audit",
|
|
"modules.intel.tool_output_parser",
|
|
]
|
|
|
|
INTEL_CLASS_NAMES = [
|
|
"CredentialDB",
|
|
"TopologyMapper",
|
|
"NetIntel",
|
|
"UserTimeline",
|
|
"SupplyChainDetect",
|
|
"ChangeDetector",
|
|
"SecurityPosture",
|
|
"OperatorAudit",
|
|
"ToolOutputParser",
|
|
]
|
|
|
|
|
|
class TestIntelModulesImportable:
|
|
|
|
@pytest.mark.parametrize("dotted_path", INTEL_MODULE_DOTTED)
|
|
def test_intel_module_importable(self, dotted_path):
|
|
"""Each intel module file can be imported without errors."""
|
|
mod = importlib.import_module(dotted_path)
|
|
assert mod is not None
|
|
|
|
|
|
class TestIntelModulesAreBaseModule:
|
|
|
|
@pytest.mark.parametrize("class_name", INTEL_CLASS_NAMES)
|
|
def test_intel_modules_are_base_module(self, class_name):
|
|
"""Each intel module class is a subclass of BaseModule."""
|
|
import modules.intel as intel_pkg
|
|
cls = getattr(intel_pkg, class_name)
|
|
assert issubclass(cls, BaseModule), f"{class_name} is not a BaseModule subclass"
|
|
|
|
|
|
class TestIntelModuleAttributes:
|
|
|
|
@pytest.mark.parametrize("class_name", INTEL_CLASS_NAMES)
|
|
def test_intel_module_attributes(self, class_name):
|
|
"""Each intel module has required class attributes."""
|
|
import modules.intel as intel_pkg
|
|
cls = getattr(intel_pkg, class_name)
|
|
|
|
assert hasattr(cls, "name") and cls.name != "unnamed"
|
|
assert hasattr(cls, "module_type") and cls.module_type == "intel"
|
|
assert hasattr(cls, "priority") and isinstance(cls.priority, int)
|
|
assert hasattr(cls, "dependencies") and isinstance(cls.dependencies, list)
|
|
assert hasattr(cls, "requires_root")
|
|
|
|
for method_name in ("start", "stop", "status", "configure"):
|
|
assert callable(getattr(cls, method_name, None))
|
|
|
|
|
|
class TestIntelModuleInstantiate:
|
|
|
|
@pytest.mark.parametrize("class_name", INTEL_CLASS_NAMES)
|
|
def test_intel_module_instantiate(self, class_name, mock_bus, mock_state, mock_config):
|
|
"""Each intel module can be instantiated with mock bus/state/config."""
|
|
import modules.intel as intel_pkg
|
|
cls = getattr(intel_pkg, class_name)
|
|
instance = cls(bus=mock_bus, state=mock_state, config=mock_config)
|
|
assert instance is not None
|
|
assert instance._running is False
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Connectivity modules (8)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
CONNECTIVITY_MODULE_DOTTED = [
|
|
"modules.connectivity.wireguard",
|
|
"modules.connectivity.tailscale",
|
|
"modules.connectivity.bridge",
|
|
"modules.connectivity.wifi_client",
|
|
"modules.connectivity.cellular_backup",
|
|
"modules.connectivity.ble_emergency",
|
|
"modules.connectivity.data_exfil",
|
|
]
|
|
|
|
CONNECTIVITY_CLASS_NAMES = [
|
|
"WireGuard",
|
|
"Tailscale",
|
|
"Bridge",
|
|
"WiFiClient",
|
|
"CellularBackup",
|
|
"BLEEmergency",
|
|
"DataExfil",
|
|
]
|
|
|
|
|
|
class TestConnectivityModulesImportable:
|
|
|
|
@pytest.mark.parametrize("dotted_path", CONNECTIVITY_MODULE_DOTTED)
|
|
def test_connectivity_module_importable(self, dotted_path):
|
|
"""Each connectivity module file can be imported without errors."""
|
|
mod = importlib.import_module(dotted_path)
|
|
assert mod is not None
|
|
|
|
|
|
class TestConnectivityModulesAreBaseModule:
|
|
|
|
@pytest.mark.parametrize("class_name", CONNECTIVITY_CLASS_NAMES)
|
|
def test_connectivity_modules_are_base_module(self, class_name):
|
|
"""Each connectivity module class is a subclass of BaseModule."""
|
|
import modules.connectivity as conn_pkg
|
|
cls = getattr(conn_pkg, class_name)
|
|
assert issubclass(cls, BaseModule), f"{class_name} is not a BaseModule subclass"
|
|
|
|
|
|
class TestConnectivityModuleAttributes:
|
|
|
|
@pytest.mark.parametrize("class_name", CONNECTIVITY_CLASS_NAMES)
|
|
def test_connectivity_module_attributes(self, class_name):
|
|
"""Each connectivity module has required class attributes."""
|
|
import modules.connectivity as conn_pkg
|
|
cls = getattr(conn_pkg, class_name)
|
|
|
|
assert hasattr(cls, "name") and cls.name != "unnamed"
|
|
assert hasattr(cls, "module_type") and cls.module_type == "connectivity"
|
|
assert hasattr(cls, "priority") and isinstance(cls.priority, int)
|
|
assert hasattr(cls, "dependencies") and isinstance(cls.dependencies, list)
|
|
assert hasattr(cls, "requires_root")
|
|
|
|
for method_name in ("start", "stop", "status", "configure"):
|
|
assert callable(getattr(cls, method_name, None))
|
|
|
|
|
|
class TestConnectivityModuleInstantiate:
|
|
|
|
@pytest.mark.parametrize("class_name", CONNECTIVITY_CLASS_NAMES)
|
|
def test_connectivity_module_instantiate(self, class_name, mock_bus, mock_state, mock_config):
|
|
"""Each connectivity module can be instantiated with mock bus/state/config."""
|
|
import modules.connectivity as conn_pkg
|
|
cls = getattr(conn_pkg, class_name)
|
|
instance = cls(bus=mock_bus, state=mock_state, config=mock_config)
|
|
assert instance is not None
|
|
assert instance._running is False
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Cross-category: package __init__.py exports
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestPackageExports:
|
|
|
|
def test_passive_init_exports_all(self):
|
|
"""modules.passive.__init__.py exports all 17 classes."""
|
|
import modules.passive as pkg
|
|
assert len(pkg.__all__) == 17
|
|
for name in pkg.__all__:
|
|
cls = getattr(pkg, name)
|
|
assert issubclass(cls, BaseModule)
|
|
|
|
def test_active_init_exports_all(self):
|
|
"""modules.active.__init__.py exports all 9 classes."""
|
|
import modules.active as pkg
|
|
assert len(pkg.__all__) == 9
|
|
for name in pkg.__all__:
|
|
cls = getattr(pkg, name)
|
|
assert issubclass(cls, BaseModule)
|
|
|
|
def test_intel_init_exports_all(self):
|
|
"""modules.intel.__init__.py exports all 9 classes."""
|
|
import modules.intel as pkg
|
|
assert len(pkg.__all__) == 9
|
|
for name in pkg.__all__:
|
|
cls = getattr(pkg, name)
|
|
assert issubclass(cls, BaseModule)
|
|
|
|
def test_connectivity_init_exports_all(self):
|
|
"""modules.connectivity.__init__.py exports all 7 classes."""
|
|
import modules.connectivity as pkg
|
|
assert len(pkg.__all__) == 7
|
|
for name in pkg.__all__:
|
|
cls = getattr(pkg, name)
|
|
assert issubclass(cls, BaseModule)
|
|
|
|
def test_stealth_init_exports_all(self):
|
|
"""modules.stealth.__init__.py exports all 12 classes."""
|
|
import modules.stealth as pkg
|
|
assert len(pkg.__all__) == 12
|
|
for name in pkg.__all__:
|
|
cls = getattr(pkg, name)
|
|
assert issubclass(cls, BaseModule)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Discovery function test
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestModuleDiscovery:
|
|
|
|
def test_discover_all_modules(self):
|
|
"""discover_all_modules() finds all 55 modules across 5 categories."""
|
|
from pathlib import Path
|
|
import sys
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
from bigbrother import discover_all_modules, discover_modules_in_category
|
|
|
|
all_mods = discover_all_modules()
|
|
# Stealth: 12, Passive: 17, Active: 9, Intel: 9, Connectivity: 7 = 54
|
|
assert len(all_mods) == 54, f"Expected 54 modules, found {len(all_mods)}"
|
|
|
|
# Verify per-category counts
|
|
assert len(discover_modules_in_category("stealth")) == 12
|
|
assert len(discover_modules_in_category("passive")) == 17
|
|
assert len(discover_modules_in_category("active")) == 9
|
|
assert len(discover_modules_in_category("intel")) == 9
|
|
assert len(discover_modules_in_category("connectivity")) == 7
|
|
|
|
def test_all_discovered_are_base_module(self):
|
|
"""Every discovered module is a BaseModule subclass."""
|
|
from bigbrother import discover_all_modules
|
|
|
|
for name, cls in discover_all_modules().items():
|
|
assert issubclass(cls, BaseModule), f"{name} ({cls}) is not a BaseModule subclass"
|