refactor: inline Windows hardening with WAPT helpers
This commit is contained in:
1 parent
11f99a588f
commit
3a046ef57d
4 files changed
+339
-82
No files matched your search
@@ -5,15 +5,23 @@ SOURCE ONLY: must build and sign with the organization's WAPT tooling. Not yet W
|
||||
## Scope
|
||||
Five HKLM registry settings: disable AutoRun and AutoPlay (including non-volume devices), disable lock-screen camera and slideshows. No account / password / LAPS change. No WAPT, firewall, network, SMB, BitLocker, Microsoft Defender, antivirus, remote desktop, printing or power settings change.
|
||||
|
||||
## Implementation
|
||||
- `setup.py` now contains the complete install, audit and conditional rollback logic.
|
||||
- Registry reads and writes use WAPT 2.6 `setuphelpers` (`reg_key_exists`, `reg_value_exists`, `reg_openkey_noredir`, `reg_getvalue`, `registry_set`, and `reg_delvalue`). No PowerShell process or external command is launched.
|
||||
- The approved setting table is centralized in `SETTINGS`. A future `update_package.py` can validate operator-selected settings and serialize them as package data; this refactor keeps the current five-setting policy fixed.
|
||||
- If a future package configuration changes a setting’s target, its update flow must migrate the saved uninstall target while preserving the original pre-install value. That migration is not part of this refactor.
|
||||
- The WAPT package source currently has no `WAPT/control` or `update_package.py`; it is code-only and is not yet a complete buildable WAPT package.
|
||||
|
||||
## Safeguards
|
||||
- Initial values saved once in `%ProgramData%\Comitari\HardeningSafe\original-values.json` before any modifications.
|
||||
- Initial values are saved once in `%ProgramData%\Comitari\HardeningSafe\original-values.json` before any modifications. The JSON layout remains compatible with the prior PowerShell package.
|
||||
- If the baseline is absent, malformed or incomplete, uninstall refuses to make registry changes.
|
||||
- Reinstallation idempotent; uninstall conditional: restores only values still equal to package target, leaving externally modified values unchanged.
|
||||
- No reboot forced. Audit implemented.
|
||||
- State persists after uninstall intentionally, so repeated uninstall does not overwrite third-party edits.
|
||||
- Policy owner can override package; audit then reports WARNING.
|
||||
- Audit is read-only. A non-compliant or unreadable setting returns `WARNING`; successful collection with all five values at target returns `OK`.
|
||||
|
||||
## Deployment
|
||||
1. Inspect `setup.py` and `hardening.ps1` in an isolated preproduction repository.
|
||||
1. Inspect `setup.py` and the five `SETTINGS` entries in an isolated preproduction repository.
|
||||
2. Validate registry settings and resulting behavior on a Windows 11 25H2 representative workstation, especially AutoPlay workflow.
|
||||
3. Build/sign WAPT package with organization's key. Do not distribute unsigned sources as a ready-to-deploy binary.
|
||||
4. Deploy to 1 test PC, verify WAPT agent/check-in, LapsByWAPT password rotation and recovery, business apps, USB workflows.
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
param([ValidateSet('install','audit','uninstall')][string]$Action = 'audit')
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$stateDir = Join-Path $env:ProgramData 'Comitari\HardeningSafe'
|
||||
$stateFile = Join-Path $stateDir 'original-values.json'
|
||||
# Only low-impact, machine-wide controls. Deliberately excludes Windows LAPS, accounts,
|
||||
# services, remote management, networking, firewall policy, BitLocker and Defender.
|
||||
$settings = @(
|
||||
@{ Path='HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer'; Name='NoAutorun'; Value=1 },
|
||||
@{ Path='HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer'; Name='NoDriveTypeAutoRun'; Value=255 },
|
||||
@{ Path='HKLM:\SOFTWARE\Policies\Microsoft\Windows\Explorer'; Name='NoAutoplayfornonVolume'; Value=1 },
|
||||
@{ Path='HKLM:\SOFTWARE\Policies\Microsoft\Windows\Personalization'; Name='NoLockScreenCamera'; Value=1 },
|
||||
@{ Path='HKLM:\SOFTWARE\Policies\Microsoft\Windows\Personalization'; Name='NoLockScreenSlideshow'; Value=1 }
|
||||
)
|
||||
function Read-Value($path, $name) {
|
||||
if (-not (Test-Path -LiteralPath $path)) { return @{ Exists=$false; Value=$null } }
|
||||
$key = Get-Item -LiteralPath $path
|
||||
if ($key.GetValueNames() -contains $name) {
|
||||
return @{ Exists=$true; Value=[int]$key.GetValue($name) }
|
||||
}
|
||||
return @{ Exists=$false; Value=$null }
|
||||
}
|
||||
function Write-Value($path, $name, [int]$value) {
|
||||
if (-not (Test-Path -LiteralPath $path)) { New-Item -Path $path -Force | Out-Null }
|
||||
New-ItemProperty -LiteralPath $path -Name $name -Value $value -PropertyType DWord -Force | Out-Null
|
||||
}
|
||||
if ($Action -eq 'install') {
|
||||
if (-not (Test-Path -LiteralPath $stateDir)) { New-Item -Path $stateDir -ItemType Directory -Force | Out-Null }
|
||||
if (-not (Test-Path -LiteralPath $stateFile)) {
|
||||
$original = @()
|
||||
foreach ($setting in $settings) {
|
||||
$old = Read-Value $setting.Path $setting.Name
|
||||
$original += [pscustomobject]@{ Path=$setting.Path; Name=$setting.Name; Exists=$old.Exists; Value=$old.Value; Target=$setting.Value }
|
||||
}
|
||||
$json = ConvertTo-Json -InputObject @($original) -Depth 5
|
||||
$temp = "$stateFile.tmp"
|
||||
[System.IO.File]::WriteAllText($temp, $json, [System.Text.UTF8Encoding]::new($false))
|
||||
Move-Item -LiteralPath $temp -Destination $stateFile -Force
|
||||
}
|
||||
foreach ($setting in $settings) { Write-Value $setting.Path $setting.Name $setting.Value }
|
||||
Write-Output 'Installed conservative hardening; LapsByWAPT untouched.'
|
||||
}
|
||||
elseif ($Action -eq 'audit') {
|
||||
$failed = @()
|
||||
foreach ($setting in $settings) {
|
||||
$actual = Read-Value $setting.Path $setting.Name
|
||||
if (-not $actual.Exists -or $actual.Value -ne $setting.Value) { $failed += "$($setting.Name) != $($setting.Value)" }
|
||||
}
|
||||
if ($failed.Count -gt 0) { $failed | ForEach-Object { Write-Output $_ }; exit 1 }
|
||||
Write-Output 'All 5 settings compliant.'
|
||||
}
|
||||
else {
|
||||
if (-not (Test-Path -LiteralPath $stateFile)) { throw 'No saved baseline; refusing blind rollback.' }
|
||||
$original = @(Get-Content -LiteralPath $stateFile -Raw | ConvertFrom-Json)
|
||||
foreach ($item in $original) {
|
||||
$current = Read-Value $item.Path $item.Name
|
||||
# Do not override later changes made by GPO or another administrator.
|
||||
if (-not $current.Exists -or $current.Value -ne [int]$item.Target) {
|
||||
Write-Output "Skipped externally changed setting: $($item.Name)"
|
||||
continue
|
||||
}
|
||||
if ($item.Exists) { Write-Value $item.Path $item.Name ([int]$item.Value) }
|
||||
else { Remove-ItemProperty -LiteralPath $item.Path -Name $item.Name -ErrorAction Stop }
|
||||
}
|
||||
# Keep baseline for forensic trace; uninstall is idempotent only for settings still matching targets.
|
||||
Write-Output 'Conditional rollback completed; LapsByWAPT untouched.'
|
||||
}
|
||||
@@ -1,33 +1,185 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Comitari conservative Windows hardening package.
|
||||
|
||||
Registry operations use WAPT setuphelpers only. The supported settings are
|
||||
kept as data in SETTINGS so a later update_package.py can validate operator
|
||||
choices and serialize an approved configuration without generating code.
|
||||
"""
|
||||
|
||||
from setuphelpers import *
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
uninstallkey = []
|
||||
|
||||
STATE_DIRECTORY = ('Comitari', 'HardeningSafe')
|
||||
STATE_FILENAME = 'original-values.json'
|
||||
|
||||
def _run(action):
|
||||
script = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'hardening.ps1')
|
||||
result = subprocess.run(['powershell.exe', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', script, '-Action', action], capture_output=True, text=True)
|
||||
print(result.stdout)
|
||||
if result.stderr:
|
||||
print(result.stderr)
|
||||
if result.returncode != 0:
|
||||
raise Exception('Hardening %s failed with code %s' % (action, result.returncode))
|
||||
# Intentionally limited machine-wide controls. Keep state-file paths in the
|
||||
# legacy PowerShell format so existing installations can roll back safely.
|
||||
SETTINGS = (
|
||||
{'Path': r'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer',
|
||||
'Name': 'NoAutorun', 'Value': 1},
|
||||
{'Path': r'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer',
|
||||
'Name': 'NoDriveTypeAutoRun', 'Value': 255},
|
||||
{'Path': r'HKLM:\SOFTWARE\Policies\Microsoft\Windows\Explorer',
|
||||
'Name': 'NoAutoplayfornonVolume', 'Value': 1},
|
||||
{'Path': r'HKLM:\SOFTWARE\Policies\Microsoft\Windows\Personalization',
|
||||
'Name': 'NoLockScreenCamera', 'Value': 1},
|
||||
{'Path': r'HKLM:\SOFTWARE\Policies\Microsoft\Windows\Personalization',
|
||||
'Name': 'NoLockScreenSlideshow', 'Value': 1},
|
||||
)
|
||||
|
||||
|
||||
def _state_path():
|
||||
return makepath(programdata(), *STATE_DIRECTORY, STATE_FILENAME)
|
||||
|
||||
|
||||
def _registry_path(path):
|
||||
prefix = 'HKLM:\\'
|
||||
if not isinstance(path, str) or not path.startswith(prefix):
|
||||
raise RuntimeError('Refusing an unsupported registry root in package state.')
|
||||
return path[len(prefix):]
|
||||
|
||||
|
||||
def _read_value(path, name):
|
||||
"""Read one HKLM value without hiding access or registry errors."""
|
||||
subkey = _registry_path(path)
|
||||
if not reg_key_exists(HKEY_LOCAL_MACHINE, subkey):
|
||||
return {'Exists': False, 'Value': None}
|
||||
if not reg_value_exists(HKEY_LOCAL_MACHINE, subkey, name):
|
||||
return {'Exists': False, 'Value': None}
|
||||
with reg_openkey_noredir(HKEY_LOCAL_MACHINE, subkey) as key:
|
||||
value = reg_getvalue(key, name)
|
||||
if value is None:
|
||||
raise RuntimeError('Registry value disappeared while it was being read: %s' % name)
|
||||
try:
|
||||
return {'Exists': True, 'Value': int(value)}
|
||||
except (TypeError, ValueError):
|
||||
raise RuntimeError('Registry value is not an integer DWORD: %s' % name)
|
||||
|
||||
|
||||
def _write_value(path, name, value):
|
||||
registry_set(HKEY_LOCAL_MACHINE, _registry_path(path), name,
|
||||
int(value), type=REG_DWORD)
|
||||
|
||||
|
||||
def _delete_value(path, name):
|
||||
subkey = _registry_path(path)
|
||||
if not reg_key_exists(HKEY_LOCAL_MACHINE, subkey):
|
||||
return
|
||||
with reg_openkey_noredir(HKEY_LOCAL_MACHINE, subkey,
|
||||
sam=KEY_ALL_ACCESS) as key:
|
||||
reg_delvalue(key, name)
|
||||
|
||||
|
||||
def _capture_original_values():
|
||||
original = []
|
||||
for setting in SETTINGS:
|
||||
current = _read_value(setting['Path'], setting['Name'])
|
||||
original.append({
|
||||
'Path': setting['Path'],
|
||||
'Name': setting['Name'],
|
||||
'Exists': current['Exists'],
|
||||
'Value': current['Value'],
|
||||
'Target': setting['Value'],
|
||||
})
|
||||
return original
|
||||
|
||||
|
||||
def _write_state_once(state_file, state):
|
||||
"""Persist the rollback baseline before changing any registry value."""
|
||||
state_dir = os.path.dirname(state_file)
|
||||
mkdirs(state_dir)
|
||||
temporary = state_file + '.tmp'
|
||||
try:
|
||||
json_write_file(temporary, state, indent=2, sort_keys=True)
|
||||
# Same-volume replace keeps a prior baseline intact until the new file
|
||||
# is complete. WAPT serializes package installations on each endpoint.
|
||||
os.replace(temporary, state_file)
|
||||
finally:
|
||||
if isfile(temporary):
|
||||
remove_file(temporary)
|
||||
|
||||
|
||||
def _load_state(state_file):
|
||||
if not isfile(state_file):
|
||||
raise RuntimeError('No saved baseline; refusing blind rollback.')
|
||||
try:
|
||||
state = json_load_file(state_file)
|
||||
except Exception as exc:
|
||||
raise RuntimeError('Saved baseline is unreadable; refusing registry changes.') from exc
|
||||
if not isinstance(state, list) or len(state) != len(SETTINGS):
|
||||
raise RuntimeError('Saved baseline is incomplete; refusing registry changes.')
|
||||
|
||||
expected = {(item['Path'], item['Name']): item for item in SETTINGS}
|
||||
seen = set()
|
||||
for item in state:
|
||||
if not isinstance(item, dict):
|
||||
raise RuntimeError('Saved baseline has an invalid entry; refusing registry changes.')
|
||||
key = (item.get('Path'), item.get('Name'))
|
||||
setting = expected.get(key)
|
||||
if setting is None or key in seen:
|
||||
raise RuntimeError('Saved baseline contains an unsupported setting; refusing registry changes.')
|
||||
seen.add(key)
|
||||
target = item.get('Target')
|
||||
if isinstance(target, bool) or not isinstance(target, int) or target != setting['Value']:
|
||||
raise RuntimeError('Saved baseline target differs from this package; refusing registry changes.')
|
||||
exists = item.get('Exists')
|
||||
value = item.get('Value')
|
||||
if not isinstance(exists, bool):
|
||||
raise RuntimeError('Saved baseline has an invalid existence flag; refusing registry changes.')
|
||||
if exists and (isinstance(value, bool) or not isinstance(value, int)):
|
||||
raise RuntimeError('Saved baseline has an invalid original DWORD; refusing registry changes.')
|
||||
if not exists and value is not None:
|
||||
raise RuntimeError('Saved baseline has an inconsistent absent value; refusing registry changes.')
|
||||
if seen != set(expected):
|
||||
raise RuntimeError('Saved baseline is missing settings; refusing registry changes.')
|
||||
return state
|
||||
|
||||
|
||||
def install():
|
||||
_run('install')
|
||||
"""Save the pre-package state once, then apply the five approved DWORDs."""
|
||||
state_file = _state_path()
|
||||
if not isfile(state_file):
|
||||
original = _capture_original_values()
|
||||
_write_state_once(state_file, original)
|
||||
|
||||
for setting in SETTINGS:
|
||||
_write_value(setting['Path'], setting['Name'], setting['Value'])
|
||||
print('Installed conservative hardening; LapsByWAPT untouched.')
|
||||
|
||||
|
||||
def audit():
|
||||
"""Read-only compliance check for the configured package settings."""
|
||||
try:
|
||||
_run('audit')
|
||||
failed = []
|
||||
for setting in SETTINGS:
|
||||
actual = _read_value(setting['Path'], setting['Name'])
|
||||
if not actual['Exists'] or actual['Value'] != setting['Value']:
|
||||
failed.append('%s != %s' % (setting['Name'], setting['Value']))
|
||||
if failed:
|
||||
for item in failed:
|
||||
print(item)
|
||||
return 'WARNING'
|
||||
print('All %s settings compliant.' % len(SETTINGS))
|
||||
return 'OK'
|
||||
except Exception as exc:
|
||||
print(exc)
|
||||
print('Hardening audit failed: %s' % type(exc).__name__)
|
||||
return 'WARNING'
|
||||
|
||||
|
||||
def uninstall():
|
||||
_run('uninstall')
|
||||
"""Conditionally restore only values still owned by this package."""
|
||||
state = _load_state(_state_path())
|
||||
for item in state:
|
||||
current = _read_value(item['Path'], item['Name'])
|
||||
if not current['Exists'] or current['Value'] != int(item['Target']):
|
||||
print('Skipped externally changed setting: %s' % item['Name'])
|
||||
continue
|
||||
if item['Exists']:
|
||||
_write_value(item['Path'], item['Name'], item['Value'])
|
||||
else:
|
||||
_delete_value(item['Path'], item['Name'])
|
||||
# Preserve the baseline as a forensic record, matching the previous
|
||||
# PowerShell implementation and allowing repeated uninstall safely.
|
||||
print('Conditional rollback completed; LapsByWAPT untouched.')
|
||||
@@ -0,0 +1,163 @@
|
||||
"""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()
|
||||
Reference in new issue
Block a user