11"""
2- SentryShield v1.0 — CERT-In Advisory Parser (cert_in_parser.py)
2+ SentryShield v2.5 — CERT-In Advisory Parser (cert_in_parser.py)
33
44Fetches live vulnerability advisories from India's CERT-In:
55 https://www.cert-in.org.in/
3535import sqlite3
3636import sys
3737import time
38- import ssl
38+ import http . client
3939from datetime import datetime , timedelta
4040from html .parser import HTMLParser
4141from pathlib import Path
42+ from urllib .parse import quote
4243from urllib .error import HTTPError , URLError
43- from urllib .parse import urlencode , quote
4444from urllib .request import Request , urlopen
45+ import ssl
4546from xml .etree import ElementTree
4647
4748logging .basicConfig (
7172CIVN_PATTERN = re .compile (r"CIVN-\d{4}-\d{4}" , re .IGNORECASE )
7273
7374
75+ def verify_nvd_key ():
76+ nvd_key = os .environ .get ("NVD_API_KEY" , "" ).strip ()
77+ if not nvd_key :
78+ log .warning ("[NVD] WARNING: No API key set — rate limited to 5 req/30s" )
79+ return
80+
81+ log .info ("[NVD] Verifying API key..." )
82+ headers = {"User-Agent" : "SentryShield/1.0" , "apiKey" : nvd_key }
83+ try :
84+ ctx = ssl ._create_unverified_context () if sys .platform == "darwin" else None
85+ conn = http .client .HTTPSConnection ("services.nvd.nist.gov" , context = ctx , timeout = 30 )
86+ conn .request ("GET" , "/rest/json/cves/2.0?resultsPerPage=1" , headers = headers )
87+ resp = conn .getresponse ()
88+ resp .read ()
89+
90+ if resp .status in (403 , 404 ):
91+ raise Exception ("NVD API key rejected (NIST returns 404/403 for invalid keys) — please verify your key or clear the NVD_API_KEY variable" )
92+ elif resp .status != 200 :
93+ log .warning (f"[NVD] API key verification failed: HTTP { resp .status } " )
94+ else :
95+ log .info ("[NVD] API key verified" )
96+ conn .close ()
97+ except Exception as e :
98+ if "rejected" in str (e ): raise
99+ log .warning (f"[NVD] API key verification failed: { e } " )
100+
101+
74102# ---------------------------------------------------------------------------
75103# CERT-In RSS parser
76104# ---------------------------------------------------------------------------
@@ -86,7 +114,6 @@ def parse(self, xml_text: str) -> list[dict]:
86114 channel = root .find ("channel" )
87115 if channel is None :
88116 # Try with namespace
89- ns = {"" : "http://www.w3.org/2005/Atom" }
90117 items = root .findall (".//item" ) or root .findall (".//entry" )
91118 else :
92119 items = channel .findall ("item" )
@@ -208,43 +235,51 @@ def fetch_nvd_cve(cve_id: str, retry: int = 3) -> dict | None:
208235 Fetch a single CVE's full record from NVD API.
209236 Returns the parsed CVE dict or None on failure.
210237 """
238+ nvd_key = os .environ .get ("NVD_API_KEY" , "" ).strip ()
211239 headers = {"User-Agent" : "SentryShield/1.0 (Security Scanner)" }
212- if NVD_API_KEY :
213- headers ["apiKey" ] = NVD_API_KEY
240+ if nvd_key :
241+ headers ["apiKey" ] = nvd_key
214242
215243 url = f"{ NVD_CVE_URL } ?cveId={ cve_id } "
216244
217245 for attempt in range (retry ):
218246 try :
219- req = Request (url , headers = headers )
220- ctx = ssl ._create_unverified_context ()
221- with urlopen (req , timeout = 20 , context = ctx ) as resp :
222- data = json .loads (resp .read ().decode ("utf-8" ))
247+ ctx = ssl ._create_unverified_context () if sys .platform == "darwin" else None
248+ conn = http .client .HTTPSConnection ("services.nvd.nist.gov" , context = ctx , timeout = 20 )
249+ conn .request ("GET" , f"/rest/json/cves/2.0?cveId={ cve_id } " , headers = headers )
250+ resp = conn .getresponse ()
251+ body = resp .read ().decode ("utf-8" )
252+ code = resp .status
253+ conn .close ()
254+ except Exception as e :
255+ log .warning ("NVD fetch error for %s: %s" , cve_id , e )
256+ break
223257
258+ if code == 200 :
259+ data = json .loads (body )
224260 vulns = data .get ("vulnerabilities" , [])
225261 if not vulns :
226262 return None
227-
228263 return vulns [0 ].get ("cve" , {})
229264
230- except HTTPError as e :
231- if e .code == 404 :
232- return None # CVE doesn't exist in NVD
233- if e .code == 429 :
234- log .warning ("NVD rate limit hit — waiting 35s..." )
235- time .sleep (35 )
265+ if code == 403 or (code == 404 and "cveId=" not in url ):
266+ raise Exception ("NVD API key rejected (NIST returned 404/403) — verify your key or clear NVD_API_KEY" )
267+ if code == 404 :
268+ log .error ("NVD HTTP 404 Not Found for %s" , url )
269+ return None # CVE doesn't exist in NVD
270+ if code == 429 :
271+ if attempt < retry - 1 :
272+ wait_time = 6 * (2 ** attempt )
273+ log .warning ("NVD rate limit hit (HTTP 429). Waiting %ds before retrying..." , wait_time )
274+ time .sleep (wait_time )
275+ continue
236276 else :
237- log .warning ("NVD HTTP %d for %s" , e . code , cve_id )
277+ log .error ("NVD HTTP error: 429 Too Many Requests (max retries reached)" )
238278 break
239- except URLError as e :
240- log .warning ("NVD connection error for %s: %s" , cve_id , e .reason )
241- break
242- except Exception as e :
243- log .warning ("NVD fetch error for %s: %s" , cve_id , e )
279+ else :
280+ log .warning ("NVD HTTP %d for %s" , code , cve_id )
244281 break
245282
246- time .sleep (2 ** attempt ) # Exponential backoff
247-
248283 return None
249284
250285
@@ -394,7 +429,7 @@ def sync(self, days_back: int = 30) -> dict:
394429 log .debug (" %s already in DB — skipped" , cve_id )
395430
396431 # NVD rate limit: 5 req/30s without key, 50 req/30s with key
397- time .sleep (0.8 if NVD_API_KEY else 6.5 )
432+ time .sleep (1.0 if os . environ . get ( " NVD_API_KEY" , "" ). strip () else 6.5 )
398433
399434 elapsed = (datetime .utcnow () - start ).total_seconds ()
400435 summary = {
@@ -428,7 +463,7 @@ def sync_single_advisory(self, civn_id: str) -> int:
428463 record = nvd_cve_to_record (cve_data , civn_id = civn_id )
429464 if record and self ._insert_record (record ):
430465 inserted += 1
431- time .sleep (0.8 if NVD_API_KEY else 6.5 )
466+ time .sleep (1.0 if os . environ . get ( " NVD_API_KEY" , "" ). strip () else 6.5 )
432467
433468 return inserted
434469
@@ -600,6 +635,8 @@ def main():
600635
601636 args = parser .parse_args ()
602637
638+ verify_nvd_key ()
639+
603640 if not Path (args .db ).exists ():
604641 log .error ("Database not found: %s — run init_db.py first" , args .db )
605642 sys .exit (1 )
0 commit comments