|
| 1 | +"""CVE Sync scan hitting new FastAPI endpoint.""" |
| 2 | +# --- Standard Libraries --- |
| 3 | +# Standard Python Libraries |
| 4 | +from datetime import datetime, timezone |
| 5 | +import hashlib |
| 6 | +import json |
| 7 | +import logging |
| 8 | +import os |
| 9 | +from urllib.parse import urljoin |
| 10 | + |
| 11 | +# Third-Party Libraries |
| 12 | +# --- Third-Party Libraries --- |
| 13 | +import django |
| 14 | +import requests |
| 15 | + |
| 16 | +# --- Django setup --- |
| 17 | +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "xfd_django.settings") |
| 18 | +os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true" |
| 19 | +django.setup() |
| 20 | + |
| 21 | +# Third-Party Libraries |
| 22 | +from xfd_api.helpers.date_time_helpers import calculate_days_back |
| 23 | + |
| 24 | +# --- Your CVE model import --- |
| 25 | +from xfd_mini_dl.models import Cve as CveModel |
| 26 | +from xfd_mini_dl.models import DataSource |
| 27 | + |
| 28 | +# --- Constants & Logging --- |
| 29 | +logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s") |
| 30 | +LOGGER = logging.getLogger(__name__) |
| 31 | + |
| 32 | +SALT = os.getenv("CHECKSUM_SALT", "default_salt") |
| 33 | +HEADERS = { |
| 34 | + "X-API-KEY": os.getenv("DMZ_API_KEY"), |
| 35 | + "Content-Type": "application/json", |
| 36 | +} |
| 37 | + |
| 38 | +# e.g. “https://api.staging-cd.crossfeed.cyber.dhs.gov/sync” |
| 39 | +base_url = os.getenv("DMZ_SYNC_ENDPOINT", "").rstrip("/") |
| 40 | + |
| 41 | +if base_url.endswith("/sync"): |
| 42 | + # drop “/sync” and append “/cves” |
| 43 | + CVE_API_URL = base_url.rsplit("/", 1)[0] + "/dmz_sync/cves" |
| 44 | +else: |
| 45 | + # fallback: join “/cves” onto whatever they provided |
| 46 | + CVE_API_URL = urljoin(base_url + "/", "dmz_sync/cves") |
| 47 | + |
| 48 | + |
| 49 | +def validate_response_checksum(response): |
| 50 | + """Validate the checksum from the API.""" |
| 51 | + try: |
| 52 | + data = response.json() |
| 53 | + received = response.headers.get("X-Salted-Checksum") |
| 54 | + if not received: |
| 55 | + LOGGER.warning("No checksum header") |
| 56 | + return False |
| 57 | + |
| 58 | + serialized = json.dumps(data, default=str, sort_keys=True) |
| 59 | + calc = hashlib.sha256((SALT + serialized).encode()).hexdigest() |
| 60 | + return received == calc |
| 61 | + except Exception as e: |
| 62 | + LOGGER.error("Error validating checksum: %s", e) |
| 63 | + return False |
| 64 | + |
| 65 | + |
| 66 | +def save_cves_to_db(cve_list): |
| 67 | + """ |
| 68 | + Upsert each CVE dict into the local DB. |
| 69 | +
|
| 70 | + Matches your corrected Cve model with ArrayFields. |
| 71 | + """ |
| 72 | + for item in cve_list: |
| 73 | + # parse ISO timestamps |
| 74 | + try: |
| 75 | + pub = datetime.datetime.fromisoformat(item["published_at"]) |
| 76 | + except Exception: |
| 77 | + pub = None |
| 78 | + try: |
| 79 | + mod = datetime.datetime.fromisoformat(item["modified_at"]) |
| 80 | + except Exception: |
| 81 | + mod = None |
| 82 | + |
| 83 | + defaults = { |
| 84 | + "name": item.get("name"), |
| 85 | + "published_at": pub, |
| 86 | + "modified_at": mod, |
| 87 | + "status": item.get("status"), |
| 88 | + "description": item.get("description"), |
| 89 | + # CVSS v2 |
| 90 | + "cvss_v2_source": item.get("cvss_v2_source"), |
| 91 | + "cvss_v2_type": item.get("cvss_v2_type"), |
| 92 | + "cvss_v2_version": item.get("cvss_v2_version"), |
| 93 | + "cvss_v2_vector_string": item.get("cvss_v2_vector_string"), |
| 94 | + "cvss_v2_base_score": item.get("cvss_v2_base_score"), |
| 95 | + "cvss_v2_base_severity": item.get("cvss_v2_base_severity"), |
| 96 | + "cvss_v2_exploitability_score": item.get("cvss_v2_exploitability_score"), |
| 97 | + "cvss_v2_impact_score": item.get("cvss_v2_impact_score"), |
| 98 | + # CVSS v3 |
| 99 | + "cvss_v3_source": item.get("cvss_v3_source"), |
| 100 | + "cvss_v3_type": item.get("cvss_v3_type"), |
| 101 | + "cvss_v3_version": item.get("cvss_v3_version"), |
| 102 | + "cvss_v3_vector_string": item.get("cvss_v3_vector_string"), |
| 103 | + "cvss_v3_base_score": item.get("cvss_v3_base_score"), |
| 104 | + "cvss_v3_base_severity": item.get("cvss_v3_base_severity"), |
| 105 | + "cvss_v3_exploitability_score": item.get("cvss_v3_exploitability_score"), |
| 106 | + "cvss_v3_impact_score": item.get("cvss_v3_impact_score"), |
| 107 | + # CVSS v4 |
| 108 | + "cvss_v4_source": item.get("cvss_v4_source"), |
| 109 | + "cvss_v4_type": item.get("cvss_v4_type"), |
| 110 | + "cvss_v4_version": item.get("cvss_v4_version"), |
| 111 | + "cvss_v4_vector_string": item.get("cvss_v4_vector_string"), |
| 112 | + "cvss_v4_base_score": item.get("cvss_v4_base_score"), |
| 113 | + "cvss_v4_base_severity": item.get("cvss_v4_base_severity"), |
| 114 | + "cvss_v4_exploitability_score": item.get("cvss_v4_exploitability_score"), |
| 115 | + "cvss_v4_impact_score": item.get("cvss_v4_impact_score"), |
| 116 | + # ArrayFields |
| 117 | + "weaknesses": item.get("weaknesses"), |
| 118 | + "reference_urls": item.get("reference_urls"), |
| 119 | + "cpe_list": item.get("cpe_list"), |
| 120 | + # dve_score left untouched (payload doesn’t include it) |
| 121 | + } |
| 122 | + |
| 123 | + try: |
| 124 | + CveModel.objects.update_or_create(id=item["id"], defaults=defaults) |
| 125 | + except Exception as e: |
| 126 | + LOGGER.error("Error saving CVE %s: %s", item["id"], e) |
| 127 | + |
| 128 | + |
| 129 | +def handler(command_options=None): |
| 130 | + """Fetch all CVEs and save them locally.""" |
| 131 | + nist_source, _ = DataSource.objects.get_or_create( |
| 132 | + name="Nist", |
| 133 | + defaults={ |
| 134 | + "description": "Nist cve capture", |
| 135 | + "last_run": timezone.now().date(), |
| 136 | + }, |
| 137 | + ) |
| 138 | + |
| 139 | + since_date = calculate_days_back(15) |
| 140 | + |
| 141 | + try: |
| 142 | + LOGGER.info("Starting CVE sync…") |
| 143 | + page = 1 |
| 144 | + per_page = 200 |
| 145 | + done = False |
| 146 | + |
| 147 | + while not done: |
| 148 | + payload = { |
| 149 | + "page": page, |
| 150 | + "page_size": per_page, |
| 151 | + "since_date": since_date, |
| 152 | + } |
| 153 | + |
| 154 | + resp = requests.post(CVE_API_URL, headers=HEADERS, json=payload, timeout=60) |
| 155 | + resp.raise_for_status() |
| 156 | + |
| 157 | + if not validate_response_checksum(resp): |
| 158 | + LOGGER.error("Checksum mismatch!") |
| 159 | + return {"statusCode": 500, "body": "Checksum mismatch"} |
| 160 | + |
| 161 | + body = resp.json() |
| 162 | + if body.get("status") != "ok": |
| 163 | + LOGGER.error("API returned bad status: %s", body) |
| 164 | + return {"statusCode": 500, "body": "Bad status"} |
| 165 | + total_pages = resp.get("total_pages", 1) |
| 166 | + current_page = resp.get("current_page", 1) |
| 167 | + payload = body.get("payload", []) |
| 168 | + LOGGER.info("Fetched %s CVEs", len(payload)) |
| 169 | + save_cves_to_db(payload) |
| 170 | + if current_page >= total_pages: |
| 171 | + done = True |
| 172 | + else: |
| 173 | + page += 1 |
| 174 | + LOGGER.info("CVE sync completed successfully") |
| 175 | + |
| 176 | + return {"statusCode": 200, "body": "Shodan sync completed successfully."} |
| 177 | + |
| 178 | + except Exception as e: |
| 179 | + LOGGER.error("Sync error: %s", e) |
| 180 | + return {"statusCode": 500, "body": str(e)} |
0 commit comments