diff --git a/app/routers/qualys.py b/app/routers/qualys.py index 8f60933..3898d4d 100644 --- a/app/routers/qualys.py +++ b/app/routers/qualys.py @@ -3,7 +3,7 @@ from fastapi import APIRouter, Request, Depends, Query, Form from fastapi.responses import HTMLResponse, RedirectResponse, StreamingResponse from fastapi.templating import Jinja2Templates from sqlalchemy import text -import csv, io, re +import csv, io, re, os from ..dependencies import get_db, get_current_user, get_user_perms, can_view, can_edit, can_admin, base_context from ..services.qualys_service import ( sync_server_qualys, search_assets_api, get_all_tags_api, @@ -833,3 +833,167 @@ async def qualys_decoder(request: Request, db=Depends(get_db), "current_tags": current_tags, "auto_prefixes": AUTO_PREFIXES, }) return templates.TemplateResponse("qualys_decoder.html", ctx) + + +# ═══════════════════════════════════════════════ +# DEPLOIEMENT AGENT QUALYS +# ═══════════════════════════════════════════════ + +@router.get("/qualys/deploy", response_class=HTMLResponse) +async def qualys_deploy_page(request: Request, db=Depends(get_db)): + user = get_current_user(request) + if not user: + return RedirectResponse(url="/login") + perms = get_user_perms(db, user) + if not can_edit(perms, "qualys"): + return RedirectResponse(url="/dashboard") + + from ..services.agent_deploy_service import list_packages + from ..services.secrets_service import get_secret + + packages = list_packages() + servers = db.execute(text(""" + SELECT s.id, s.hostname, s.os_family, s.etat, s.ssh_user, s.ssh_port, s.ssh_method, + d.name as domain, e.name as env + FROM servers s + LEFT JOIN domain_environments de ON s.domain_env_id = de.id + LEFT JOIN domains d ON de.domain_id = d.id + LEFT JOIN environments e ON de.environment_id = e.id + ORDER BY s.hostname + """)).fetchall() + servers = [dict(r._mapping) for r in servers] + + ctx = base_context(request, db, user) + ctx.update({ + "app_name": APP_NAME, + "packages": packages, + "servers": servers, + "activation_id": get_secret(db, "qualys_activation_id") or "081a9a12-ca97-4de1-828c-c6ad918ce77e", + "customer_id": get_secret(db, "qualys_customer_id") or "a2e3271b-c2a1-ec6b-8324-8f51948783d4", + "server_uri": get_secret(db, "qualys_server_uri") or "https://qagpublic.qg2.apps.qualys.eu/CloudAgent/", + "msg": request.query_params.get("msg", ""), + }) + return templates.TemplateResponse("qualys_deploy.html", ctx) + + +@router.post("/qualys/deploy/run") +async def qualys_deploy_run(request: Request, db=Depends(get_db), + server_ids: str = Form(""), + activation_id: str = Form(""), + customer_id: str = Form(""), + server_uri: str = Form(""), + package_deb: str = Form(""), + package_rpm: str = Form("")): + user = get_current_user(request) + if not user: + return RedirectResponse(url="/login") + perms = get_user_perms(db, user) + if not can_edit(perms, "qualys"): + return RedirectResponse(url="/dashboard") + + from ..services.agent_deploy_service import deploy_agent + from ..services.secrets_service import get_secret + + ids = [int(x) for x in server_ids.split(",") if x.strip().isdigit()] + if not ids: + return RedirectResponse(url="/qualys/deploy?msg=no_servers", status_code=303) + + ssh_key = get_secret(db, "ssh_key_file") or "/opt/patchcenter/keys/id_ed25519" + + # Get servers + placeholders = ",".join(str(i) for i in ids) + servers = db.execute(text(f""" + SELECT id, hostname, os_family, ssh_user, ssh_port, ssh_method + FROM servers WHERE id IN ({placeholders}) + """)).fetchall() + + results = [] + log_lines = [] + + for srv in servers: + # Choose package based on OS + if "debian" in (srv.os_family or "").lower() or srv.os_family == "linux": + pkg = package_deb + else: + pkg = package_rpm + + if not pkg or not os.path.exists(pkg): + results.append({"hostname": srv.hostname, "status": "FAILED", "detail": f"Package introuvable: {pkg}"}) + continue + + def on_line(msg, h=srv.hostname): + log_lines.append(f"[{h}] {msg}") + + result = deploy_agent( + hostname=srv.hostname, + ssh_user=srv.ssh_user or "root", + ssh_key_path=ssh_key, + ssh_port=srv.ssh_port or 22, + os_family=srv.os_family, + package_path=pkg, + activation_id=activation_id, + customer_id=customer_id, + server_uri=server_uri, + on_line=on_line, + ) + results.append(result) + + # Save to app state for display + request.app.state.last_deploy_results = results + request.app.state.last_deploy_log = log_lines + + # Audit log + from ..services.audit_service import log_action + ok = sum(1 for r in results if r["status"] in ("SUCCESS", "ALREADY_INSTALLED")) + fail = sum(1 for r in results if r["status"] not in ("SUCCESS", "ALREADY_INSTALLED")) + log_action(db, request, user, "qualys_deploy", f"{ok} OK, {fail} fail sur {len(results)} serveurs") + db.commit() + + ctx = base_context(request, db, user) + ctx.update({ + "app_name": APP_NAME, + "results": results, + "log_lines": log_lines, + "total": len(results), + "ok": ok, + "failed": fail, + }) + return templates.TemplateResponse("qualys_deploy_results.html", ctx) + + +@router.post("/qualys/deploy/check") +async def qualys_deploy_check(request: Request, db=Depends(get_db), + server_ids: str = Form("")): + user = get_current_user(request) + if not user: + return RedirectResponse(url="/login") + perms = get_user_perms(db, user) + if not can_view(perms, "qualys"): + return RedirectResponse(url="/dashboard") + + from ..services.agent_deploy_service import check_agent + from ..services.secrets_service import get_secret + + ids = [int(x) for x in server_ids.split(",") if x.strip().isdigit()] + if not ids: + return RedirectResponse(url="/qualys/deploy?msg=no_servers", status_code=303) + + ssh_key = get_secret(db, "ssh_key_file") or "/opt/patchcenter/keys/id_ed25519" + placeholders = ",".join(str(i) for i in ids) + servers = db.execute(text(f"SELECT id, hostname, ssh_user, ssh_port FROM servers WHERE id IN ({placeholders})")).fetchall() + + results = [] + for srv in servers: + r = check_agent(srv.hostname, srv.ssh_user or "root", ssh_key, srv.ssh_port or 22) + results.append(r) + + ctx = base_context(request, db, user) + ctx.update({ + "app_name": APP_NAME, + "results": results, + "total": len(results), + "active": sum(1 for r in results if r["status"] == "ACTIVE"), + "not_installed": sum(1 for r in results if r["status"] == "NOT_INSTALLED"), + "failed": sum(1 for r in results if r["status"] == "CONNECTION_FAILED"), + }) + return templates.TemplateResponse("qualys_deploy_results.html", ctx) diff --git a/app/services/agent_deploy_service.py b/app/services/agent_deploy_service.py new file mode 100644 index 0000000..af952a8 --- /dev/null +++ b/app/services/agent_deploy_service.py @@ -0,0 +1,217 @@ +"""Service de deploiement Qualys Cloud Agent via SSH""" +import os +import logging +import glob +from datetime import datetime + +log = logging.getLogger(__name__) + +AGENTS_DIR = "/opt/patchcenter/agents" + +try: + import paramiko + PARAMIKO_OK = True +except ImportError: + PARAMIKO_OK = False + + +def list_packages(): + """Liste les packages disponibles dans /opt/patchcenter/agents/""" + packages = {"deb": [], "rpm": []} + if not os.path.isdir(AGENTS_DIR): + return packages + for f in sorted(os.listdir(AGENTS_DIR)): + path = os.path.join(AGENTS_DIR, f) + size_mb = round(os.path.getsize(path) / 1024 / 1024, 1) + if f.endswith(".deb"): + packages["deb"].append({"name": f, "path": path, "size": size_mb}) + elif f.endswith(".rpm"): + packages["rpm"].append({"name": f, "path": path, "size": size_mb}) + return packages + + +def _get_ssh_client(hostname, ssh_user, ssh_key_path, ssh_port=22): + """Crée un client SSH paramiko""" + if not PARAMIKO_OK: + return None, "paramiko non disponible" + client = paramiko.SSHClient() + client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + try: + for cls in [paramiko.Ed25519Key, paramiko.RSAKey, paramiko.ECDSAKey]: + try: + key = cls.from_private_key_file(ssh_key_path) + client.connect(hostname, port=ssh_port, username=ssh_user, pkey=key, + timeout=15, look_for_keys=False, allow_agent=False) + return client, None + except Exception: + continue + return None, f"Impossible de charger la cle {ssh_key_path}" + except Exception as e: + return None, str(e) + + +def _run_cmd(client, cmd, sudo=False, timeout=120): + """Execute une commande SSH""" + if sudo: + _, stdout_id, _ = client.exec_command("id -u", timeout=5) + uid = stdout_id.read().decode().strip() + if uid != "0": + cmd = f"sudo {cmd}" + _, stdout, stderr = client.exec_command(cmd, timeout=timeout) + exit_code = stdout.channel.recv_exit_status() + out = stdout.read().decode("utf-8", errors="replace") + err = stderr.read().decode("utf-8", errors="replace") + return exit_code, out, err + + +def check_agent(hostname, ssh_user, ssh_key_path, ssh_port=22): + """Vérifie le statut de l'agent Qualys sur un serveur""" + client, error = _get_ssh_client(hostname, ssh_user, ssh_key_path, ssh_port) + if not client: + return {"hostname": hostname, "status": "CONNECTION_FAILED", "detail": error} + + result = {"hostname": hostname} + + # Check if installed + code, out, _ = _run_cmd(client, "which qualys-cloud-agent 2>/dev/null || rpm -q qualys-cloud-agent 2>/dev/null || dpkg -l qualys-cloud-agent 2>/dev/null | grep '^ii'") + if code != 0 and not out.strip(): + result["status"] = "NOT_INSTALLED" + result["detail"] = "Agent non installe" + client.close() + return result + + # Check if running + code, out, _ = _run_cmd(client, "systemctl is-active qualys-cloud-agent 2>/dev/null") + status = out.strip() + result["service_status"] = status + + # Get version + code, out, _ = _run_cmd(client, "qualys-cloud-agent --version 2>/dev/null || cat /etc/qualys/cloud-agent/qualys-cloud-agent.conf 2>/dev/null | grep -i version | head -1") + result["version"] = out.strip()[:50] + + # Get last checkin from log + code, out, _ = _run_cmd(client, "tail -5 /var/log/qualys/qualys-cloud-agent.log 2>/dev/null | grep 'HTTP response code: 200' | tail -1 | awk '{print $1, $2}'") + result["last_checkin"] = out.strip()[:25] + + if status == "active": + result["status"] = "ACTIVE" + result["detail"] = "Agent actif" + elif status == "inactive" or status == "dead": + result["status"] = "INACTIVE" + result["detail"] = "Agent installe mais inactif" + else: + result["status"] = "UNKNOWN" + result["detail"] = f"Statut: {status}" + + client.close() + return result + + +def deploy_agent(hostname, ssh_user, ssh_key_path, ssh_port, os_family, + package_path, activation_id, customer_id, server_uri, + on_line=None): + """Deploie l'agent Qualys sur un serveur""" + + def emit(msg): + if on_line: + on_line(msg) + log.info(f"[{hostname}] {msg}") + + emit(f"Connexion SSH {ssh_user}@{hostname}:{ssh_port}...") + client, error = _get_ssh_client(hostname, ssh_user, ssh_key_path, ssh_port) + if not client: + emit(f"ERREUR connexion: {error}") + return {"hostname": hostname, "status": "FAILED", "detail": error} + + result = {"hostname": hostname, "status": "PENDING"} + + try: + # 1. Detect OS if not provided + if not os_family: + code, out, _ = _run_cmd(client, "cat /etc/os-release 2>/dev/null | head -3") + os_family = "linux" # default + if "debian" in out.lower() or "ubuntu" in out.lower(): + os_family = "debian" + elif "red hat" in out.lower() or "centos" in out.lower() or "rocky" in out.lower(): + os_family = "rhel" + emit(f"OS detecte: {os_family}") + + # 2. Check if already installed + code, out, _ = _run_cmd(client, "systemctl is-active qualys-cloud-agent 2>/dev/null") + if out.strip() == "active": + emit("Agent deja installe et actif - skip") + result["status"] = "ALREADY_INSTALLED" + result["detail"] = "Agent deja actif" + client.close() + return result + + # 3. Copy package + pkg_name = os.path.basename(package_path) + remote_path = f"/tmp/{pkg_name}" + emit(f"Copie {pkg_name} ({os.path.getsize(package_path)//1024//1024} Mo)...") + + sftp = client.open_sftp() + sftp.put(package_path, remote_path) + sftp.close() + emit("Copie terminee") + + # 4. Install + is_deb = pkg_name.endswith(".deb") + if is_deb: + emit("Installation (dpkg)...") + code, out, err = _run_cmd(client, f"dpkg --install {remote_path}", sudo=True, timeout=120) + else: + emit("Installation (rpm)...") + code, out, err = _run_cmd(client, f"rpm -ivh --nosignature {remote_path}", sudo=True, timeout=120) + + if code != 0 and "already installed" not in (out + err).lower(): + emit(f"ERREUR installation (code {code}): {err[:200]}") + result["status"] = "INSTALL_FAILED" + result["detail"] = err[:200] + client.close() + return result + emit("Installation OK") + + # 5. Activate + emit("Activation de l'agent...") + activate_cmd = ( + f"/usr/local/qualys/cloud-agent/bin/qualys-cloud-agent.sh " + f"ActivationId={activation_id} " + f"CustomerId={customer_id} " + f"ServerUri={server_uri} " + f"ProviderName=NONE" + ) + code, out, err = _run_cmd(client, activate_cmd, sudo=True, timeout=60) + if code != 0: + emit(f"ERREUR activation (code {code}): {err[:200]}") + result["status"] = "ACTIVATE_FAILED" + result["detail"] = err[:200] + client.close() + return result + emit("Activation OK") + + # 6. Restart service + emit("Redemarrage du service...") + _run_cmd(client, "systemctl restart qualys-cloud-agent", sudo=True) + + # 7. Verify + code, out, _ = _run_cmd(client, "systemctl is-active qualys-cloud-agent") + if out.strip() == "active": + emit("Agent deploye et actif !") + result["status"] = "SUCCESS" + result["detail"] = "Agent deploye avec succes" + else: + emit(f"Agent installe mais statut: {out.strip()}") + result["status"] = "PARTIAL" + result["detail"] = f"Installe, service: {out.strip()}" + + # 8. Cleanup + _run_cmd(client, f"rm -f {remote_path}") + + except Exception as e: + emit(f"ERREUR: {e}") + result["status"] = "FAILED" + result["detail"] = str(e)[:200] + + client.close() + return result diff --git a/app/templates/base.html b/app/templates/base.html index 244d073..7bdde1e 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -59,7 +59,8 @@ {% if p.qualys %}Qualys{% endif %} {% if p.qualys %}Tags{% endif %} {% if p.qualys %}Décodeur{% endif %} - {% if p.qualys %}Agents{% endif %} + {% if p.qualys %}Agents{% endif %} + {% if p.qualys in ('edit', 'admin') %}Déployer Agent{% endif %} {% if p.campaigns %}Safe Patching{% endif %} {% if p.campaigns or p.quickwin %}QuickWin{% endif %} {% if p.campaigns or p.quickwin %}Config exclusions{% endif %} diff --git a/app/templates/qualys_deploy.html b/app/templates/qualys_deploy.html new file mode 100644 index 0000000..cb6708c --- /dev/null +++ b/app/templates/qualys_deploy.html @@ -0,0 +1,146 @@ +{% extends 'base.html' %} +{% block title %}Déploiement Agent Qualys{% endblock %} +{% block content %} +
Installer et vérifier l'agent Qualys Cloud sur les serveurs
+| s.id) : []" + x-init="servers = {{ servers | tojson }}"> | +Hostname | +OS | +Domaine | +Env | +État | +SSH | +
|---|---|---|---|---|---|---|
| x !== {{ s.id }})"> | +{{ s.hostname }} | ++ {% if s.os_family == 'linux' %}Linux + {% else %}{{ s.os_family or '?' }}{% endif %} + | +{{ s.domain or '-' }} | +{{ s.env or '-' }} | ++ {% if s.etat == 'en_production' %}Prod + {% else %}{{ s.etat or '-' }}{% endif %} + | +{{ s.ssh_user or 'root' }}:{{ s.ssh_port or 22 }} | +
{{ total }} serveur(s) traité(s)
+| Hostname | +Statut | +Détail | + {% if results and results[0].version is defined %} +Version | +Service | + {% endif %} +
|---|---|---|---|---|
| {{ r.hostname }} | ++ {% if r.status == 'SUCCESS' or r.status == 'ACTIVE' %} + {{ r.status }} + {% elif r.status == 'ALREADY_INSTALLED' %} + DÉJÀ INSTALLÉ + {% elif r.status == 'NOT_INSTALLED' %} + NON INSTALLÉ + {% elif r.status == 'INACTIVE' %} + INACTIF + {% else %} + {{ r.status }} + {% endif %} + | +{{ r.detail or r.get('detail', '') }} | + {% if r.version is defined %} +{{ r.version or '-' }} | +{{ r.service_status or '-' }} | + {% endif %} +