Modules: Dashboard, Serveurs, Campagnes, Planning, Specifiques, Settings, Users Stack: FastAPI + Jinja2 + HTMX + Alpine.js + TailwindCSS + PostgreSQL Features: Qualys sync, prereqs auto, planning annuel, server specifics, role-based access Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
157 lines
6.1 KiB
Python
157 lines
6.1 KiB
Python
"""Service Qualys — sync tags pour un serveur via API"""
|
|
import re
|
|
import requests
|
|
import urllib3
|
|
from sqlalchemy import text
|
|
from .secrets_service import get_secret
|
|
|
|
urllib3.disable_warnings()
|
|
|
|
|
|
def _get_qualys_creds(db):
|
|
"""Recupere les credentials Qualys depuis les secrets chiffres"""
|
|
url = get_secret(db, "qualys_url") or "https://qualysapi.qualys.eu"
|
|
user = get_secret(db, "qualys_user") or ""
|
|
pwd = get_secret(db, "qualys_pass") or ""
|
|
proxy = get_secret(db, "qualys_proxy") or ""
|
|
return url, user, pwd, proxy
|
|
|
|
|
|
def parse_xml(txt, tag):
|
|
return re.findall(f"<{tag}>([^<]*)</{tag}>", txt)
|
|
|
|
|
|
def sync_server_qualys(db, server_id):
|
|
"""Sync les tags Qualys pour un serveur donne. Retourne un dict resultat."""
|
|
row = db.execute(text(
|
|
"SELECT hostname, qualys_asset_id FROM servers WHERE id = :id"
|
|
), {"id": server_id}).fetchone()
|
|
if not row:
|
|
return {"ok": False, "msg": "Serveur introuvable"}
|
|
|
|
hostname = row.hostname
|
|
qid = row.qualys_asset_id
|
|
|
|
qualys_url, qualys_user, qualys_pass, qualys_proxy = _get_qualys_creds(db)
|
|
if not qualys_user:
|
|
return {"ok": False, "msg": "Credentials Qualys non configures (Settings)"}
|
|
proxies = {"https": qualys_proxy, "http": qualys_proxy} if qualys_proxy else None
|
|
|
|
# Chercher l'asset par hostname si pas de qualys_asset_id
|
|
if not qid:
|
|
qid = _find_asset_by_hostname(qualys_url, qualys_user, qualys_pass, hostname, proxies)
|
|
if not qid:
|
|
return {"ok": False, "msg": f"Asset '{hostname}' non trouve dans Qualys"}
|
|
db.execute(text("UPDATE servers SET qualys_asset_id = :qid WHERE id = :id"),
|
|
{"qid": qid, "id": server_id})
|
|
|
|
# Recuperer l'asset complet avec tags
|
|
try:
|
|
r = requests.post(
|
|
f"{qualys_url}/qps/rest/2.0/search/am/hostasset",
|
|
json={"ServiceRequest": {
|
|
"filters": {"Criteria": [
|
|
{"field": "id", "operator": "EQUALS", "value": str(qid)}
|
|
]}
|
|
}},
|
|
auth=(qualys_user, qualys_pass),
|
|
verify=False, timeout=60, proxies=proxies,
|
|
headers={"Content-Type": "application/json"}
|
|
)
|
|
except Exception as e:
|
|
return {"ok": False, "msg": f"Erreur API: {e}"}
|
|
|
|
if r.status_code != 200 or "SUCCESS" not in r.text:
|
|
return {"ok": False, "msg": f"API HTTP {r.status_code}"}
|
|
|
|
# Parser asset
|
|
blocks = r.text.split("<HostAsset>")
|
|
if len(blocks) < 2:
|
|
return {"ok": False, "msg": "Asset non trouve dans la reponse"}
|
|
|
|
block = blocks[1].split("</HostAsset>")[0]
|
|
fqdn = (parse_xml(block, "fqdn") or [""])[0]
|
|
address = (parse_xml(block, "address") or [""])[0]
|
|
os_val = (parse_xml(block, "os") or [""])[0]
|
|
agent_status = (parse_xml(block, "status") or [""])[0] if "<agentInfo>" in block else ""
|
|
agent_version = (parse_xml(block, "agentVersion") or [""])[0]
|
|
last_checkin = (parse_xml(block, "lastCheckedIn") or [""])[0] or None
|
|
|
|
os_family = None
|
|
os_low = os_val.lower()
|
|
if any(k in os_low for k in ("linux", "red hat", "centos", "debian")):
|
|
os_family = "linux"
|
|
elif "windows" in os_low:
|
|
os_family = "windows"
|
|
|
|
# Update qualys_assets
|
|
db.execute(text("""
|
|
INSERT INTO qualys_assets (qualys_asset_id, name, hostname, fqdn, ip_address, os, os_family,
|
|
agent_status, agent_version, last_checkin, server_id)
|
|
VALUES (:qid, :name, :hn, :fqdn, :ip, :os, :osf, :ast, :av, :lc, :sid)
|
|
ON CONFLICT (qualys_asset_id) DO UPDATE SET
|
|
fqdn=EXCLUDED.fqdn, ip_address=EXCLUDED.ip_address, os=EXCLUDED.os,
|
|
os_family=EXCLUDED.os_family, agent_status=EXCLUDED.agent_status,
|
|
agent_version=EXCLUDED.agent_version, last_checkin=EXCLUDED.last_checkin, updated_at=now()
|
|
"""), {"qid": qid, "name": hostname, "hn": hostname.split(".")[0].lower(),
|
|
"fqdn": fqdn or None, "ip": address or None, "os": os_val, "osf": os_family,
|
|
"ast": agent_status, "av": agent_version, "lc": last_checkin, "sid": server_id})
|
|
|
|
# Enrichir servers
|
|
db.execute(text("""
|
|
UPDATE servers SET
|
|
fqdn = COALESCE(NULLIF(:fqdn, ''), fqdn),
|
|
os_version = COALESCE(NULLIF(:os, ''), os_version)
|
|
WHERE id = :id
|
|
"""), {"fqdn": fqdn, "os": os_val, "id": server_id})
|
|
|
|
# Tags
|
|
tag_count = 0
|
|
if "<tags>" in block:
|
|
tag_block = block.split("<tags>")[1].split("</tags>")[0]
|
|
tag_ids = parse_xml(tag_block, "id")
|
|
tag_names = parse_xml(tag_block, "name")
|
|
|
|
# Supprimer anciens liens
|
|
db.execute(text("DELETE FROM qualys_asset_tags WHERE qualys_asset_id = :qid"), {"qid": qid})
|
|
|
|
for tid, tname in zip(tag_ids, tag_names):
|
|
# Upsert tag
|
|
db.execute(text("""
|
|
INSERT INTO qualys_tags (qualys_tag_id, name) VALUES (:tid, :tn)
|
|
ON CONFLICT (qualys_tag_id) DO UPDATE SET name=EXCLUDED.name, updated_at=now()
|
|
"""), {"tid": int(tid), "tn": tname})
|
|
# Lien asset-tag
|
|
db.execute(text("""
|
|
INSERT INTO qualys_asset_tags (qualys_asset_id, qualys_tag_id)
|
|
VALUES (:qid, :tid) ON CONFLICT DO NOTHING
|
|
"""), {"qid": qid, "tid": int(tid)})
|
|
tag_count += 1
|
|
|
|
db.commit()
|
|
return {"ok": True, "msg": f"Synchro OK — {tag_count} tags", "tags": tag_count}
|
|
|
|
|
|
def _find_asset_by_hostname(qualys_url, qualys_user, qualys_pass, hostname, proxies=None):
|
|
"""Cherche un asset Qualys par hostname"""
|
|
try:
|
|
r = requests.post(
|
|
f"{qualys_url}/qps/rest/2.0/search/am/hostasset",
|
|
json={"ServiceRequest": {
|
|
"preferences": {"limitResults": 5},
|
|
"filters": {"Criteria": [
|
|
{"field": "name", "operator": "CONTAINS", "value": hostname}
|
|
]}
|
|
}},
|
|
auth=(qualys_user, qualys_pass),
|
|
verify=False, timeout=60, proxies=proxies,
|
|
headers={"Content-Type": "application/json"}
|
|
)
|
|
if r.status_code == 200 and "SUCCESS" in r.text:
|
|
ids = parse_xml(r.text, "id")
|
|
if ids:
|
|
return int(ids[0])
|
|
except Exception:
|
|
pass
|
|
return None
|