refactor: inline Windows hardening with WAPT helpers
This commit is contained in:
1 parent
11f99a588f
commit
3a046ef57d
4 files changed
+340
-83
No files matched your search
@@ -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.')
|
||||
Reference in new issue
Block a user