Qualys vulns: clic sur badges ouvre detail (QID, titre, CVE avec lien NVD, CVSS3, detection, solution)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
662b9c3535
commit
31bf62651c
@ -432,6 +432,124 @@ async def qualys_search(request: Request, db=Depends(get_db),
|
|||||||
return templates.TemplateResponse("qualys_search.html", ctx)
|
return templates.TemplateResponse("qualys_search.html", ctx)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/qualys/vulns/{ip}", response_class=HTMLResponse)
|
||||||
|
async def qualys_vulns_detail(request: Request, ip: str, db=Depends(get_db)):
|
||||||
|
"""Retourne le detail des vulns severity 3,4,5 pour une IP (fragment HTMX)"""
|
||||||
|
user = get_current_user(request)
|
||||||
|
if not user:
|
||||||
|
return HTMLResponse("<p>Non autorise</p>")
|
||||||
|
|
||||||
|
from ..services.qualys_service import _get_qualys_creds, parse_xml
|
||||||
|
import requests as _req, urllib3, re as _re
|
||||||
|
urllib3.disable_warnings()
|
||||||
|
|
||||||
|
qualys_url, qualys_user, qualys_pass, qualys_proxy = _get_qualys_creds(db)
|
||||||
|
proxies = {"https": qualys_proxy, "http": qualys_proxy} if qualys_proxy else None
|
||||||
|
|
||||||
|
def parse_cdata(txt, tag):
|
||||||
|
import re
|
||||||
|
m = re.search(rf'<{tag}>\s*(?:<!\[CDATA\[)?(.*?)(?:\]\]>)?\s*</{tag}>', txt, re.DOTALL)
|
||||||
|
return m.group(1).strip() if m else ""
|
||||||
|
|
||||||
|
# 1. Detections
|
||||||
|
try:
|
||||||
|
r = _req.post(f"{qualys_url}/api/2.0/fo/asset/host/vm/detection/",
|
||||||
|
data={"action": "list", "ips": ip, "severities": "3,4,5",
|
||||||
|
"status": "New,Active,Re-Opened", "show_results": "1", "output_format": "XML"},
|
||||||
|
auth=(qualys_user, qualys_pass), verify=False, timeout=90, proxies=proxies,
|
||||||
|
headers={"X-Requested-With": "Python"})
|
||||||
|
except Exception as e:
|
||||||
|
return HTMLResponse(f"<p class='text-cyber-red'>Erreur API: {e}</p>")
|
||||||
|
|
||||||
|
detections = []
|
||||||
|
qids = []
|
||||||
|
for det in r.text.split("<DETECTION>")[1:]:
|
||||||
|
det = det.split("</DETECTION>")[0]
|
||||||
|
qid = (parse_xml(det, "QID") or [""])[0]
|
||||||
|
sev = (parse_xml(det, "SEVERITY") or [""])[0]
|
||||||
|
dtype = (parse_xml(det, "TYPE") or [""])[0]
|
||||||
|
status = (parse_xml(det, "STATUS") or [""])[0]
|
||||||
|
results = parse_cdata(det, "RESULTS")
|
||||||
|
sev_i = int(sev) if sev.isdigit() else 0
|
||||||
|
if sev_i < 3 or dtype not in ("Confirmed", "Potential"):
|
||||||
|
continue
|
||||||
|
detections.append({"qid": qid, "severity": sev_i, "type": dtype, "status": status, "results": results})
|
||||||
|
if qid not in qids:
|
||||||
|
qids.append(qid)
|
||||||
|
|
||||||
|
if not detections:
|
||||||
|
return HTMLResponse("<p class='text-gray-500 p-4'>Aucune vulnerabilite active (severity 3+)</p>")
|
||||||
|
|
||||||
|
# 2. KB pour titre, CVE, description
|
||||||
|
kb = {}
|
||||||
|
if qids:
|
||||||
|
try:
|
||||||
|
r2 = _req.post(f"{qualys_url}/api/2.0/fo/knowledge_base/vuln/",
|
||||||
|
data={"action": "list", "ids": ",".join(qids)},
|
||||||
|
auth=(qualys_user, qualys_pass), verify=False, timeout=90, proxies=proxies,
|
||||||
|
headers={"X-Requested-With": "Python"})
|
||||||
|
for vuln_block in r2.text.split("<VULN>")[1:]:
|
||||||
|
vuln_block = vuln_block.split("</VULN>")[0]
|
||||||
|
qid = (parse_xml(vuln_block, "QID") or [""])[0]
|
||||||
|
title = parse_cdata(vuln_block, "TITLE")
|
||||||
|
diagnosis = parse_cdata(vuln_block, "DIAGNOSIS")
|
||||||
|
solution = parse_cdata(vuln_block, "SOLUTION")
|
||||||
|
consequence = parse_cdata(vuln_block, "CONSEQUENCE")
|
||||||
|
# CVEs
|
||||||
|
cves = []
|
||||||
|
if "<CVE_LIST>" in vuln_block:
|
||||||
|
cve_block = vuln_block.split("<CVE_LIST>")[1].split("</CVE_LIST>")[0]
|
||||||
|
cve_ids = _re.findall(r'<!\[CDATA\[(CVE-[\d-]+)\]\]>', cve_block)
|
||||||
|
if not cve_ids:
|
||||||
|
cve_ids = parse_xml(cve_block, "ID")
|
||||||
|
cves = cve_ids
|
||||||
|
# CVSS
|
||||||
|
cvss = ""
|
||||||
|
if "<CVSS_V3>" in vuln_block:
|
||||||
|
cvss = (parse_xml(vuln_block.split("<CVSS_V3>")[1].split("</CVSS_V3>")[0], "BASE") or [""])[0]
|
||||||
|
kb[qid] = {"title": title, "diagnosis": diagnosis, "solution": solution,
|
||||||
|
"consequence": consequence, "cves": cves, "cvss3": cvss}
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 3. Construire HTML
|
||||||
|
html = f'<div class="p-4"><h3 class="text-sm font-bold text-cyber-accent mb-3">{len(detections)} vulnerabilite(s) — {ip}</h3>'
|
||||||
|
html += '<table class="w-full table-cyber text-xs"><thead><tr>'
|
||||||
|
html += '<th class="p-2">Sev</th><th class="p-2">QID</th><th class="text-left p-2">Titre</th>'
|
||||||
|
html += '<th class="p-2">Type</th><th class="p-2">CVE</th><th class="p-2">CVSS3</th>'
|
||||||
|
html += '</tr></thead><tbody>'
|
||||||
|
|
||||||
|
for d in sorted(detections, key=lambda x: -x["severity"]):
|
||||||
|
qid = d["qid"]
|
||||||
|
k = kb.get(qid, {})
|
||||||
|
sev_class = "badge-red" if d["severity"] >= 5 else ("badge-yellow" if d["severity"] >= 4 else "badge-gray")
|
||||||
|
cve_links = " ".join([f'<a href="https://nvd.nist.gov/vuln/detail/{c}" target="_blank" class="text-cyber-accent hover:underline">{c}</a>' for c in k.get("cves", [])])
|
||||||
|
|
||||||
|
html += f'<tr>'
|
||||||
|
html += f'<td class="p-2 text-center"><span class="badge {sev_class}">{d["severity"]}</span></td>'
|
||||||
|
html += f'<td class="p-2 text-center font-mono">{qid}</td>'
|
||||||
|
html += f'<td class="p-2 text-cyber-accent">{k.get("title", "N/A")}</td>'
|
||||||
|
html += f'<td class="p-2 text-center">{d["type"]}</td>'
|
||||||
|
html += f'<td class="p-2">{cve_links or "-"}</td>'
|
||||||
|
html += f'<td class="p-2 text-center font-bold">{k.get("cvss3", "-")}</td>'
|
||||||
|
html += '</tr>'
|
||||||
|
|
||||||
|
# Ligne detail
|
||||||
|
diag = k.get("diagnosis", "")[:300]
|
||||||
|
results = d.get("results", "")[:200]
|
||||||
|
html += f'<tr class="bg-cyber-hover/30"><td colspan="6" class="p-2 text-xs text-gray-400">'
|
||||||
|
if diag:
|
||||||
|
html += f'<b>Detection :</b> {diag}<br>'
|
||||||
|
if results:
|
||||||
|
html += f'<b>Resultat :</b> <span class="font-mono">{results}</span><br>'
|
||||||
|
if k.get("solution"):
|
||||||
|
html += f'<b>Solution :</b> {k["solution"][:200]}'
|
||||||
|
html += '</td></tr>'
|
||||||
|
|
||||||
|
html += '</tbody></table></div>'
|
||||||
|
return HTMLResponse(html)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/qualys/asset/{asset_id}", response_class=HTMLResponse)
|
@router.get("/qualys/asset/{asset_id}", response_class=HTMLResponse)
|
||||||
async def qualys_asset_detail(request: Request, asset_id: int, db=Depends(get_db)):
|
async def qualys_asset_detail(request: Request, asset_id: int, db=Depends(get_db)):
|
||||||
user = get_current_user(request)
|
user = get_current_user(request)
|
||||||
|
|||||||
@ -45,6 +45,9 @@
|
|||||||
<!-- Panel détail -->
|
<!-- Panel détail -->
|
||||||
<div id="qualys-detail" class="card mb-4 p-5" style="display:none"></div>
|
<div id="qualys-detail" class="card mb-4 p-5" style="display:none"></div>
|
||||||
|
|
||||||
|
<!-- Panel vulnérabilités -->
|
||||||
|
<div id="vuln-detail" class="card mb-4" style="display:none"></div>
|
||||||
|
|
||||||
<!-- Résultats -->
|
<!-- Résultats -->
|
||||||
{% if assets %}
|
{% if assets %}
|
||||||
<!-- Bulk actions -->
|
<!-- Bulk actions -->
|
||||||
@ -169,11 +172,13 @@ function updateBulkTag() {
|
|||||||
<td class="p-2 text-center">
|
<td class="p-2 text-center">
|
||||||
{% set vc = vuln_map.get(ip|string, {}) if vuln_map else {} %}
|
{% set vc = vuln_map.get(ip|string, {}) if vuln_map else {} %}
|
||||||
{% if vc and vc.total > 0 %}
|
{% if vc and vc.total > 0 %}
|
||||||
<span title="S3:{{ vc.severity3 }} S4:{{ vc.severity4 }} S5:{{ vc.severity5 }} | Confirmed:{{ vc.confirmed }} Potential:{{ vc.potential }}">
|
<button class="hover:opacity-80" hx-get="/qualys/vulns/{{ ip }}" hx-target="#vuln-detail" hx-swap="innerHTML"
|
||||||
|
onclick="document.getElementById('vuln-detail').style.display='block'; window.scrollTo({top:0,behavior:'smooth'})"
|
||||||
|
title="S3:{{ vc.severity3 }} S4:{{ vc.severity4 }} S5:{{ vc.severity5 }} | Confirmed:{{ vc.confirmed }} Potential:{{ vc.potential }}">
|
||||||
{% if vc.severity5 > 0 %}<span class="badge badge-red">{{ vc.severity5 }} crit</span> {% endif %}
|
{% if vc.severity5 > 0 %}<span class="badge badge-red">{{ vc.severity5 }} crit</span> {% endif %}
|
||||||
{% if vc.severity4 > 0 %}<span class="badge badge-yellow">{{ vc.severity4 }} high</span> {% endif %}
|
{% if vc.severity4 > 0 %}<span class="badge badge-yellow">{{ vc.severity4 }} high</span> {% endif %}
|
||||||
{% if vc.severity3 > 0 %}<span class="text-gray-400 text-xs">+{{ vc.severity3 }} med</span>{% endif %}
|
{% if vc.severity3 > 0 %}<span class="text-gray-400 text-xs">+{{ vc.severity3 }} med</span>{% endif %}
|
||||||
</span>
|
</button>
|
||||||
{% elif vc is mapping %}<span class="text-cyber-green text-xs">0</span>
|
{% elif vc is mapping %}<span class="text-cyber-green text-xs">0</span>
|
||||||
{% else %}<span class="text-gray-600 text-xs">-</span>{% endif %}
|
{% else %}<span class="text-gray-600 text-xs">-</span>{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user