From 792dd928b11c17ad4c208993914d0f23c492a033 Mon Sep 17 00:00:00 2001 From: Fabio Boi Date: Sun, 4 Oct 2026 14:40:08 +0200 Subject: [PATCH] Fix CISA access and publish scanner recovery/status events --- docker-security/scan.py | 50 ++++++++++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/docker-security/scan.py b/docker-security/scan.py index b3b9afb..85a7aed 100644 --- a/docker-security/scan.py +++ b/docker-security/scan.py @@ -20,6 +20,29 @@ CONFIG = Path('/etc/wazuh-docker-security/config.json') LOG = Path('/var/log/wazuh-docker-security/events.jsonl') SOCKET = 'unix:///var/run/docker.sock' KEV_URL = 'https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json' +KEV_URLS = (KEV_URL, 'https://raw.githubusercontent.com/cisagov/kev-data/main/known_exploited_vulnerabilities.json') + + +def fetch_kev(url, now): + """Only publisher-controlled sources; reject invalid/old mirror snapshots.""" + request = urllib.request.Request(url, headers={'User-Agent': 'wazuh-docker-security/1'}) + with urllib.request.urlopen(request, timeout=15) as response: + raw = response.read(12 * 1024 * 1024 + 1) + if len(raw) > 12 * 1024 * 1024: + raise ValueError('KEV response too large') + document = json.loads(raw) + published = dt.datetime.fromisoformat(document['dateReleased'].replace('Z', '+00:00')).timestamp() + if not -86400 <= now - published <= 14 * 86400: + raise ValueError('KEV publication timestamp outside freshness window') + entries = document['vulnerabilities'] + if document.get('count') != len(entries): + raise ValueError('incomplete KEV catalog') + ids = sorted({v['cveID'] for v in entries + if re.fullmatch(r'CVE-\d{4}-\d{4,}', v.get('cveID', ''))}) + if not ids or len(ids) != len(entries): + raise ValueError('empty or invalid KEV catalog') + return {'fetched_at': now, 'published_at': published, 'ids': ids, + 'catalog_version': document.get('catalogVersion'), 'source': url} def atomic_json(path, data): @@ -88,21 +111,16 @@ def kev_catalog(): now = time.time() error = None if now - cached.get('fetched_at', 0) >= 86400: - try: - request = urllib.request.Request(KEV_URL, headers={'User-Agent': 'wazuh-docker-security/1'}) - with urllib.request.urlopen(request, timeout=30) as response: - raw = response.read(12 * 1024 * 1024 + 1) - if len(raw) > 12 * 1024 * 1024: - raise ValueError('KEV response too large') - document = json.loads(raw) - ids = sorted({v['cveID'] for v in document['vulnerabilities'] - if re.fullmatch(r'CVE-\d{4}-\d{4,}', v.get('cveID', ''))}) - if not ids: - raise ValueError('empty KEV catalog') - cached = {'fetched_at': now, 'ids': ids} - atomic_json(path, cached) - except Exception: - error = 'KEV refresh failed; cached information may be incomplete' + for url in KEV_URLS: + try: + updated = fetch_kev(url, now) + atomic_json(path, updated) + cached = updated + break + except Exception: + continue + else: + error = 'KEV refresh failed on both official sources; cached information may be incomplete' fresh = now - cached.get('fetched_at', 0) <= 7 * 86400 if not fresh: error = 'KEV catalog unavailable or older than seven days' @@ -237,6 +255,8 @@ def main(): for key in state['health']: if key not in errors: events.append({'event': 'scan_recovered', 'component': key}) + events.append({'event': 'scan_status', 'complete': 'yes' if inventory_ok and not any(k.startswith('image:') for k in errors) else 'no', + 'kev_fresh': 'yes' if fresh else 'no', 'problem_count': len(errors)}) append_events(events) atomic_json(state_path, {'findings': merged, 'health': errors, 'last_scan': time.time()}) atomic_json(BASE / 'inventory.json', {'updated_at': time.time(), 'scope': scope,