1- """Scheduled periodic scans of installed HACS components."""
1+ """Scheduled periodic scans and CVE watch for installed HACS components."""
22
33import asyncio
44import logging
77log = logging .getLogger (__name__ )
88
99_task : asyncio .Task | None = None
10+ _cve_task : asyncio .Task | None = None
1011_interval_hours : float = 0
12+ _cve_interval_hours : float = 6
1113_enabled : bool = False
14+ _cve_enabled : bool = False
15+ _last_cve_alerts : dict [str , list [str ]] = {}
1216
1317
1418async def _scheduled_loop ():
@@ -57,6 +61,70 @@ async def _scheduled_loop():
5761 publish_status ("idle" )
5862
5963
64+ async def _cve_watch_loop ():
65+ """Lightweight CVE-only check — queries OSV.dev for known deps without full scan."""
66+ from app .scanner .hacs_list import fetch_installed_hacs , repo_to_url
67+ from app .scanner .fetch import fetch_and_parse
68+ from app .scanner .cve_lookup import check_cve , check_cve_repo
69+ from app .report .mqtt import publish_status
70+ from app .models import ScanJob
71+
72+ global _last_cve_alerts
73+
74+ while True :
75+ await asyncio .sleep (_cve_interval_hours * 3600 )
76+ if not _cve_enabled :
77+ continue
78+
79+ log .info ("CVE watch check starting (interval=%gh)" , _cve_interval_hours )
80+
81+ try :
82+ installed = await fetch_installed_hacs ()
83+ if not installed :
84+ continue
85+
86+ new_alerts : dict [str , list [str ]] = {}
87+ for comp in installed :
88+ url = repo_to_url (comp .get ("full_name" , "" ))
89+ if not url :
90+ continue
91+ name = comp .get ("name" , comp .get ("full_name" , "" ))
92+
93+ try :
94+ job = ScanJob (id = f"cve:{ name [:8 ]} " , repo_url = url , name = name )
95+ repo_path = fetch_and_parse (job )
96+
97+ findings = []
98+ if job .manifest and job .manifest .requirements :
99+ cve_findings = await check_cve (job .manifest )
100+ findings .extend (cve_findings )
101+ repo_cve = await check_cve_repo (repo_path )
102+ findings .extend (repo_cve )
103+
104+ if findings :
105+ cve_ids = [f .description [:80 ] for f in findings ]
106+ prev = _last_cve_alerts .get (name , [])
107+ new_cves = [c for c in cve_ids if c not in prev ]
108+ if new_cves :
109+ new_alerts [name ] = new_cves
110+ log .warning ("CVE watch: %s has %d new vulnerabilities" , name , len (new_cves ))
111+ _last_cve_alerts [name ] = cve_ids
112+
113+ except Exception as e :
114+ log .warning ("CVE watch failed for %s: %s" , name , e )
115+
116+ if new_alerts :
117+ total = sum (len (v ) for v in new_alerts .values ())
118+ publish_status (f"cve_alert:{ total } _new" )
119+ log .warning ("CVE watch: %d new vulnerabilities across %d components" ,
120+ total , len (new_alerts ))
121+ else :
122+ log .info ("CVE watch: no new vulnerabilities found" )
123+
124+ except Exception as e :
125+ log .exception ("CVE watch loop error: %s" , e )
126+
127+
60128def start (interval_hours : float = 24 ):
61129 """Start the scheduled scan loop."""
62130 global _task , _interval_hours , _enabled
@@ -75,14 +143,36 @@ def start(interval_hours: float = 24):
75143 log .info ("Scheduled scans enabled: every %gh" , interval_hours )
76144
77145
146+ def start_cve_watch (interval_hours : float = 6 ):
147+ """Start the CVE watch loop (lightweight, CVE-only checks)."""
148+ global _cve_task , _cve_interval_hours , _cve_enabled
149+ if interval_hours <= 0 :
150+ log .info ("CVE watch disabled (interval=0)" )
151+ return
152+
153+ _cve_interval_hours = interval_hours
154+ _cve_enabled = True
155+
156+ if _cve_task and not _cve_task .done ():
157+ log .info ("CVE watch already running, updating interval to %gh" , interval_hours )
158+ return
159+
160+ _cve_task = asyncio .create_task (_cve_watch_loop ())
161+ log .info ("CVE watch enabled: every %gh" , interval_hours )
162+
163+
78164def stop ():
79- """Stop the scheduled scan loop ."""
80- global _task , _enabled
165+ """Stop all scheduled tasks ."""
166+ global _task , _cve_task , _enabled , _cve_enabled
81167 _enabled = False
168+ _cve_enabled = False
82169 if _task and not _task .done ():
83170 _task .cancel ()
84171 _task = None
85- log .info ("Scheduled scans disabled" )
172+ if _cve_task and not _cve_task .done ():
173+ _cve_task .cancel ()
174+ _cve_task = None
175+ log .info ("All scheduled tasks disabled" )
86176
87177
88178def status () -> dict :
@@ -91,4 +181,15 @@ def status() -> dict:
91181 "enabled" : _enabled ,
92182 "interval_hours" : _interval_hours ,
93183 "running" : _task is not None and not _task .done () if _task else False ,
184+ "cve_watch" : {
185+ "enabled" : _cve_enabled ,
186+ "interval_hours" : _cve_interval_hours ,
187+ "running" : _cve_task is not None and not _cve_task .done () if _cve_task else False ,
188+ "active_alerts" : len (_last_cve_alerts ),
189+ },
94190 }
191+
192+
193+ def get_cve_alerts () -> dict [str , list [str ]]:
194+ """Return current CVE alerts from last watch run."""
195+ return _last_cve_alerts
0 commit comments