fix(audit): harden HardeningKitty packaging and reporting
This commit is contained in:
1 parent
f4a67f3e1b
commit
91eb704215
10 files changed
+1171
-236
No files matched your search
+54
-5
@@ -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.')
|
||||
|
||||
Reference in new issue
Block a user