Files
comi-hardening-windows/tests/test_setup.py
T

164 lines
6.4 KiB
Python

"""Synthetic tests for the WAPT package; no Windows registry is accessed."""
import importlib.util
import json
import os
from pathlib import Path
import sys
import tempfile
import types
import unittest
from unittest import mock
ROOT = Path(__file__).resolve().parents[1]
REGISTRY = {}
WRITES = []
DELETES = []
HKLM = 'synthetic-HKLM'
class RegistryHandle:
def __init__(self, root, path):
self.address = (root, path)
def __enter__(self):
return self.address
def __exit__(self, *args):
return False
def registry_set(root, path, name, value, type=None):
REGISTRY.setdefault((root, path), {})[name] = value
WRITES.append((root, path, name, value, type))
def _load_setup_module():
helpers = types.ModuleType('setuphelpers')
helpers.HKEY_LOCAL_MACHINE = HKLM
helpers.KEY_ALL_ACCESS = 0xF003F
helpers.REG_DWORD = 4
helpers.makepath = lambda *parts: os.path.join(*(str(part) for part in parts))
helpers.programdata = lambda: tempfile.gettempdir()
helpers.mkdirs = lambda path: os.makedirs(path, exist_ok=True)
helpers.isfile = os.path.isfile
helpers.remove_file = lambda path: os.remove(path) if os.path.exists(path) else None
helpers.json_write_file = lambda path, data, **kwargs: Path(path).write_text(
json.dumps(data, **kwargs), encoding='utf-8')
helpers.json_load_file = lambda path: json.loads(Path(path).read_text(encoding='utf-8'))
helpers.reg_key_exists = lambda root, path: (root, path) in REGISTRY
helpers.reg_value_exists = lambda root, path, name: name in REGISTRY.get((root, path), {})
helpers.reg_openkey_noredir = lambda root, path, **kwargs: RegistryHandle(root, path)
helpers.reg_getvalue = lambda key, name, default=None: REGISTRY.get(key, {}).get(name, default)
helpers.registry_set = registry_set
def delete_value(key, name):
DELETES.append((key[0], key[1], name))
REGISTRY.get(key, {}).pop(name, None)
helpers.reg_delvalue = delete_value
with mock.patch.dict(sys.modules, {'setuphelpers': helpers}):
spec = importlib.util.spec_from_file_location('hardening_setup', ROOT / 'setup.py')
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
setup = _load_setup_module()
class HardeningSetupTests(unittest.TestCase):
def setUp(self):
REGISTRY.clear()
WRITES.clear()
DELETES.clear()
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.state_dir = Path(self.temp.name) / 'Comitari' / 'HardeningSafe'
self.state_file = self.state_dir / 'original-values.json'
self.programdata_patch = mock.patch.object(setup, 'programdata', lambda: self.temp.name)
self.programdata_patch.start()
self.addCleanup(self.programdata_patch.stop)
def _seed(self, setting, value):
address = (HKLM, setup._registry_path(setting['Path']))
REGISTRY.setdefault(address, {})[setting['Name']] = value
def test_install_saves_originals_once_before_applying_dwords(self):
setting = setup.SETTINGS[0]
self._seed(setting, 0)
setup.install()
state = json.loads(self.state_file.read_text(encoding='utf-8'))
self.assertEqual(len(state), 5)
self.assertEqual(state[0]['Value'], 0)
self.assertTrue(state[0]['Exists'])
self.assertEqual(state[0]['Target'], 1)
self.assertEqual(len(WRITES), 5)
self.assertTrue(all(write[4] == setup.REG_DWORD for write in WRITES))
self._seed(setting, 7)
setup.install()
state_again = json.loads(self.state_file.read_text(encoding='utf-8'))
self.assertEqual(state_again[0]['Value'], 0)
def test_audit_is_read_only_and_reports_noncompliance(self):
setup.install()
self.assertEqual(setup.audit(), 'OK')
before = list(WRITES)
self._seed(setup.SETTINGS[1], 0)
self.assertEqual(setup.audit(), 'WARNING')
self.assertEqual(WRITES, before)
def test_audit_read_error_is_not_reported_as_healthy(self):
with mock.patch.object(setup, 'reg_key_exists', side_effect=PermissionError):
self.assertEqual(setup.audit(), 'WARNING')
def test_uninstall_restores_owned_values_and_preserves_external_changes(self):
existing = setup.SETTINGS[0]
absent = setup.SETTINGS[1]
externally_changed = setup.SETTINGS[2]
self._seed(existing, 0)
setup.install()
self._seed(externally_changed, 77)
setup.uninstall()
self.assertEqual(REGISTRY[(HKLM, setup._registry_path(existing['Path']))][existing['Name']], 0)
self.assertNotIn(absent['Name'], REGISTRY[(HKLM, setup._registry_path(absent['Path']))])
self.assertEqual(REGISTRY[(HKLM, setup._registry_path(externally_changed['Path']))]
[externally_changed['Name']], 77)
self.assertIn((HKLM, setup._registry_path(absent['Path']), absent['Name']), DELETES)
self.assertTrue(self.state_file.is_file())
def test_uninstall_without_baseline_refuses_changes(self):
with self.assertRaisesRegex(RuntimeError, 'No saved baseline'):
setup.uninstall()
self.assertEqual(WRITES, [])
self.assertEqual(DELETES, [])
def test_invalid_baseline_refuses_all_registry_changes(self):
self.state_dir.mkdir(parents=True)
self.state_file.write_text(json.dumps([{'Path': 'HKLM:\\SOFTWARE\\Elsewhere',
'Name': 'Synthetic', 'Exists': False,
'Value': None, 'Target': 1}]), encoding='utf-8')
with self.assertRaisesRegex(RuntimeError, 'incomplete'):
setup.uninstall()
self.assertEqual(WRITES, [])
self.assertEqual(DELETES, [])
def test_failure_to_persist_baseline_prevents_registry_modification(self):
with mock.patch.object(setup, 'json_write_file', side_effect=OSError('synthetic disk error')):
with self.assertRaises(OSError):
setup.install()
self.assertEqual(WRITES, [])
def test_no_powershell_or_child_process_dependency_remains(self):
source = (ROOT / 'setup.py').read_text(encoding='utf-8').lower()
self.assertNotIn('subprocess', source)
self.assertNotIn('powershell.exe', source)
self.assertNotIn('hardening.ps1', source)
if __name__ == '__main__':
unittest.main()