feat(audit): publish structured HardeningKitty JSON

This commit is contained in:
PierreCsn committed 2026-10-29 09:03:53 +01:00
1 parent 349b95fbc6
commit f60d102233
7 files changed
+1033 -116

No files matched your search

+77 -30
View File
@@ -130,7 +130,7 @@ class PackageTests(unittest.TestCase):
self.assertEqual(counts['unknown'], 1)
self.assertEqual(pkg._status_from_counts(counts, {'error_on_high': True, 'warn_on_low': True}), 'ERROR')
self.assertEqual(len(findings), 2)
self.assertEqual(len(pkg._shard_findings(findings)), 16)
self.assertEqual(counts['expected'], 3)
def test_only_reviewed_conditional_exclusions_leave_coverage_denominator(self):
with tempfile.TemporaryDirectory() as tmp:
@@ -194,23 +194,36 @@ class PackageTests(unittest.TestCase):
_, findings2 = pkg._read_report(str(report), True)
self.assertEqual(findings2[0]['current'], 'private_value')
def test_reporting_payload_is_smaller_than_raw_report_without_sensitive_values(self):
def test_reporting_payload_size_is_measured_without_sensitive_values(self):
with tempfile.TemporaryDirectory() as tmp:
report = Path(tmp, 'report.csv')
baseline = Path(tmp, 'baseline.csv')
rows = [['%04d' % i, 'Synthetic category', 'Synthetic control %04d' % i,
'Low', 'current-setting-' * 8, 'recommended-setting-' * 8, 'Failed']
for i in range(500)]
report.write_text(csv_content(rows), encoding='utf-8')
baseline.write_text(csv_content([
[row[0], row[1], row[2], 'Low', '', row[5], ''] for row in rows
]), encoding='utf-8')
raw_bytes = report.stat().st_size
_, compact = pkg._read_report(str(report), publish_current_values=False)
_, expanded = pkg._read_report(str(report), publish_current_values=True)
compact_bytes = sum(len(json.dumps(bucket, ensure_ascii=False, separators=(',', ':')).encode('utf-8'))
for bucket in pkg._shard_findings(compact))
expanded_bytes = sum(len(json.dumps(bucket, ensure_ascii=False, separators=(',', ':')).encode('utf-8'))
for bucket in pkg._shard_findings(expanded))
self.assertEqual(len(compact), 500)
self.assertLess(compact_bytes, raw_bytes)
counts, findings = pkg._read_report(str(report), publish_current_values=False)
counts, findings = pkg._check_coverage(str(baseline), str(report), counts, findings)
summary = dict(counts, status='WARNING', source='GPO', upstream_commit=pkg.UPSTREAM_COMMIT,
last_run_utc='2026-10-29T00:00:00+00:00', benchmark='synthetic.csv',
os_caption='Synthetic Windows', os_build='1', display_version='synthetic')
compact = pkg._build_audit_document(summary, findings, str(report), str(baseline), {})
expanded = pkg._build_audit_document(summary, findings, str(report), str(baseline),
{'publish_current_values': True})
compact_bytes = len(json.dumps(compact, ensure_ascii=False, separators=(',', ':')).encode('utf-8'))
expanded_bytes = len(json.dumps(expanded, ensure_ascii=False, separators=(',', ':')).encode('utf-8'))
self.assertEqual(len(compact['controls']), 500)
# A complete structured schema is richer than the CSV; record both
# measured sizes instead of claiming it is smaller than source.
self.assertGreater(compact_bytes, 0)
self.assertGreater(raw_bytes, 0)
self.assertGreater(expanded_bytes, compact_bytes)
self.assertLessEqual(compact_bytes, pkg.MAX_AUDIT_JSON_BYTES)
self.assertNotIn('current', compact['controls']['0000'])
def test_ignored_not_applicable_and_unknown_are_not_compliant(self):
with tempfile.TemporaryDirectory() as tmp:
@@ -305,38 +318,68 @@ class PackageTests(unittest.TestCase):
finally:
pkg._package_source, pkg.INSTALL_ROOT, pkg.REPORT_ROOT = old
def test_published_shards_and_keys(self):
events = {}
def test_structured_audit_publishes_one_native_json_key(self):
events = []
class FakeWapt:
def write_audit_data_if_changed(self, section, key, value, **kwargs):
events[key] = value
events.append((section, key, value, kwargs))
def write_audit_data(self, section, key, value, **kwargs):
events[key] = value
events.append((section, key, value, kwargs))
old = getattr(pkg, 'WAPT', None)
try:
pkg.WAPT = FakeWapt()
summary = {k: '1' for k in ('status', 'benchmark', 'os_build', 'source',
'upstream_commit', 'passed', 'high', 'medium', 'low', 'unknown',
'ignored', 'not_applicable', 'excluded', 'os_caption', 'display_version',
'total', 'expected', 'executed', 'skipped', 'compliant_percent', 'last_run_utc', 'last_error')}
pkg._publish_audit(summary, [dict(id='42', name='Enabled', category='Firewall',
severity='high', result='Failed')], {'publish_to_wapt': True})
self.assertEqual(len([k for k in events if k.startswith('findings_')]), 16)
self.assertEqual(sum(len(json.loads(v)) for k, v in events.items() if k.startswith('findings_')), 1)
self.assertIn('status', events)
document = {'schema_version': 2, 'status': 'WARNING', 'controls': {'42': {'status': 'failed'}}}
pkg._publish_audit(document, {'publish_to_wapt': True})
self.assertEqual(len(events), 1)
section, key, value, kwargs = events[0]
self.assertEqual((section, key), ('hardeningkitty', 'hardeningkitty'))
self.assertIs(value, document)
self.assertEqual(kwargs['max_count'], 2)
finally:
if old is None:
del pkg.WAPT
else:
pkg.WAPT = old
def test_failed_audit_updates_wapt_status(self):
events = {}
def test_console_template_matches_schema_and_uses_escaped_tags(self):
template = (ROOT / 'console-templates' /
'host_audit_hardeningkitty_hardeningkitty.html').read_text(encoding='utf-8')
self.assertIn('{{#Items categories}}', template)
self.assertIn('{{#Items controls}}', template)
self.assertIn('{{error.code}}', template)
self.assertIn('{{error.description}}', template)
self.assertIn('{{summary.expected}}', template)
self.assertNotIn('{{{', template)
self.assertNotIn('<script', template.lower())
def test_structured_error_does_not_publish_exception_details(self):
events = []
class FakeWapt:
def write_audit_data_if_changed(self, section, key, value, **kwargs):
events[key] = value
events.append((section, key, value))
old_wapt = getattr(pkg, 'WAPT', None)
try:
pkg.WAPT = FakeWapt()
pkg._publish_failure(RuntimeError('private path or setting value'))
self.assertEqual(len(events), 1)
section, key, payload = events[0]
self.assertEqual((section, key), ('hardeningkitty', 'hardeningkitty'))
self.assertEqual(payload['error']['code'], 'AUDIT_FAILED')
self.assertNotIn('private path', json.dumps(payload))
self.assertNotIn('setting value', json.dumps(payload))
finally:
if old_wapt is None:
del pkg.WAPT
else:
pkg.WAPT = old_wapt
def test_failed_audit_updates_wapt_status(self):
events = []
class FakeWapt:
def write_audit_data_if_changed(self, section, key, value, **kwargs):
events.append((section, key, value, kwargs))
def write_audit_data(self, section, key, value, **kwargs):
events[key] = value
events.append((section, key, value, kwargs))
old_wapt = getattr(pkg, 'WAPT', None)
old_audit = pkg._audit_impl
try:
@@ -345,9 +388,13 @@ class PackageTests(unittest.TestCase):
raise RuntimeError('Missing PowerShell')
pkg._audit_impl = fails
self.assertEqual(pkg.audit(), 'ERROR')
self.assertEqual(events['status'], 'ERROR')
self.assertEqual(events['last_error'], 'Collection failed: RuntimeError')
self.assertIn('last_run_utc', events)
self.assertEqual(len(events), 1)
section, key, payload, _ = events[0]
self.assertEqual((section, key), ('hardeningkitty', 'hardeningkitty'))
self.assertEqual(payload['status'], 'ERROR')
self.assertEqual(payload['error']['code'], 'AUDIT_FAILED')
self.assertIn('no current compliance assessment', payload['error']['description'])
self.assertNotIn('findings_00', [event[1] for event in events])
finally:
pkg._audit_impl = old_audit
if old_wapt is None: