Phase 5: Final integration — fix imports, full module discovery, interactive menu
- Fix pyserial import error in cellular_backup.py (lazy import with fallback) - Replace stealth-only module discovery with discover_all_modules() across all 5 categories (stealth, passive, active, intel, connectivity) - Wire up interactive menu options 5-8: - View Credentials: query credential_db SQLite, Rich table with stats - View Intelligence: host count, cred stats, security posture summary - Network Topology: host table from topology_mapper state data - Timeline: recent entries from operator_audit HMAC-chained log - Update start command to discover+register all enabled modules (not just stealth) - Update modules/activate/selftest commands to use full module discovery - Add is_module_enabled() config-aware helper for per-category defaults - Fix modules.yaml module count headers (16->17 passive, 8->9 active/intel) - Set bettercap_mgr enabled=false (all active modules disabled by default) - Add tests/test_all_modules.py: 179 parametrized tests covering import, BaseModule subclass, attributes, and instantiation for all 55 modules across passive, active, intel, and connectivity categories - All 256 tests passing
This commit is contained in:
@@ -0,0 +1,416 @@
|
||||
"""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.reverse_tunnel",
|
||||
"modules.connectivity.cellular_backup",
|
||||
"modules.connectivity.ble_emergency",
|
||||
"modules.connectivity.data_exfil",
|
||||
]
|
||||
|
||||
CONNECTIVITY_CLASS_NAMES = [
|
||||
"WireGuard",
|
||||
"Tailscale",
|
||||
"Bridge",
|
||||
"WiFiClient",
|
||||
"ReverseTunnel",
|
||||
"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 8 classes."""
|
||||
import modules.connectivity as pkg
|
||||
assert len(pkg.__all__) == 8
|
||||
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: 8 = 55
|
||||
assert len(all_mods) == 55, f"Expected 55 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")) == 8
|
||||
|
||||
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"
|
||||
Reference in New Issue
Block a user