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)
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user