Add module Qualys Tags V3: catalogue YAML + service + pages /qualys/tagsv3 et /gap
- deploy/qualys_tags_v3.yaml: catalogue 19 DYN + 6 SPEC manuel + prefixes - app/services/qualys_tags_service.py: list/analyze_gap/create_static/delete via API - app/routers/qualys_tags.py: routes /qualys/tagsv3 et /gap - templates: qualys_tagsv3.html (liste) + qualys_tagsv3_gap.html (diff catalogue) - Route /qualys/tagsv3/create-all-static pour creer les STAT manquants en bulk - DYN manquants affiches avec QQL copy-paste pour console Qualys (API ne permet pas) - PyYAML ajoute aux requirements
This commit is contained in:
parent
105a756008
commit
ec7712f0c9
@ -6,7 +6,7 @@ from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from .config import APP_NAME, APP_VERSION
|
||||
from .dependencies import get_current_user, get_user_perms
|
||||
from .database import SessionLocal, SessionLocalDemo
|
||||
from .routers import auth, dashboard, servers, settings, users, campaigns, planning, specifics, audit, contacts, qualys, quickwin, referentiel, patching, applications
|
||||
from .routers import auth, dashboard, servers, settings, users, campaigns, planning, specifics, audit, contacts, qualys, qualys_tags, quickwin, referentiel, patching, applications
|
||||
|
||||
|
||||
class PermissionsMiddleware(BaseHTTPMiddleware):
|
||||
@ -60,6 +60,7 @@ app.include_router(specifics.router)
|
||||
app.include_router(audit.router)
|
||||
app.include_router(contacts.router)
|
||||
app.include_router(qualys.router)
|
||||
app.include_router(qualys_tags.router)
|
||||
app.include_router(quickwin.router)
|
||||
app.include_router(referentiel.router)
|
||||
app.include_router(patching.router)
|
||||
|
||||
105
app/routers/qualys_tags.py
Normal file
105
app/routers/qualys_tags.py
Normal file
@ -0,0 +1,105 @@
|
||||
"""Router Qualys Tags V3 — liste, gap analysis, creation"""
|
||||
from fastapi import APIRouter, Request, Depends, Form
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from ..dependencies import get_db, get_current_user, get_user_perms, can_view, can_edit
|
||||
from ..services.qualys_tags_service import (
|
||||
list_qualys_tags, analyze_gap, create_static_tag,
|
||||
delete_tag, generate_console_steps, load_catalog,
|
||||
)
|
||||
from ..config import APP_NAME
|
||||
|
||||
router = APIRouter()
|
||||
templates = Jinja2Templates(directory="app/templates")
|
||||
|
||||
|
||||
@router.get("/qualys/tagsv3", response_class=HTMLResponse)
|
||||
def tags_list_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_view(perms, "qualys"):
|
||||
return RedirectResponse(url="/dashboard")
|
||||
|
||||
result = list_qualys_tags(db)
|
||||
tags = result.get("tags", [])
|
||||
tags.sort(key=lambda t: (t["type"], t["name"]))
|
||||
stats = {
|
||||
"total": len(tags),
|
||||
"dyn": sum(1 for t in tags if t["type"] == "DYN"),
|
||||
"stat": sum(1 for t in tags if t["type"] == "STAT"),
|
||||
}
|
||||
return templates.TemplateResponse("qualys_tagsv3.html", {
|
||||
"request": request, "user": user, "app_name": APP_NAME,
|
||||
"tags": tags, "stats": stats,
|
||||
"error": result.get("msg") if not result.get("ok") else None,
|
||||
})
|
||||
|
||||
|
||||
@router.get("/qualys/tagsv3/gap", response_class=HTMLResponse)
|
||||
def tags_gap_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_view(perms, "qualys"):
|
||||
return RedirectResponse(url="/dashboard")
|
||||
|
||||
gap = analyze_gap(db)
|
||||
console_steps = generate_console_steps(gap.get("missing_dyn", [])) if gap.get("ok") else []
|
||||
can_modify = can_edit(perms, "qualys")
|
||||
return templates.TemplateResponse("qualys_tagsv3_gap.html", {
|
||||
"request": request, "user": user, "app_name": APP_NAME,
|
||||
"gap": gap, "console_steps": console_steps, "can_modify": can_modify,
|
||||
})
|
||||
|
||||
|
||||
@router.post("/qualys/tagsv3/create-static")
|
||||
def tags_create_static(request: Request, db=Depends(get_db),
|
||||
name: str = Form(...), color: str = Form(""),
|
||||
description: 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="/qualys/tagsv3/gap?msg=denied")
|
||||
r = create_static_tag(db, name, color, description)
|
||||
msg = "created" if r.get("ok") else f"error_{r.get('msg', 'unknown')[:30]}"
|
||||
return RedirectResponse(url=f"/qualys/tagsv3/gap?msg={msg}", status_code=303)
|
||||
|
||||
|
||||
@router.post("/qualys/tagsv3/create-all-static")
|
||||
def tags_create_all_static(request: Request, db=Depends(get_db)):
|
||||
"""Cree tous les tags STAT manquants en un coup."""
|
||||
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="/qualys/tagsv3/gap?msg=denied")
|
||||
|
||||
gap = analyze_gap(db)
|
||||
if not gap.get("ok"):
|
||||
return RedirectResponse(url="/qualys/tagsv3/gap?msg=qualys_err", status_code=303)
|
||||
|
||||
created = failed = 0
|
||||
for t in gap.get("missing_stat", []):
|
||||
r = create_static_tag(db, t["name"], t["color"], t.get("description", ""))
|
||||
if r.get("ok"):
|
||||
created += 1
|
||||
else:
|
||||
failed += 1
|
||||
return RedirectResponse(
|
||||
url=f"/qualys/tagsv3/gap?msg=bulk_created_{created}_failed_{failed}",
|
||||
status_code=303,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/qualys/tagsv3/catalog", response_class=JSONResponse)
|
||||
def tags_catalog_json(request: Request, db=Depends(get_db)):
|
||||
user = get_current_user(request)
|
||||
if not user:
|
||||
return JSONResponse({"error": "Non autorise"}, status_code=403)
|
||||
return load_catalog()
|
||||
192
app/services/qualys_tags_service.py
Normal file
192
app/services/qualys_tags_service.py
Normal file
@ -0,0 +1,192 @@
|
||||
"""Service Qualys Tags V3 — CRUD + analyse gap vs catalogue.
|
||||
|
||||
Limitations API Qualys (confirmées par la V3) :
|
||||
- Tag dynamique : creation impossible via API -> UI console uniquement
|
||||
- Tag statique : creation + assignment OK via API
|
||||
- Lecture tous tags (avec ruleType DYNAMIC/STATIC) OK
|
||||
"""
|
||||
import os
|
||||
import requests
|
||||
from pathlib import Path
|
||||
from sqlalchemy import text
|
||||
|
||||
# Path vers le catalogue V3
|
||||
CATALOG_PATH = Path(__file__).parent.parent.parent / "deploy" / "qualys_tags_v3.yaml"
|
||||
|
||||
|
||||
def _get_creds(db):
|
||||
"""Retourne (url, user, pass, proxy) depuis app_secrets."""
|
||||
from .secrets_service import get_secret
|
||||
return (
|
||||
get_secret(db, "qualys_url"),
|
||||
get_secret(db, "qualys_user"),
|
||||
get_secret(db, "qualys_pass"),
|
||||
get_secret(db, "qualys_proxy"),
|
||||
)
|
||||
|
||||
|
||||
def _qualys_post(db, endpoint, payload, timeout=60):
|
||||
"""POST sur Qualys QPS REST 2.0."""
|
||||
url, user, pwd, proxy = _get_creds(db)
|
||||
if not user:
|
||||
return {"ok": False, "msg": "Credentials Qualys non configures"}
|
||||
proxies = {"https": proxy, "http": proxy} if proxy else None
|
||||
try:
|
||||
r = requests.post(
|
||||
f"{url}{endpoint}",
|
||||
json=payload, auth=(user, pwd),
|
||||
verify=False, timeout=timeout, proxies=proxies,
|
||||
headers={"X-Requested-With": "PatchCenter", "Content-Type": "application/json"},
|
||||
)
|
||||
return {"ok": r.status_code == 200, "status": r.status_code, "text": r.text}
|
||||
except Exception as e:
|
||||
return {"ok": False, "msg": str(e)[:200]}
|
||||
|
||||
|
||||
def _parse_xml_text(text_block, tag):
|
||||
"""Extrait <tag>valeur</tag> (premier match)."""
|
||||
import re
|
||||
m = re.search(f"<{tag}>(.*?)</{tag}>", text_block, re.DOTALL)
|
||||
return m.group(1).strip() if m else ""
|
||||
|
||||
|
||||
def list_qualys_tags(db):
|
||||
"""Liste tous les tags Qualys (nom, id, type DYN/STAT, ruleText)."""
|
||||
# search all tags
|
||||
payload = {"ServiceRequest": {"preferences": {"limitResults": 1000}}}
|
||||
r = _qualys_post(db, "/qps/rest/2.0/search/am/tag", payload)
|
||||
if not r.get("ok"):
|
||||
return {"ok": False, "msg": r.get("msg") or f"HTTP {r.get('status')}", "tags": []}
|
||||
tags = []
|
||||
for block in r["text"].split("<Tag>")[1:]:
|
||||
block = block.split("</Tag>")[0]
|
||||
tid = _parse_xml_text(block, "id")
|
||||
name = _parse_xml_text(block, "name")
|
||||
rule_type = _parse_xml_text(block, "ruleType")
|
||||
rule_text = _parse_xml_text(block, "ruleText")
|
||||
color = _parse_xml_text(block, "color")
|
||||
if tid and name:
|
||||
tags.append({
|
||||
"id": int(tid),
|
||||
"name": name,
|
||||
"type": "DYN" if rule_type else "STAT",
|
||||
"rule_type": rule_type,
|
||||
"rule_text": rule_text,
|
||||
"color": color,
|
||||
})
|
||||
return {"ok": True, "tags": tags}
|
||||
|
||||
|
||||
def load_catalog():
|
||||
"""Charge le catalogue YAML V3."""
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
return {"error": "pyyaml non installe (pip install pyyaml)"}
|
||||
if not CATALOG_PATH.exists():
|
||||
return {"error": f"Catalogue introuvable: {CATALOG_PATH}"}
|
||||
with open(CATALOG_PATH, "r", encoding="utf-8") as f:
|
||||
return yaml.safe_load(f)
|
||||
|
||||
|
||||
def analyze_gap(db):
|
||||
"""Compare catalogue V3 vs tags Qualys. Retourne missing/extra/mismatch."""
|
||||
catalog = load_catalog()
|
||||
if "error" in catalog:
|
||||
return {"ok": False, "msg": catalog["error"]}
|
||||
|
||||
ql = list_qualys_tags(db)
|
||||
if not ql.get("ok"):
|
||||
return ql
|
||||
|
||||
qualys_by_name = {t["name"]: t for t in ql["tags"]}
|
||||
catalog_tags = []
|
||||
for cat_name, cat in catalog.get("categories", {}).items():
|
||||
for t in cat.get("tags", []) or []:
|
||||
catalog_tags.append({
|
||||
"category": cat_name,
|
||||
"name": t["name"],
|
||||
"type": t["type"],
|
||||
"auto": t.get("auto", False),
|
||||
"qql": t.get("qql", ""),
|
||||
"color": t.get("color", ""),
|
||||
"description": t.get("description", ""),
|
||||
})
|
||||
|
||||
missing_dyn = [] # Dans catalogue, pas dans Qualys, type DYN -> creer en console
|
||||
missing_stat = [] # Dans catalogue, pas dans Qualys, type STAT -> creable via API
|
||||
mismatch = [] # Present mais type different (DYN vs STAT)
|
||||
present = [] # OK
|
||||
|
||||
for c in catalog_tags:
|
||||
q = qualys_by_name.get(c["name"])
|
||||
if not q:
|
||||
if c["type"] == "DYN":
|
||||
missing_dyn.append(c)
|
||||
else:
|
||||
missing_stat.append(c)
|
||||
elif c["type"] != q["type"]:
|
||||
mismatch.append({"catalog": c, "qualys": q})
|
||||
else:
|
||||
present.append({"catalog": c, "qualys": q})
|
||||
|
||||
# Tags Qualys pas dans le catalogue (pour info)
|
||||
catalog_names = {c["name"] for c in catalog_tags}
|
||||
extra = [t for t in ql["tags"] if t["name"] not in catalog_names]
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"present": present,
|
||||
"missing_dyn": missing_dyn,
|
||||
"missing_stat": missing_stat,
|
||||
"mismatch": mismatch,
|
||||
"extra": extra,
|
||||
"total_catalog": len(catalog_tags),
|
||||
"total_qualys": len(ql["tags"]),
|
||||
}
|
||||
|
||||
|
||||
def create_static_tag(db, name, color="", description=""):
|
||||
"""Cree un tag statique Qualys via API."""
|
||||
payload = {
|
||||
"ServiceRequest": {
|
||||
"data": {
|
||||
"Tag": {
|
||||
"name": name,
|
||||
"color": color or "#607D8B",
|
||||
"description": description,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
r = _qualys_post(db, "/qps/rest/2.0/create/am/tag", payload)
|
||||
if r.get("ok"):
|
||||
return {"ok": True, "msg": f"Tag '{name}' cree"}
|
||||
return {"ok": False, "msg": r.get("msg") or f"HTTP {r.get('status')}: {r.get('text', '')[:200]}"}
|
||||
|
||||
|
||||
def delete_tag(db, tag_id):
|
||||
"""Supprime un tag Qualys par id."""
|
||||
r = _qualys_post(db, f"/qps/rest/2.0/delete/am/tag/{tag_id}", {})
|
||||
return {"ok": r.get("ok"), "msg": r.get("msg") or f"HTTP {r.get('status')}"}
|
||||
|
||||
|
||||
def generate_console_steps(missing_dyn):
|
||||
"""Retourne un texte avec les etapes console Qualys pour chaque tag DYN manquant."""
|
||||
steps = []
|
||||
for t in missing_dyn:
|
||||
steps.append({
|
||||
"name": t["name"],
|
||||
"category": t["category"],
|
||||
"qql": t["qql"],
|
||||
"color": t["color"],
|
||||
"steps": [
|
||||
"VMDR > Assets > Tags > New Tag",
|
||||
f"Name: {t['name']}",
|
||||
f"Color: {t['color']}",
|
||||
"Cocher 'Create as Dynamic Tag'",
|
||||
f"Asset Criteria: {t['qql']}",
|
||||
"Sauvegarder",
|
||||
],
|
||||
})
|
||||
return steps
|
||||
44
app/templates/qualys_tagsv3.html
Normal file
44
app/templates/qualys_tagsv3.html
Normal file
@ -0,0 +1,44 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Tags V3 - Vue Qualys{% endblock %}
|
||||
{% block content %}
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-xl font-bold text-cyber-accent">Qualys Tags V3 — Vue actuelle</h2>
|
||||
<div class="flex gap-2">
|
||||
<a href="/qualys/tagsv3/gap" class="btn-sm bg-cyber-accent text-black">Analyse gap vs nomenclature V3</a>
|
||||
<a href="/qualys/tags" class="btn-sm bg-cyber-border text-cyber-accent">Tags (vue legacy)</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if error %}
|
||||
<div class="card p-3 mb-4 border border-red-700 text-red-400">
|
||||
Erreur Qualys : {{ error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div style="display:flex;gap:8px;margin-bottom:16px;">
|
||||
<div class="card p-3 text-center" style="flex:1"><div class="text-2xl font-bold text-cyber-accent">{{ stats.total }}</div><div class="text-xs text-gray-500">Total Qualys</div></div>
|
||||
<div class="card p-3 text-center" style="flex:1"><div class="text-2xl font-bold" style="color:#2196F3">{{ stats.dyn }}</div><div class="text-xs text-gray-500">Dynamiques</div></div>
|
||||
<div class="card p-3 text-center" style="flex:1"><div class="text-2xl font-bold" style="color:#FF9800">{{ stats.stat }}</div><div class="text-xs text-gray-500">Statiques</div></div>
|
||||
</div>
|
||||
|
||||
<div class="card overflow-x-auto">
|
||||
<table class="w-full table-cyber">
|
||||
<thead><tr>
|
||||
<th class="p-2 text-left">Nom</th>
|
||||
<th class="p-2">Type</th>
|
||||
<th class="p-2 text-left">Règle (QQL)</th>
|
||||
<th class="p-2">ID</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
{% for t in tags %}
|
||||
<tr>
|
||||
<td class="p-2 font-mono text-sm" style="color:{{ t.color or '#fff' }}">{{ t.name }}</td>
|
||||
<td class="p-2 text-center"><span class="badge {% if t.type == 'DYN' %}badge-blue{% else %}badge-yellow{% endif %}">{{ t.type }}</span></td>
|
||||
<td class="p-2 text-xs text-gray-400 font-mono">{{ t.rule_text or '-' }}</td>
|
||||
<td class="p-2 text-xs text-center text-gray-500">{{ t.id }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
135
app/templates/qualys_tagsv3_gap.html
Normal file
135
app/templates/qualys_tagsv3_gap.html
Normal file
@ -0,0 +1,135 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Tags V3 - Gap analysis{% endblock %}
|
||||
{% block content %}
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-xl font-bold text-cyber-accent">Gap nomenclature V3 ↔ Qualys</h2>
|
||||
<a href="/qualys/tagsv3" class="btn-sm bg-cyber-border text-cyber-accent">← Vue Qualys</a>
|
||||
</div>
|
||||
|
||||
{% if not gap.ok %}
|
||||
<div class="card p-4 border border-red-700 text-red-400">
|
||||
Erreur : {{ gap.msg }}
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
<!-- KPIs -->
|
||||
<div style="display:flex;gap:8px;margin-bottom:16px;">
|
||||
<div class="card p-3 text-center" style="flex:1"><div class="text-2xl font-bold text-cyber-green">{{ gap.present|length }}</div><div class="text-xs text-gray-500">Présents OK</div></div>
|
||||
<div class="card p-3 text-center" style="flex:1"><div class="text-2xl font-bold text-cyber-red">{{ gap.missing_dyn|length }}</div><div class="text-xs text-gray-500">DYN manquants (console)</div></div>
|
||||
<div class="card p-3 text-center" style="flex:1"><div class="text-2xl font-bold text-cyber-yellow">{{ gap.missing_stat|length }}</div><div class="text-xs text-gray-500">STAT manquants (API)</div></div>
|
||||
<div class="card p-3 text-center" style="flex:1"><div class="text-2xl font-bold" style="color:#9C27B0">{{ gap.mismatch|length }}</div><div class="text-xs text-gray-500">Type divergent</div></div>
|
||||
<div class="card p-3 text-center" style="flex:1"><div class="text-2xl font-bold text-gray-400">{{ gap.extra|length }}</div><div class="text-xs text-gray-500">Extra (hors V3)</div></div>
|
||||
</div>
|
||||
|
||||
<!-- DYN manquants -->
|
||||
{% if gap.missing_dyn %}
|
||||
<div class="card p-4 mb-4" style="border:1px solid #F44336">
|
||||
<div class="flex justify-between items-center mb-3">
|
||||
<h3 class="text-sm font-bold text-cyber-red">🔴 Tags DYN manquants ({{ gap.missing_dyn|length }}) — à créer MANUELLEMENT dans console Qualys</h3>
|
||||
</div>
|
||||
<p class="text-xs text-gray-400 mb-3">L'API Qualys ne permet pas de créer des tags dynamiques. Copie les étapes ci-dessous dans VMDR > Assets > Tags > New Tag.</p>
|
||||
<table class="w-full table-cyber text-sm">
|
||||
<thead><tr>
|
||||
<th class="p-2 text-left">Tag</th>
|
||||
<th class="p-2">Catégorie</th>
|
||||
<th class="p-2 text-left">QQL à saisir</th>
|
||||
<th class="p-2">Couleur</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
{% for t in console_steps %}
|
||||
<tr>
|
||||
<td class="p-2 font-mono">{{ t.name }}</td>
|
||||
<td class="p-2 text-center text-xs text-gray-400">{{ t.category }}</td>
|
||||
<td class="p-2 font-mono text-xs text-cyber-accent" style="user-select:all">{{ t.qql }}</td>
|
||||
<td class="p-2 text-center"><span style="display:inline-block;width:20px;height:20px;background:{{ t.color }};border-radius:4px;vertical-align:middle"></span> <span class="text-xs text-gray-400">{{ t.color }}</span></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- STAT manquants -->
|
||||
{% if gap.missing_stat %}
|
||||
<div class="card p-4 mb-4" style="border:1px solid #FF9800">
|
||||
<div class="flex justify-between items-center mb-3">
|
||||
<h3 class="text-sm font-bold text-cyber-yellow">🟡 Tags STAT manquants ({{ gap.missing_stat|length }}) — créables via API</h3>
|
||||
{% if can_modify %}
|
||||
<form method="POST" action="/qualys/tagsv3/create-all-static" style="display:inline">
|
||||
<button class="btn-sm bg-cyber-yellow text-black" onclick="return confirm('Créer {{ gap.missing_stat|length }} tags statiques dans Qualys ?')" data-loading="Création des tags...">+ Créer tous</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
<table class="w-full table-cyber text-sm">
|
||||
<thead><tr>
|
||||
<th class="p-2 text-left">Tag</th>
|
||||
<th class="p-2">Catégorie</th>
|
||||
<th class="p-2 text-left">Description</th>
|
||||
<th class="p-2">Couleur</th>
|
||||
{% if can_modify %}<th class="p-2">Action</th>{% endif %}
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
{% for t in gap.missing_stat %}
|
||||
<tr>
|
||||
<td class="p-2 font-mono">{{ t.name }}</td>
|
||||
<td class="p-2 text-center text-xs text-gray-400">{{ t.category }}</td>
|
||||
<td class="p-2 text-xs">{{ t.description or '-' }}</td>
|
||||
<td class="p-2 text-center"><span style="display:inline-block;width:20px;height:20px;background:{{ t.color }};border-radius:4px;vertical-align:middle"></span></td>
|
||||
{% if can_modify %}
|
||||
<td class="p-2 text-center">
|
||||
<form method="POST" action="/qualys/tagsv3/create-static" style="display:inline">
|
||||
<input type="hidden" name="name" value="{{ t.name }}">
|
||||
<input type="hidden" name="color" value="{{ t.color }}">
|
||||
<input type="hidden" name="description" value="{{ t.description }}">
|
||||
<button class="btn-sm bg-cyber-yellow text-black text-xs">Créer</button>
|
||||
</form>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Mismatch -->
|
||||
{% if gap.mismatch %}
|
||||
<div class="card p-4 mb-4" style="border:1px solid #9C27B0">
|
||||
<h3 class="text-sm font-bold" style="color:#9C27B0">⚠ Type divergent ({{ gap.mismatch|length }})</h3>
|
||||
<p class="text-xs text-gray-400 mb-2">Le type (DYN/STAT) dans Qualys ne correspond pas au catalogue V3.</p>
|
||||
<table class="w-full table-cyber text-sm">
|
||||
<thead><tr><th class="p-2 text-left">Tag</th><th class="p-2">V3</th><th class="p-2">Qualys</th></tr></thead>
|
||||
<tbody>
|
||||
{% for m in gap.mismatch %}
|
||||
<tr><td class="p-2 font-mono">{{ m.catalog.name }}</td><td class="p-2 text-center">{{ m.catalog.type }}</td><td class="p-2 text-center">{{ m.qualys.type }}</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Présents OK -->
|
||||
<details class="card p-3 mb-4">
|
||||
<summary class="cursor-pointer text-sm font-bold text-cyber-green">✓ Tags V3 présents ({{ gap.present|length }})</summary>
|
||||
<table class="w-full table-cyber text-sm mt-2">
|
||||
<thead><tr><th class="p-2 text-left">Tag</th><th class="p-2">Type</th></tr></thead>
|
||||
<tbody>
|
||||
{% for p in gap.present %}
|
||||
<tr><td class="p-2 font-mono">{{ p.catalog.name }}</td><td class="p-2 text-center"><span class="badge {% if p.catalog.type == 'DYN' %}badge-blue{% else %}badge-yellow{% endif %}">{{ p.catalog.type }}</span></td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</details>
|
||||
|
||||
<!-- Extras -->
|
||||
{% if gap.extra %}
|
||||
<details class="card p-3">
|
||||
<summary class="cursor-pointer text-sm font-bold text-gray-400">Extras Qualys (non-V3) ({{ gap.extra|length }})</summary>
|
||||
<div class="text-xs text-gray-500 mt-2">
|
||||
{% for t in gap.extra %}<span class="mr-2">{{ t.name }}</span>{% endfor %}
|
||||
</div>
|
||||
</details>
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
179
deploy/qualys_tags_v3.yaml
Normal file
179
deploy/qualys_tags_v3.yaml
Normal file
@ -0,0 +1,179 @@
|
||||
# Catalogue des tags Qualys V3 SANEF
|
||||
# Source : SANEF DSI / Sécurité Opérationnelle — Plan d'action Qualys V3 (Mars 2026)
|
||||
#
|
||||
# type: DYN (dynamic — création console web Qualys UNIQUEMENT)
|
||||
# STAT (static — création + assignation via API OK)
|
||||
# auto: True = entierement automatisable (Tag Rule ou script)
|
||||
# False = necessite decision humaine
|
||||
|
||||
categories:
|
||||
OS:
|
||||
description: "Système d'exploitation — dynamique sur operatingSystem"
|
||||
tags:
|
||||
- name: OS-LIN
|
||||
type: DYN
|
||||
auto: true
|
||||
qql: 'operatingSystem.category1: "Linux"'
|
||||
color: "#4CAF50"
|
||||
- name: OS-WIN
|
||||
type: DYN
|
||||
auto: true
|
||||
qql: 'operatingSystem.category1: "Windows"'
|
||||
color: "#2196F3"
|
||||
- name: OS-WIN-SRV
|
||||
type: DYN
|
||||
auto: true
|
||||
qql: 'operatingSystem: "Windows Server"'
|
||||
color: "#1976D2"
|
||||
- name: OS-ESX
|
||||
type: DYN
|
||||
auto: true
|
||||
qql: 'operatingSystem: "ESXi"'
|
||||
color: "#9C27B0"
|
||||
|
||||
ENV:
|
||||
description: "Environnement — dynamique sur hostname position 2"
|
||||
tags:
|
||||
- name: ENV-PRD
|
||||
type: DYN
|
||||
auto: true
|
||||
qql: 'name: "vp" OR name: "sp" OR name: "lp" OR name: "ls-"'
|
||||
color: "#F44336"
|
||||
- name: ENV-REC
|
||||
type: DYN
|
||||
auto: true
|
||||
qql: 'name: "vr" OR name: "sr" OR name: "lr"'
|
||||
color: "#FF9800"
|
||||
- name: ENV-PPR
|
||||
type: DYN
|
||||
auto: true
|
||||
qql: 'name: "vi" OR name: "si" OR name: "vo"'
|
||||
color: "#FFC107"
|
||||
- name: ENV-TST
|
||||
type: DYN
|
||||
auto: true
|
||||
qql: 'name: "vv" OR name: "vt"'
|
||||
color: "#CDDC39"
|
||||
- name: ENV-DEV
|
||||
type: DYN
|
||||
auto: true
|
||||
qql: 'name: "vd" OR name: "sd"'
|
||||
color: "#8BC34A"
|
||||
|
||||
POS:
|
||||
description: "Périmètre / Domaine — dynamique sur hostname positions 2-N"
|
||||
tags:
|
||||
- name: POS-FL
|
||||
type: DYN
|
||||
auto: true
|
||||
qql: 'name: "*bot" OR name: "*boo" OR name: "*boc" OR name: "*afl" OR name: "*sup"'
|
||||
color: "#009688"
|
||||
- name: POS-INF
|
||||
type: DYN
|
||||
auto: true
|
||||
qql: 'name: "*dsi" OR name: "*cyb" OR name: "*vsa" OR name: "*iad" OR name: "*bur" OR name: "*aii" OR name: "*ecm" OR name: "*log" OR name: "*vid" OR name: "*gaw" OR name: "*bck" OR name: "*ngw" OR name: "*pct" OR name: "*pix" OR name: "*sim" OR name: "*nms" OR name: "*ges" OR name: "*mon"'
|
||||
color: "#3F51B5"
|
||||
- name: POS-PEA
|
||||
type: DYN
|
||||
auto: true
|
||||
qql: 'name: "*pea" OR name: "*osa" OR name: "*svp" OR name: "*adv" OR name: "*rpa" OR name: "*rpn" OR name: "ls-"'
|
||||
color: "#673AB7"
|
||||
- name: POS-TRA
|
||||
type: DYN
|
||||
auto: true
|
||||
qql: 'name: "*ame" OR name: "*tra" OR name: "*dai" OR name: "*pat" OR name: "*rau" OR name: "*dep" OR name: "*exp" OR name: "*sig" OR name: "*air"'
|
||||
color: "#E91E63"
|
||||
- name: POS-BI
|
||||
type: DYN
|
||||
auto: true
|
||||
qql: 'name: "*dec" OR name: "*sas" OR name: "*bip" OR name: "*apt" OR name: "*pbi" OR name: "*rep"'
|
||||
color: "#FF5722"
|
||||
- name: POS-GES
|
||||
type: DYN
|
||||
auto: true
|
||||
qql: 'name: "*int" OR name: "*agt" OR name: "*pin" OR name: "*ech"'
|
||||
color: "#795548"
|
||||
- name: POS-DMZ
|
||||
type: DYN
|
||||
auto: true
|
||||
qql: 'name: "*ssi"'
|
||||
color: "#607D8B"
|
||||
|
||||
EQT:
|
||||
description: "Type equipement — position 1"
|
||||
tags:
|
||||
- name: EQT-VIR
|
||||
type: DYN
|
||||
auto: true
|
||||
qql: 'name: "v"'
|
||||
color: "#00BCD4"
|
||||
- name: EQT-SRV
|
||||
type: DYN
|
||||
auto: true
|
||||
qql: 'name: "l" OR name: "s"'
|
||||
color: "#03A9F4"
|
||||
- name: EQT-SWI
|
||||
type: DYN
|
||||
auto: true
|
||||
qql: 'name: "n"'
|
||||
color: "#4DD0E1"
|
||||
|
||||
SPEC_AUTO:
|
||||
description: "Tags spécifiques automatisables"
|
||||
tags:
|
||||
- name: TAG-OBS
|
||||
type: DYN
|
||||
auto: true
|
||||
qql: 'operatingSystem: "Windows Server 2008" OR operatingSystem: "Windows Server 2012" OR operatingSystem: "CentOS release 6" OR operatingSystem: "Red Hat Enterprise Linux Server release 6"'
|
||||
color: "#B71C1C"
|
||||
- name: TAG-EMV
|
||||
type: DYN
|
||||
auto: true
|
||||
qql: 'name: "*emv" OR name: "*pci"'
|
||||
color: "#D500F9"
|
||||
|
||||
SPEC_MANUAL:
|
||||
description: "Tags spécifiques non automatisables (decision humaine)"
|
||||
tags:
|
||||
- name: TAG-SED
|
||||
type: STAT
|
||||
auto: false
|
||||
description: "Securite Exposition Directe — IP publique / NAT direct"
|
||||
color: "#C62828"
|
||||
- name: TAG-SEI
|
||||
type: STAT
|
||||
auto: false
|
||||
description: "Securite Exposition Indirecte — derriere frontal"
|
||||
color: "#EF6C00"
|
||||
- name: TAG-DEC
|
||||
type: STAT
|
||||
auto: false
|
||||
description: "Decommissionnement en cours"
|
||||
color: "#6D4C41"
|
||||
- name: TAG-INT
|
||||
type: STAT
|
||||
auto: false
|
||||
description: "Integration / Implémentation en cours"
|
||||
color: "#FDD835"
|
||||
- name: TAG-SIC
|
||||
type: STAT
|
||||
auto: false
|
||||
description: "Zone SIC — Systeme Information Classifie"
|
||||
color: "#1A237E"
|
||||
- name: TAG-SIA
|
||||
type: STAT
|
||||
auto: false
|
||||
description: "Zone SIA — Systeme Information Administration"
|
||||
color: "#283593"
|
||||
|
||||
PREFIXES_MANUAL:
|
||||
description: "Prefixes statiques pour tags nominatifs (crees a la demande via API)"
|
||||
prefixes:
|
||||
- prefix: APP-
|
||||
description: "Application hebergee — APP-SAT, APP-JIRA, APP-GLPI..."
|
||||
- prefix: BDD-
|
||||
description: "Type de base de donnees — BDD-ORA, BDD-PG, BDD-SQL..."
|
||||
- prefix: VRF-
|
||||
description: "VRF reseau — VRF-TRAFIC, VRF-EMV..."
|
||||
- prefix: MID-
|
||||
description: "Middleware — MID-TOMCAT, MID-HAPROXY..."
|
||||
@ -34,6 +34,7 @@ PyNaCl==1.6.2
|
||||
python-jose==3.5.0
|
||||
python-multipart==0.0.26
|
||||
python-pptx==1.0.2
|
||||
PyYAML==6.0.2
|
||||
reportlab==4.4.10
|
||||
requests==2.33.1
|
||||
rsa==4.9.1
|
||||
|
||||
Loading…
Reference in New Issue
Block a user