216 lines
8.9 KiB
Python
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.')
|