fix(audit): harden HardeningKitty packaging and reporting

This commit is contained in:
PierreCsn committed 2026-10-28 13:28:48 +01:00
1 parent f4a67f3e1b
commit 91eb704215
10 files changed
+1171 -236

No files matched your search

+54 -5
View File
@@ -13,6 +13,7 @@ from pathlib import Path
import shutil
import sys
import tempfile
import uuid
import urllib.request
import zipfile
@@ -31,7 +32,19 @@ REQUIRED = {
'lists/finding_list_0x6d69636b_machine.csv',
'lists/finding_list_msft_security_baseline_windows_11_24h2_machine.csv',
'lists/finding_list_msft_security_baseline_windows_11_25h2_machine.csv',
'lists/finding_list_msft_security_baseline_windows_11_21h2_machine.csv',
'lists/finding_list_msft_security_baseline_windows_11_22h2_machine.csv',
'lists/finding_list_msft_security_baseline_windows_11_23h2_machine.csv',
'lists/finding_list_msft_security_baseline_windows_10_22h2_machine.csv',
'lists/finding_list_msft_security_baseline_windows_10_20h2_21h1_machine.csv',
'lists/finding_list_msft_security_baseline_windows_10_21h2_machine.csv',
'lists/finding_list_cis_microsoft_windows_server_2012_r2_3.0.0_machine.csv',
'lists/finding_list_cis_microsoft_windows_server_2016_1607_3.0.0_machine.csv',
'lists/finding_list_cis_microsoft_windows_server_2019_1809_3.0.0_machine.csv',
'lists/finding_list_msft_security_baseline_windows_server_2022_21h2_dc_machine.csv',
'lists/finding_list_msft_security_baseline_windows_server_2022_21h2_member_machine.csv',
'lists/finding_list_msft_security_baseline_windows_server_2025_24h2_dc_machine.csv',
'lists/finding_list_msft_security_baseline_windows_server_2025_24h2_member_machine.csv',
}
@@ -63,6 +76,8 @@ def _wanted(relative):
def vendor_from_archive(archive_bytes, destination):
root = Path(destination)
if root.is_symlink() or (root.exists() and not root.is_dir()):
raise RuntimeError('Vendor destination must be a real directory, not a symlink or file.')
root.parent.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(prefix='vendor-tmp-', dir=str(root.parent)) as tmp:
files = {}
@@ -109,10 +124,42 @@ def vendor_from_archive(archive_bytes, destination):
'files': dict(sorted(files.items())),
}
Path(tmp, 'vendor-manifest.json').write_text(json.dumps(manifest, indent=2, sort_keys=True) + '\n', encoding='utf-8')
# Preserve a previous verified vendor folder when the download fails.
if root.exists():
shutil.rmtree(root)
shutil.move(tmp, root)
if root.is_dir():
try:
current = json.loads((root / 'vendor-manifest.json').read_text(encoding='utf-8'))
current_files = {path.relative_to(root).as_posix() for path in root.rglob('*') if path.is_file()}
intact = (current == manifest and
current_files == set(manifest['files']) | {'vendor-manifest.json'} and all(
(root.joinpath(*name.split('/')).is_file() and
sha256(root.joinpath(*name.split('/')).read_bytes()) == digest)
for name, digest in manifest['files'].items()))
if intact:
return manifest
except (OSError, ValueError):
pass
# Validate the complete candidate before replacing the current assets.
candidate = Path(tmp)
candidate_manifest = json.loads((candidate / 'vendor-manifest.json').read_text(encoding='utf-8'))
for name, expected in candidate_manifest['files'].items():
if sha256(candidate.joinpath(*name.split('/')).read_bytes()) != expected:
raise RuntimeError('Candidate vendor verification failed: %s' % name)
backup = root.with_name(root.name + '.previous-' + uuid.uuid4().hex)
moved_old = False
try:
if root.exists():
root.rename(backup)
moved_old = True
candidate.rename(root)
except Exception:
if moved_old and not root.exists() and backup.exists():
backup.rename(root)
raise
else:
if backup.exists():
try:
shutil.rmtree(backup)
except OSError as exc:
print('Previous vendor backup retained at %s: %s' % (backup, type(exc).__name__), file=sys.stderr)
return manifest
@@ -120,9 +167,11 @@ def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--dest', default='vendor', help='Directory to populate in the WAPT package sources')
parser.add_argument('--archive', help='Use a locally downloaded GitHub archive for offline preparation')
parser.add_argument('--archive-sha256', help='Optional expected hash of the local ZIP for offline verification')
parser.add_argument('--archive-sha256', help='Required independently obtained hash of the local ZIP')
args = parser.parse_args(argv)
if args.archive:
if not args.archive_sha256:
raise RuntimeError('--archive-sha256 is required for offline archive preparation.')
data = Path(args.archive).read_bytes()
if len(data) > MAX_ARCHIVE_BYTES:
raise RuntimeError('Offline archive exceeds size limit.')