import requests
import sys
import time
import os
import requests
import re
import psycopg2
from psycopg2.extras import execute_values
JENKINS_URL = "https://jenkins.lmru.tech"
AUTH_USER = "lm-sa-devsecops"
AUTH_TOKEN = "11b0f9ff6c986b18f0c7fa6e8ae769b66c"
VAULT_URL = "https://vault.lmru.tech"
ARTIFACTORY_URL = "https://art.lmru.tech"
SONARQUBE_URL = "https://sonar.lmru.tech"
CROWD_URL = "https://crowd.lmru.tech"
GHE_URL = "https://github.lmru.tech"
ARGOCD_URL = "https://argocd.devops.lmru.tech"
GRAFANA_OBS_URL = "https://obs-grafana-yc-techno.apps.lmru.tech"
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 get_vault_version():
"""Получить версию Vault."""
# Предполагаем, что Vault доступен по https://vault.lmru.tech/v1/sys/health
# и возвращает JSON с полем "version"
url = f"{VAULT_URL}/v1/sys/health"
resp = requests.get(url, verify=False)
resp.raise_for_status()
data = resp.json()
vault_version = data.get("version")
return vault_version
def get_artifactory_version():
# Artifactory version endpoint: /api/system/version
url = f"{ARTIFACTORY_URL}/api/system/version"
resp = requests.get(url, verify=False)
resp.raise_for_status()
data = resp.json()
artifactory_version = data.get("version")
return artifactory_version
def get_sonarqube_version():
# Эндпоинт SonarQube: /api/server/version
url = f"{SONARQUBE_URL}/api/server/version"
resp = requests.get(url, verify=False)
resp.raise_for_status()
# Версия приходит как простой текст (str)
sonarqube_version = resp.text.strip()
return sonarqube_version
def get_crowd_version():
url = f"{CROWD_URL}/crowd/console/login.action"
resp = requests.get(url, verify=False)
resp.raise_for_status()
html = resp.text
# Ищем значение атрибута value у input с id="crowd-version"
match_input = re.search(r']*id="crowd-version"[^>]*value="([^"]+)"', html)
if not match_input:
print("Could not find the crowd-version input field.")
return None
value_str = match_input.group(1) # например: 'Version: 4.2.2 (Build:#1581 - 2020-11-19) '
# Заменим на пробел
value_str = value_str.replace(' ', ' ')
# Ищем версию после слова Version:
match_version = re.search(r'Version:\s*([0-9]+\.[0-9]+\.[0-9]+)', value_str)
if match_version:
return match_version.group(1)
else:
print("Could not extract Crowd version from the value string.")
return None
def get_ghe_version():
url = f"{GHE_URL}/api/v3/meta"
resp = requests.get(url, verify=False)
resp.raise_for_status()
data = resp.json()
ghe_version = data.get("installed_version")
if ghe_version:
return ghe_version
else:
print("Could not extract GitHub Enterprise version from the /meta endpoint.")
return None
def get_argocd_version():
url = f"{ARGOCD_URL}/api/version"
resp = requests.get(url, verify=False)
resp.raise_for_status()
data = resp.json()
argocd_version = data.get("Version")
if argocd_version:
# Удалим префикс 'v'
argocd_version = argocd_version.lstrip('v')
# Если есть суффикс вида +коммита, уберём всё после +
if '+' in argocd_version:
argocd_version = argocd_version.split('+')[0]
return argocd_version
else:
print("Could not determine ArgoCD version from /api/version endpoint.")
return None
def get_oncall_version():
url = f"{GRAFANA_OBS_URL}/metrics"
resp = requests.get(url, verify=False)
resp.raise_for_status()
text = resp.text
# Ищем строку с plugin_id="grafana-oncall-app"
# Пример строки:
# grafana_plugin_build_info{plugin_id="grafana-oncall-app",plugin_type="app",signature_status="valid",version="1.3.58"} 1
match_line = re.search(r'grafana_plugin_build_info\{[^}]*plugin_id="grafana-oncall-app"[^}]*\}', text)
if not match_line:
print("Could not find grafana-oncall-app plugin info in /metrics.")
return None
line = match_line.group(0)
# Извлекаем version="..."
match_version = re.search(r'version="([^"]+)"', line)
if match_version:
oncall_version = match_version.group(1)
return oncall_version
else:
print("Could not extract version from grafana-oncall-app plugin line.")
return None
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, component, name, version, vulnerabilities):
"""
Вставить или обновить информацию об уязвимостях в PostgreSQL.
component - строка, например 'jenkins' или 'vault'.
"""
if vulnerabilities:
delete_query = """
DELETE FROM software_vulnerabilities
WHERE component = %s AND name = %s AND version = %s AND cve_id = 'NO_VULN';
"""
with conn.cursor() as cur:
cur.execute(delete_query, (component, 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)
# Добавляем component как первый элемент кортежа
records.append((component, name, version, cve_id, desc_en, severity, published_date, last_modified, True))
query = """
INSERT INTO software_vulnerabilities (component, name, version, cve_id, description, severity, published_date, last_modified, has_vulnerabilities)
VALUES %s
ON CONFLICT (component, 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:
query = """
INSERT INTO software_vulnerabilities (component, name, version, cve_id, has_vulnerabilities)
VALUES (%s, %s, %s, 'NO_VULN', FALSE)
ON CONFLICT (component, 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, (component, 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
component = "jenkins"
jenkins_version = get_jenkins_version()
if not jenkins_version:
print("Could not determine Jenkins version.")
sys.exit(1)
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, component, "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_name = f"{shortName}_plugin"
# # Для плагинов также component = 'jenkins', так как они относятся к Jenkins
# insert_vulnerabilities(conn, component, plugin_name, version, plugin_vulns)
# Vault
vault_version = get_vault_version()
if not vault_version:
print("Could not determine Vault version.")
sys.exit(1)
vault_version_clean = vault_version.split('+')[0]
vault_cpe = f"cpe:2.3:a:hashicorp:vault:{vault_version_clean}:*:*:*:enterprise:*:*:*"
print(f"Vault version: {vault_version}")
print(f"Vault CPE: {vault_cpe}")
# Для Vault component = 'vault'
vault_vulns = fetch_cves_by_cpe(vault_cpe)
insert_vulnerabilities(conn, "vault", "vault", vault_version, vault_vulns)
# Artifactory
artifactory_version = get_artifactory_version()
if not artifactory_version:
print("Could not determine Artifactory version.")
sys.exit(1)
artifactory_cpe = f"cpe:2.3:a:jfrog:artifactory:{artifactory_version}:*:*:*:*:*:*:*"
print(f"Artifactory version: {artifactory_version}")
print(f"Artifactory CPE: {artifactory_cpe}")
artifactory_vulns = fetch_cves_by_cpe(artifactory_cpe)
insert_vulnerabilities(conn, "artifactory", "artifactory", artifactory_version, artifactory_vulns)
# SonarQube
sonarqube_version = get_sonarqube_version()
if not sonarqube_version:
print("Could not determine SonarQube version.")
sys.exit(1)
sonarqube_cpe = f"cpe:2.3:a:sonarsource:sonarqube:{sonarqube_version}:*:*:*:*:*:*:*"
print(f"SonarQube version: {sonarqube_version}")
print(f"SonarQube CPE: {sonarqube_cpe}")
sonarqube_vulns = fetch_cves_by_cpe(sonarqube_cpe)
insert_vulnerabilities(conn, "sonarqube", "sonarqube", sonarqube_version, sonarqube_vulns)
# Crowd
crowd_version = get_crowd_version()
if not crowd_version:
print("Could not determine Crowd version.")
sys.exit(1)
crowd_cpe = f"cpe:2.3:a:atlassian:crowd:{crowd_version}:*:*:*:*:*:*:*"
print(f"Crowd version: {crowd_version}")
print(f"Crowd CPE: {crowd_cpe}")
crowd_vulns = fetch_cves_by_cpe(crowd_cpe)
insert_vulnerabilities(conn, "crowd", "crowd", crowd_version, crowd_vulns)
# GitHub Enterprise
ghe_version = get_ghe_version()
if not ghe_version:
print("Could not determine GitHub Enterprise version.")
sys.exit(1)
# Предположим такой CPE для GitHub Enterprise.
# Если точного CPE нет, можно поискать в NVD. Допустим:
# cpe:2.3:a:github:enterprise_server::*:*:*:*:*:*:*
ghe_cpe = f"cpe:2.3:a:github:enterprise_server:{ghe_version}:*:*:*:*:*:*:*"
print(f"GitHub Enterprise version: {ghe_version}")
print(f"GitHub Enterprise CPE: {ghe_cpe}")
ghe_vulns = fetch_cves_by_cpe(ghe_cpe)
insert_vulnerabilities(conn, "github_enterprise", "github_enterprise", ghe_version, ghe_vulns)
# ArgoCD
argocd_version = get_argocd_version()
if not argocd_version:
print("Could not determine ArgoCD version.")
sys.exit(1)
# Формируем CPE для ArgoCD:
argocd_cpe = f"cpe:2.3:a:argoproj:argo-cd:{argocd_version}:*:*:*:*:*:*:*"
print(f"ArgoCD version: {argocd_version}")
print(f"ArgoCD CPE: {argocd_cpe}")
argocd_vulns = fetch_cves_by_cpe(argocd_cpe)
insert_vulnerabilities(conn, "argocd", "argocd", argocd_version, argocd_vulns)
# OnCall
oncall_version = get_oncall_version()
if not oncall_version:
print("Could not determine OnCall version.")
sys.exit(1)
oncall_cpe = f"cpe:2.3:a:grafana:oncall:{oncall_version}:*:*:*:*:*:*:*"
print(f"OnCall version: {oncall_version}")
print(f"OnCall CPE: {oncall_cpe}")
oncall_vulns = fetch_cves_by_cpe(oncall_cpe)
insert_vulnerabilities(conn, "grafana", "oncall", oncall_version, oncall_vulns)
conn.close()
if __name__ == "__main__":
main()