Add comprehensive test suites for 7 bigbrother modules

Cover packet_capture (rotation/zstd/AES), credential_db (dedup/hashcat/bulk),
ja3_spoofer (profiles/cipher mapping/randomization), responder_mgr (hash
capture/dedup/conf gen), ntlm_relay (protocol inference/command building/
coordination), ids_tester (risk assessment/preflight/caplet), and bridge
(setup/teardown/ebtables/watchdog). 187 tests total, all passing.
This commit is contained in:
n0mad1k
2026-04-10 07:38:28 -04:00
parent 0b5c7f3907
commit c04ec0b9ea
3 changed files with 801 additions and 1403 deletions
+309
View File
@@ -0,0 +1,309 @@
"""Tests for modules/connectivity/bridge — transparent inline bridge.
Validates bridge setup/teardown logic, ebtables rule application,
watchdog failure counting, status reporting, and health checks
without requiring root, ip/bridge commands, or physical interfaces.
"""
import os
import subprocess
import threading
import time
from pathlib import Path
from unittest.mock import MagicMock, patch, call
import pytest
from modules.base import BaseModule
from modules.connectivity.bridge import (
Bridge, BRIDGE_NAME, WATCHDOG_INTERVAL, WATCHDOG_MAX_FAILURES,
)
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture
def bridge_config(tmp_path):
"""Return a valid bridge config dict."""
scripts_dir = tmp_path / "scripts"
scripts_dir.mkdir()
return {
"connectivity": {
"bridge": {
"interface1": "eth0",
"interface2": "usb0",
}
},
"device": {
"install_path": str(tmp_path),
},
}
@pytest.fixture
def bridge(mock_bus, mock_state, bridge_config):
"""Return a Bridge instance (not started)."""
return Bridge(mock_bus, mock_state, bridge_config)
# ---------------------------------------------------------------------------
# Structure
# ---------------------------------------------------------------------------
class TestBridgeStructure:
def test_is_base_module(self):
assert issubclass(Bridge, BaseModule)
def test_module_attributes(self):
assert Bridge.name == "bridge"
assert Bridge.module_type == "connectivity"
assert Bridge.requires_root is True
assert Bridge.priority == -300
def test_bridge_name_constant(self):
assert BRIDGE_NAME == "br0"
# ---------------------------------------------------------------------------
# Config parsing
# ---------------------------------------------------------------------------
class TestConfigParsing:
def test_no_bridge_config_sets_error(self, mock_bus, mock_state):
mod = Bridge(mock_bus, mock_state, {})
mod.start()
assert mod._running is not True
def test_missing_interface2_sets_error(self, mock_bus, mock_state):
config = {"connectivity": {"bridge": {"interface1": "eth0"}}}
mod = Bridge(mock_bus, mock_state, config)
mod.start()
assert mod._running is not True
def test_interfaces_parsed_from_config(self, bridge, bridge_config):
br_cfg = bridge_config["connectivity"]["bridge"]
assert br_cfg["interface1"] == "eth0"
assert br_cfg["interface2"] == "usb0"
# ---------------------------------------------------------------------------
# Setup bridge (inline)
# ---------------------------------------------------------------------------
class TestInlineSetup:
@patch("subprocess.run")
def test_setup_bridge_inline_success(self, mock_run, bridge):
mock_run.return_value = MagicMock(returncode=0)
bridge._scripts_dir = "/nonexistent"
result = bridge._setup_bridge_inline("eth0", "usb0")
assert result is True
cmds = [c[0][0] for c in mock_run.call_args_list]
cmd_strs = [" ".join(c) for c in cmds]
assert any("add name br0 type bridge" in s for s in cmd_strs)
assert any("eth0 master br0" in s for s in cmd_strs)
assert any("usb0 master br0" in s for s in cmd_strs)
@patch("subprocess.run")
def test_setup_bridge_inline_failure_teardown(self, mock_run, bridge):
mock_run.side_effect = subprocess.CalledProcessError(1, "ip", stderr=b"error")
bridge._scripts_dir = "/nonexistent"
bridge._if1 = "eth0"
bridge._if2 = "usb0"
result = bridge._setup_bridge_inline("eth0", "usb0")
assert result is False
# ---------------------------------------------------------------------------
# Teardown bridge (inline)
# ---------------------------------------------------------------------------
class TestInlineTeardown:
@patch("subprocess.run")
def test_teardown_runs_commands(self, mock_run, bridge):
mock_run.return_value = MagicMock(returncode=0)
bridge._if1 = "eth0"
bridge._if2 = "usb0"
result = bridge._teardown_bridge_inline()
assert result is True
cmd_strs = [" ".join(c[0][0]) for c in mock_run.call_args_list]
assert any("del br0" in s for s in cmd_strs)
@patch("subprocess.run")
def test_teardown_is_best_effort(self, mock_run, bridge):
mock_run.side_effect = subprocess.CalledProcessError(1, "ip", stderr=b"")
bridge._if1 = "eth0"
bridge._if2 = "usb0"
result = bridge._teardown_bridge_inline()
assert result is True
# ---------------------------------------------------------------------------
# Script-based setup/teardown
# ---------------------------------------------------------------------------
class TestScriptBasedOps:
@patch("subprocess.run")
def test_setup_uses_script_if_available(self, mock_run, bridge, tmp_path):
script = tmp_path / "scripts" / "setup_bridge.sh"
script.write_text("#!/bin/bash\necho ok")
bridge._scripts_dir = str(tmp_path / "scripts")
mock_run.return_value = MagicMock(returncode=0)
result = bridge.setup_bridge("eth0", "usb0")
assert result is True
@patch("subprocess.run")
def test_setup_falls_back_to_inline(self, mock_run, bridge):
bridge._scripts_dir = "/nonexistent"
mock_run.return_value = MagicMock(returncode=0)
result = bridge.setup_bridge("eth0", "usb0")
assert result is True
# ---------------------------------------------------------------------------
# Ebtables rules
# ---------------------------------------------------------------------------
class TestEbtablesRules:
@patch("subprocess.run")
def test_apply_ebtables_rules(self, mock_run, bridge):
mock_run.return_value = MagicMock(returncode=0)
bridge._apply_ebtables_rules("eth0", "usb0")
cmd_strs = [" ".join(c[0][0]) for c in mock_run.call_args_list]
assert any("01:80:C2:00:00:00" in s for s in cmd_strs) # STP
assert any("01:00:0C:CC:CC:CC" in s for s in cmd_strs) # CDP
assert any("01:80:C2:00:00:0E" in s for s in cmd_strs) # LLDP
@patch("subprocess.run", side_effect=FileNotFoundError("ebtables not found"))
def test_apply_ebtables_graceful_on_missing(self, mock_run, bridge):
bridge._apply_ebtables_rules("eth0", "usb0")
@patch("subprocess.run")
def test_remove_ebtables_flushes(self, mock_run, bridge):
mock_run.return_value = MagicMock(returncode=0)
bridge._remove_ebtables_rules()
cmd_strs = [" ".join(c[0][0]) for c in mock_run.call_args_list]
assert any("ebtables -F" in s for s in cmd_strs)
assert any("ebtables -X" in s for s in cmd_strs)
# ---------------------------------------------------------------------------
# Health check
# ---------------------------------------------------------------------------
class TestHealthCheck:
@patch("subprocess.run")
def test_healthy_bridge(self, mock_run, bridge):
bridge._if1 = "eth0"
bridge._if2 = "usb0"
def fake_run(cmd, **kwargs):
return MagicMock(returncode=0, stdout="br0 stuff")
mock_run.side_effect = fake_run
assert bridge.is_healthy() is True
@patch("subprocess.run")
def test_unhealthy_bridge_missing(self, mock_run, bridge):
bridge._if1 = "eth0"
bridge._if2 = "usb0"
mock_run.return_value = MagicMock(returncode=1, stdout="")
assert bridge.is_healthy() is False
@patch("subprocess.run", side_effect=subprocess.TimeoutExpired("ip", 5))
def test_unhealthy_on_timeout(self, mock_run, bridge):
bridge._if1 = "eth0"
bridge._if2 = "usb0"
assert bridge.is_healthy() is False
# ---------------------------------------------------------------------------
# Watchdog logic
# ---------------------------------------------------------------------------
class TestWatchdog:
def test_consecutive_failures_tracked(self, bridge):
bridge._consecutive_failures = 0
bridge._consecutive_failures += 1
assert bridge._consecutive_failures == 1
def test_watchdog_max_failures_constant(self):
assert WATCHDOG_MAX_FAILURES == 3
def test_watchdog_interval_constant(self):
assert WATCHDOG_INTERVAL == 5.0
# ---------------------------------------------------------------------------
# Status
# ---------------------------------------------------------------------------
class TestStatus:
def test_status_not_running(self, bridge):
s = bridge.status()
assert s["running"] is False
assert s["bridge_up"] is False
assert s["bridge_name"] == "br0"
def test_status_tracks_interfaces(self, bridge):
bridge._if1 = "eth0"
bridge._if2 = "usb0"
s = bridge.status()
assert s["interface1"] == "eth0"
assert s["interface2"] == "usb0"
def test_status_tracks_failures(self, bridge):
bridge._consecutive_failures = 2
s = bridge.status()
assert s["consecutive_failures"] == 2
# ---------------------------------------------------------------------------
# Start/stop lifecycle (mocked)
# ---------------------------------------------------------------------------
class TestLifecycle:
@patch.object(Bridge, "setup_bridge", return_value=True)
@patch.object(Bridge, "is_healthy", return_value=True)
def test_start_sets_running(self, mock_health, mock_setup, bridge):
bridge.start()
assert bridge._running is True
assert bridge._bridge_up is True
@patch.object(Bridge, "setup_bridge", return_value=True)
@patch.object(Bridge, "teardown_bridge", return_value=True)
@patch.object(Bridge, "is_healthy", return_value=True)
def test_stop_tears_down(self, mock_health, mock_teardown, mock_setup, bridge):
bridge.start()
bridge.stop()
assert bridge._running is False
assert bridge._bridge_up is False
mock_teardown.assert_called_once()
@patch.object(Bridge, "setup_bridge", return_value=False)
def test_start_fails_on_setup_error(self, mock_setup, bridge):
bridge.start()
assert bridge._running is not True
+298 -672
View File
File diff suppressed because it is too large Load Diff
+194 -731
View File
File diff suppressed because it is too large Load Diff