import requests import sys import time import os import psycopg2 from psycopg2.extras import execute_values JENKINS_URL = "https://jenkins.lmru.tech" AUTH_USER = "lm-sa-devsecops" AUTH_TOKEN = "11b0f9ff6c986b18f0c7fa6e8ae769b66c" NVD_API_URL = "https://services.nvd.nist.gov/rest/json/cves/2.0" NVD_API_KEY = "aa054ea4-4a99-4f16-b5ed-1f77b5193d80" DB_HOST = "rc1d-f1mdariunacszmi6.mdb.yandexcloud.net" DB_PORT = 6432 DB_NAME = "securitydebt" DB_USER = "cicd-monitoring" DB_PASSWORD = "prelude-uT9/HER" def get_jenkins_version(): """Получить версию Jenkins.""" resp = requests.get(f"{JENKINS_URL}/api/json", auth=(AUTH_USER, AUTH_TOKEN), verify=False) resp.raise_for_status() jenkins_version = resp.headers.get("X-Jenkins") if not jenkins_version: # Если нет в заголовке, пытаемся достать из JSON data = resp.json() jenkins_version = data.get("jenkinsVersion") return jenkins_version def get_jenkins_plugins(): """Получить список (shortName, version) для установленных плагинов Jenkins.""" url = f"{JENKINS_URL}/pluginManager/api/json?depth=1" resp = requests.get(url, auth=(AUTH_USER, AUTH_TOKEN), verify=False) resp.raise_for_status() data = resp.json() plugins = data.get("plugins", []) plugin_list = [] for p in plugins: shortName = p.get("shortName") version = p.get("version") if shortName and version: plugin_list.append((shortName, version)) return plugin_list def fetch_cves_by_cpe(cpe_name, max_retries=3, delay=5): """ Запросить уязвимости из NVD по cpeName. При ошибках 403/503 делает несколько попыток. """ params = { "cpeName": cpe_name, "resultsPerPage": 50 } headers = { "apiKey": NVD_API_KEY } for attempt in range(max_retries): resp = requests.get(NVD_API_URL, params=params, headers=headers) if resp.status_code == 403: if attempt < max_retries - 1: print(f"Got 403 Forbidden with API key. Retrying in {delay} seconds...") time.sleep(delay) continue else: print("Got 403 Forbidden after maximum retries. Exiting.") sys.exit(1) elif resp.status_code == 503: if attempt < max_retries - 1: print(f"Got 503 Service Unavailable. Retrying in {delay} seconds...") time.sleep(delay) continue else: print("Got 503 Service Unavailable after maximum retries. Exiting.") sys.exit(1) resp.raise_for_status() data = resp.json() return data.get("vulnerabilities", []) print("All attempts failed unexpectedly. Exiting.") sys.exit(1) def insert_vulnerabilities(conn, name, version, vulnerabilities): """ Вставить или обновить информацию об уязвимостях в PostgreSQL. Если нет уязвимостей: - вставляем/обновляем запись с cve_id='NO_VULN'. Если уязвимости появились: - удаляем запись 'NO_VULN' (если была), - вставляем все уязвимости с UPSERT. Используем ON CONFLICT DO UPDATE для актуализации данных. """ if vulnerabilities: # Удаляем запись NO_VULN, если есть delete_query = """ DELETE FROM software_vulnerabilities WHERE name = %s AND version = %s AND cve_id = 'NO_VULN'; """ with conn.cursor() as cur: cur.execute(delete_query, (name, version)) conn.commit() # Вставляем реальные уязвимости records = [] for v in vulnerabilities: cve = v.get("cve", {}) cve_id = cve.get("id") descs = cve.get("descriptions", []) desc_en = next((d.get("value") for d in descs if d.get("lang") == "en"), "") severity = None published_date = cve.get("published") last_modified = cve.get("lastModified") metrics = cve.get("metrics", {}) cvss_v3 = metrics.get("cvssMetricV31", []) if cvss_v3: m = cvss_v3[0] cvss_data = m.get("cvssData", {}) severity = cvss_data.get("baseSeverity", None) records.append((name, version, cve_id, desc_en, severity, published_date, last_modified, True)) query = """ INSERT INTO software_vulnerabilities (name, version, cve_id, description, severity, published_date, last_modified, has_vulnerabilities) VALUES %s ON CONFLICT (name, version, cve_id) DO UPDATE SET description = EXCLUDED.description, severity = EXCLUDED.severity, published_date = EXCLUDED.published_date, last_modified = EXCLUDED.last_modified, has_vulnerabilities = EXCLUDED.has_vulnerabilities, inserted_at = NOW(); """ with conn.cursor() as cur: execute_values(cur, query, records) conn.commit() else: # Нет уязвимостей - вставляем/обновляем NO_VULN query = """ INSERT INTO software_vulnerabilities (name, version, cve_id, has_vulnerabilities) VALUES (%s, %s, 'NO_VULN', FALSE) ON CONFLICT (name, version, cve_id) DO UPDATE SET has_vulnerabilities = EXCLUDED.has_vulnerabilities, inserted_at = NOW(), description = NULL, severity = NULL, published_date = NULL, last_modified = NULL; """ with conn.cursor() as cur: cur.execute(query, (name, version)) conn.commit() def main(): # Подключаемся к БД conn = psycopg2.connect( host=DB_HOST, port=DB_PORT, dbname=DB_NAME, user=DB_USER, password=DB_PASSWORD ) jenkins_version = get_jenkins_version() if not jenkins_version: print("Could not determine Jenkins version.") sys.exit(1) # Обрабатываем Jenkins Core jenkins_cpe = f"cpe:2.3:a:jenkins:jenkins:{jenkins_version}:*:*:*:-:*:*:*" print(f"Jenkins version: {jenkins_version}") print(f"Jenkins CPE: {jenkins_cpe}") jenkins_vulns = fetch_cves_by_cpe(jenkins_cpe) insert_vulnerabilities(conn, "jenkins_core", jenkins_version, jenkins_vulns) # Обрабатываем плагины plugins = get_jenkins_plugins() print(f"\nFound {len(plugins)} plugins installed.") for shortName, version in plugins: plugin_cpe = f"cpe:2.3:a:jenkins:{shortName}:{version}:*:*:*:*:jenkins:*:*" print(f"\nPlugin: {shortName}, version: {version}") print(f"CPE: {plugin_cpe}") plugin_vulns = fetch_cves_by_cpe(plugin_cpe) # Добавляем суффикс _plugin для отличия в графиках plugin_name = f"{shortName}_plugin" insert_vulnerabilities(conn, plugin_name, version, plugin_vulns) conn.close() if __name__ == "__main__": main()