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:
Khalid MOUTAOUAKIL 2026-04-06 22:48:44 +02:00
parent 662b9c3535
commit 31bf62651c
2 changed files with 125 additions and 2 deletions

View File

@ -432,6 +432,124 @@ async def qualys_search(request: Request, db=Depends(get_db),
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)
async def qualys_asset_detail(request: Request, asset_id: int, db=Depends(get_db)):
user = get_current_user(request)

View File

@ -45,6 +45,9 @@
<!-- Panel détail -->
<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 -->
{% if assets %}
<!-- Bulk actions -->
@ -169,11 +172,13 @@ function updateBulkTag() {
<td class="p-2 text-center">
{% set vc = vuln_map.get(ip|string, {}) if vuln_map else {} %}
{% 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.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 %}
</span>
</button>
{% elif vc is mapping %}<span class="text-cyber-green text-xs">0</span>
{% else %}<span class="text-gray-600 text-xs">-</span>{% endif %}
</td>