186 lines
7.3 KiB
Python
186 lines
7.3 KiB
Python
# -*- 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
|
|
|
|
uninstallkey = []
|
|
|
|
STATE_DIRECTORY = ('Comitari', 'HardeningSafe')
|
|
STATE_FILENAME = 'original-values.json'
|
|
|
|
# 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():
|
|
"""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:
|
|
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('Hardening audit failed: %s' % type(exc).__name__)
|
|
return 'WARNING'
|
|
|
|
|
|
def 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.')
|