fix(package): apply setup ACL correction

This commit is contained in:
PierreCsn committed 2026-10-28 18:17:40 +01:00
1 parent f817d41189
commit 349b95fbc6
3 files changed
+46 -53

No files matched your search

+25 -47
View File
@@ -65,7 +65,7 @@ def _package_source():
root = os.path.dirname(os.path.abspath(__file__)) if globals().get('__file__') else None
if not root:
raise RuntimeError('WAPT basedir is not available.')
root = os.path.relpath(os.path.abspath(root), os.getcwd())
root = os.path.abspath(root)
if not isfile(makepath(root, 'runner.ps1')) or not isfile(makepath(root, 'audit_config.json')):
raise RuntimeError('WAPT package source is missing runner.ps1 or audit_config.json: %s' % root)
return root
@@ -85,7 +85,7 @@ def _safe_vendor_name(name):
all(part not in ('', '.', '..') for part in name.split('/')))
def _verify_vendor(root, runtime=False, allow_extra=False):
def _verify_vendor(root, runtime=False):
"""Verify all publisher payload hashes and reject unexpected files/symlinks."""
manifest_file = makepath(root, 'vendor-manifest.json')
if not isfile(manifest_file):
@@ -131,13 +131,13 @@ def _verify_vendor(root, runtime=False, allow_extra=False):
raise RuntimeError('HardeningKitty directory contains a symlink; refusing it.')
for name in names:
actual.add(os.path.relpath(makepath(directory, name), root).replace(os.sep, '/'))
if not expected.issubset(actual) or (not allow_extra and actual != expected):
if actual != expected:
raise RuntimeError('Unexpected/missing files in HardeningKitty directory: %s' %
', '.join(sorted(actual.symmetric_difference(expected))[:10]))
return manifest
def _owned_install(root, allow_extra=False):
def _owned_install(root):
"""Never replace/delete a directory belonging to someone else."""
if os.path.islink(root) or not isdir(root):
return False
@@ -148,54 +148,29 @@ def _owned_install(root, allow_extra=False):
if stream.read().strip() != PACKAGE_NAME:
return False
# Legacy upgrade without owner marker: require intact original payload.
_verify_vendor(root, runtime=True, allow_extra=allow_extra)
_verify_vendor(root, runtime=True)
return True
except Exception:
return False
def _remove_owned_install_files(root):
"""Remove only verified package files, keeping unrelated operator data."""
manifest = _verify_vendor(root, runtime=True, allow_extra=True)
managed = set(manifest['files'])
managed.update(('vendor-manifest.json',) + RUNTIME_FILES + LEGACY_FILES + (OWNER_MARKER,))
root_real = os.path.realpath(root)
for relative in sorted(managed, key=lambda value: value.count('/'), reverse=True):
if not _safe_vendor_name(relative):
raise RuntimeError('Unsafe installed manifest path; refusing removal.')
path = makepath(root, *relative.split('/'))
resolved = os.path.realpath(path)
if not resolved.startswith(root_real + os.sep):
raise RuntimeError('Installed manifest path escapes package root; refusing removal.')
if os.path.isfile(path) and not os.path.islink(path):
os.unlink(path)
parent = os.path.dirname(path)
while parent.startswith(root_real + os.sep):
try:
os.rmdir(parent)
except OSError:
break
parent = os.path.dirname(parent)
for current, folders, names in os.walk(root, topdown=False, followlinks=False):
for folder in folders:
path = makepath(current, folder)
if not os.path.islink(path):
try:
os.rmdir(path)
except OSError:
pass
try:
os.rmdir(root)
except OSError:
print('Preserved non-package data remaining in %s.' % root)
def _secure_directory(path):
"""Apply SID-based ACLs independent of Windows display language."""
"""Protect this directory; let its children inherit SYSTEM/Admin rights.
Do not use /inheritance:r /T on every file. That removes inherited
permissions from each individual file and is unnecessary for a freshly
staged directory whose children inherit from its protected parent.
"""
if os.name != 'nt':
return
run(['icacls.exe', path, '/inheritance:r', '/grant:r',
'*S-1-5-18:(OI)(CI)F', '*S-1-5-32-544:(OI)(CI)F', '/T', '/Q'],
# Grant first: avoid losing the installer's access before the new ACEs exist.
# OI/CI make newly created files and subdirectories inherit the grants.
run(['icacls.exe', path, '/grant:r',
'*S-1-5-18:(OI)(CI)F', '*S-1-5-32-544:(OI)(CI)F'],
shell=False, timeout=120, accept_returncodes=[0])
# Only protect the directory itself. Existing descendants keep inheriting
# from it; unlike /T, this does not strip every file's inherited ACEs.
run(['icacls.exe', path, '/inheritance:r'],
shell=False, timeout=120, accept_returncodes=[0])
@@ -221,6 +196,9 @@ def install():
with open(makepath(staging, OWNER_MARKER), 'w', encoding='ascii') as stream:
stream.write(PACKAGE_NAME + '\n')
_secure_directory(staging)
# A security operation can change ACLs. Verify readability and all
# checksums AFTER the final ACL change, before replacing a working install.
_verify_vendor(staging, runtime=True)
mkdirs(REPORT_ROOT)
_secure_directory(REPORT_ROOT)
if isdir(INSTALL_ROOT):
@@ -247,9 +225,9 @@ def install():
def uninstall():
if os.path.lexists(INSTALL_ROOT):
if not _owned_install(INSTALL_ROOT, allow_extra=True):
if not _owned_install(INSTALL_ROOT):
raise RuntimeError('Uninstall refused: directory is unverified or not owned by this package.')
_remove_owned_install_files(INSTALL_ROOT)
remove_tree(INSTALL_ROOT)
print('HardeningKitty removed. Historical reports kept: %s' % REPORT_ROOT)
@@ -505,7 +483,7 @@ def _audit_impl():
# run() is WAPT's process helper and handles timeouts and exit statuses.
# No subprocess, shell interpolation, or endpoint Internet access.
command = [_powershell_executable(), '-NoLogo', '-NoProfile', '-NonInteractive',
'-File', makepath(INSTALL_ROOT, 'runner.ps1'),
'-ExecutionPolicy', 'Bypass', '-File', makepath(INSTALL_ROOT, 'runner.ps1'),
'-InstallRoot', INSTALL_ROOT, '-CsvFile', csv_file,
'-MetadataFile', metadata_file, '-Source', config['source'], '-Profile', profile]
with open(log_file, 'w', encoding='utf-8') as log: