Files

216 lines
8.9 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 = []
# Relative subdirectories under ProgramData used for this package's rollback state.
STATE_DIRECTORY = ('Comitari', 'HardeningSafe')
# Stores original registry values plus their package target for conditional rollback.
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.
# Purpose explains what the policy does; Rationale records why this package
# applies it. These fields are descriptive and do not affect registry writes.
SETTINGS = (
{
'Path': r'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer',
'Name': 'NoAutorun',
'Value': 1,
'Purpose': 'Disable AutoRun commands for the computer.',
'Rationale': 'AutoRun can launch commands associated with removable media. Disabling it reduces the chance that connecting media starts a command without an explicit user action.',
},
{
'Path': r'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer',
'Name': 'NoDriveTypeAutoRun',
'Value': 255,
'Purpose': 'Disable AutoPlay for all drive types covered by the Windows policy mask.',
'Rationale': 'The 0xFF mask applies the policy to every drive type represented by this setting, reducing automatic handling of newly connected or mounted media.',
},
{
'Path': r'HKLM:\SOFTWARE\Policies\Microsoft\Windows\Explorer',
'Name': 'NoAutoplayfornonVolume',
'Value': 1,
'Purpose': 'Disable AutoPlay for devices that do not expose a volume.',
'Rationale': 'The drive-type policy does not cover every device category. This additional policy closes that gap for non-volume devices.',
},
{
'Path': r'HKLM:\SOFTWARE\Policies\Microsoft\Windows\Personalization',
'Name': 'NoLockScreenCamera',
'Value': 1,
'Purpose': 'Prevent camera use from the Windows lock screen.',
'Rationale': 'The camera remains unavailable from the lock screen, limiting access to this device feature before a user signs in.',
},
{
'Path': r'HKLM:\SOFTWARE\Policies\Microsoft\Windows\Personalization',
'Name': 'NoLockScreenSlideshow',
'Value': 1,
'Purpose': 'Prevent a slideshow from running on the Windows lock screen.',
'Rationale': 'This prevents lock-screen slideshow configuration, avoiding display of rotating images while the computer is locked.',
},
)
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}
missing = object()
with reg_openkey_noredir(HKEY_LOCAL_MACHINE, subkey) as key:
value = reg_getvalue(key, name, default=missing)
if value is missing:
return {'Exists': False, 'Value': None}
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.')