756 lines
46 KiB
HTML
756 lines
46 KiB
HTML
{% extends 'base.html' %}
|
||
{% block title %}Settings{% endblock %}
|
||
{% block content %}
|
||
<h2 class="text-xl font-bold text-cyber-accent mb-6">Settings</h2>
|
||
|
||
{% if saved %}
|
||
<div class="mb-4 p-3 rounded bg-green-900/30 text-cyber-green text-sm">
|
||
Section "{{ saved }}" sauvegardee.
|
||
</div>
|
||
{% endif %}
|
||
|
||
{% macro section_header(key, title, badge_text, badge_class, extra="") %}
|
||
<button @click="open = open === '{{ key }}' ? '' : '{{ key }}'" class="w-full flex items-center justify-between p-4 hover:bg-cyber-border/20 transition-colors">
|
||
<div class="flex items-center gap-3">
|
||
<span class="text-cyber-accent font-bold">{{ title }}</span>
|
||
<span class="badge {{ badge_class }}">{{ badge_text }}</span>
|
||
{% if extra %}<span class="text-xs text-gray-500">{{ extra }}</span>{% endif %}
|
||
</div>
|
||
<span class="text-gray-500 text-lg" x-text="open === '{{ key }}' ? '▼' : '▶'"></span>
|
||
</button>
|
||
{% endmacro %}
|
||
|
||
<div x-data="{ open: '{{ saved or '' }}' }" class="space-y-2">
|
||
|
||
<!-- Qualys API -->
|
||
{% if visible.qualys %}
|
||
<div class="card overflow-hidden">
|
||
{{ section_header("qualys", "Qualys API", "Connecte", "badge-green", q_tags|string + " tags / " + q_assets|string + " assets / " + q_linked|string + " lies") }}
|
||
<div x-show="open === 'qualys'" class="border-t border-cyber-border p-4">
|
||
<form method="POST" action="/settings/qualys" class="space-y-3">
|
||
<div>
|
||
<label class="text-xs text-gray-500">URL API</label>
|
||
<input type="text" name="qualys_url" value="{{ vals.qualys_url }}" placeholder="https://qualysapi.qualys.eu" class="w-full" {% if not editable.qualys %}disabled{% endif %}>
|
||
</div>
|
||
<div class="grid grid-cols-2 gap-3">
|
||
<div>
|
||
<label class="text-xs text-gray-500">Utilisateur</label>
|
||
<input type="text" name="qualys_user" value="{{ vals.qualys_user }}" class="w-full" {% if not editable.qualys %}disabled{% endif %}>
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">Mot de passe</label>
|
||
<input type="password" name="qualys_pass" value="{{ vals.qualys_pass }}" class="w-full" {% if not editable.qualys %}disabled{% endif %}>
|
||
</div>
|
||
</div>
|
||
<div class="flex gap-3 items-end">
|
||
<div class="flex-1">
|
||
<label class="text-xs text-gray-500">Proxy</label>
|
||
<input type="text" name="qualys_proxy" value="{{ vals.qualys_proxy }}" placeholder="http://proxy.sanef.fr:8080" class="w-full font-mono text-xs" {% if not editable.qualys %}disabled{% endif %}>
|
||
</div>
|
||
<label class="flex items-center gap-2 text-xs text-gray-400 pb-1">
|
||
<input type="checkbox" name="qualys_bypass_proxy" value="true" {% if vals.qualys_bypass_proxy == 'true' %}checked{% endif %} {% if not editable.qualys %}disabled{% endif %}>
|
||
Bypass proxy (accès direct)
|
||
</label>
|
||
</div>
|
||
{% if editable.qualys %}<button type="submit" class="btn-primary px-4 py-2 text-sm">Sauvegarder</button>{% endif %}
|
||
</form>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- SSH Cle privee -->
|
||
{% if visible.ssh_key %}
|
||
<div class="card overflow-hidden">
|
||
{{ section_header("ssh_key", "SSH Cle privee", "ssh_key", "badge-green") }}
|
||
<div x-show="open === 'ssh_key'" class="border-t border-cyber-border p-4">
|
||
<form method="POST" action="/settings/ssh_key" class="space-y-3">
|
||
<div class="grid grid-cols-2 gap-3">
|
||
<div>
|
||
<label class="text-xs text-gray-500">User SSH par defaut</label>
|
||
<input type="text" name="ssh_key_default_user" value="{{ vals.ssh_key_default_user }}" placeholder="root" class="w-full" {% if not editable.ssh_key %}disabled{% endif %}>
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">Port par defaut</label>
|
||
<input type="text" name="ssh_key_default_port" value="{{ vals.ssh_key_default_port }}" placeholder="22" class="w-full" {% if not editable.ssh_key %}disabled{% endif %}>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">Cle privee (PEM)</label>
|
||
<textarea name="ssh_key_private_key" rows="4" class="w-full font-mono text-xs" placeholder="-----BEGIN OPENSSH PRIVATE KEY-----" {% if not editable.ssh_key %}disabled{% endif %}>{{ vals.ssh_key_private_key }}</textarea>
|
||
</div>
|
||
<p class="text-xs text-gray-600">Surchargeable par serveur (ssh_user, ssh_port dans la fiche serveur).</p>
|
||
{% if editable.ssh_key %}<button type="submit" class="btn-primary px-4 py-2 text-sm">Sauvegarder</button>{% endif %}
|
||
</form>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- SSH Password -->
|
||
{% if visible.ssh_pwd %}
|
||
<div class="card overflow-hidden">
|
||
{{ section_header("ssh_pwd", "SSH Password", "ssh_pwd", "badge-yellow") }}
|
||
<div x-show="open === 'ssh_pwd'" class="border-t border-cyber-border p-4">
|
||
<form method="POST" action="/settings/ssh_pwd" class="space-y-3">
|
||
<div>
|
||
<label class="text-xs text-gray-500">User par defaut</label>
|
||
<input type="text" name="ssh_pwd_default_user" value="{{ vals.ssh_pwd_default_user }}" class="w-full" {% if not editable.ssh_pwd %}disabled{% endif %}>
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">Password par defaut</label>
|
||
<input type="password" name="ssh_pwd_default_pass" value="{{ vals.ssh_pwd_default_pass }}" class="w-full" {% if not editable.ssh_pwd %}disabled{% endif %}>
|
||
</div>
|
||
<p class="text-xs text-gray-600">Pour les environnements recette sans cle SSH. Chaque operateur peut configurer son propre compte.</p>
|
||
{% if editable.ssh_pwd %}<button type="submit" class="btn-primary px-4 py-2 text-sm">Sauvegarder</button>{% endif %}
|
||
</form>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- SSH PSMP (CyberArk) -->
|
||
{% if visible.ssh_psmp %}
|
||
<div class="card overflow-hidden">
|
||
{{ section_header("ssh_psmp", "SSH PSMP — CyberArk", "ssh_psmp", "badge-yellow") }}
|
||
<div x-show="open === 'ssh_psmp'" class="border-t border-cyber-border p-4">
|
||
<form method="POST" action="/settings/ssh_psmp" class="space-y-3">
|
||
<div class="grid grid-cols-2 gap-3">
|
||
<div>
|
||
<label class="text-xs text-gray-500">Adresse PSMP</label>
|
||
<input type="text" name="psmp_host" value="{{ vals.psmp_host }}" placeholder="psmp.sanef.fr" class="w-full" {% if not editable.ssh_psmp %}disabled{% endif %}>
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">Port PSMP</label>
|
||
<input type="text" name="psmp_port" value="{{ vals.psmp_port }}" placeholder="22" class="w-full" {% if not editable.ssh_psmp %}disabled{% endif %}>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">Format user</label>
|
||
<input type="text" name="psmp_user_format" value="{{ vals.psmp_user_format }}" placeholder="{cybr_user}@{target_user}@{hostname}" class="w-full font-mono text-xs" {% if not editable.ssh_psmp %}disabled{% endif %}>
|
||
</div>
|
||
<div class="grid grid-cols-3 gap-3">
|
||
<div>
|
||
<label class="text-xs text-gray-500">Compte CyberArk</label>
|
||
<input type="text" name="psmp_cyberark_user" value="{{ vals.psmp_cyberark_user }}" placeholder="CYBP01336" class="w-full" {% if not editable.ssh_psmp %}disabled{% endif %}>
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">Utilisateur cible</label>
|
||
<input type="text" name="psmp_target_user" value="{{ vals.psmp_target_user }}" placeholder="cybsecope" class="w-full" {% if not editable.ssh_psmp %}disabled{% endif %}>
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">Safe par defaut</label>
|
||
<input type="text" name="psmp_default_safe" value="{{ vals.psmp_default_safe }}" class="w-full" {% if not editable.ssh_psmp %}disabled{% endif %}>
|
||
</div>
|
||
</div>
|
||
<p class="text-xs text-gray-600">Auth keyboard-interactive. Chaque operateur configure son propre compte CyberArk. MDP saisi en session.</p>
|
||
{% if editable.ssh_psmp %}<button type="submit" class="btn-primary px-4 py-2 text-sm">Sauvegarder</button>{% endif %}
|
||
</form>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- RDP PSM (CyberArk) -->
|
||
{% if visible.rdp_psm %}
|
||
<div class="card overflow-hidden">
|
||
{{ section_header("rdp_psm", "RDP PSM — CyberArk", "rdp_psm", "badge-blue") }}
|
||
<div x-show="open === 'rdp_psm'" class="border-t border-cyber-border p-4">
|
||
<form method="POST" action="/settings/rdp_psm" class="space-y-3">
|
||
<div>
|
||
<label class="text-xs text-gray-500">URL PVWA</label>
|
||
<input type="text" name="rdp_psm_pvwa_url" value="{{ vals.rdp_psm_pvwa_url }}" placeholder="https://pvwa.sanef.fr" class="w-full" {% if not editable.rdp_psm %}disabled{% endif %}>
|
||
</div>
|
||
<div class="grid grid-cols-2 gap-3">
|
||
<div>
|
||
<label class="text-xs text-gray-500">User PVWA</label>
|
||
<input type="text" name="rdp_psm_pvwa_user" value="{{ vals.rdp_psm_pvwa_user }}" class="w-full" {% if not editable.rdp_psm %}disabled{% endif %}>
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">Password PVWA</label>
|
||
<input type="password" name="rdp_psm_pvwa_pass" value="{{ vals.rdp_psm_pvwa_pass }}" class="w-full" {% if not editable.rdp_psm %}disabled{% endif %}>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">Connection Component</label>
|
||
<input type="text" name="rdp_psm_component" value="{{ vals.rdp_psm_component }}" placeholder="PSM-RDP" class="w-full" {% if not editable.rdp_psm %}disabled{% endif %}>
|
||
</div>
|
||
<p class="text-xs text-gray-600">Connexion RDP via token PVWA API. Production Windows uniquement.</p>
|
||
{% if editable.rdp_psm %}<button type="submit" class="btn-primary px-4 py-2 text-sm">Sauvegarder</button>{% endif %}
|
||
</form>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- RDP Password — disabled -->
|
||
|
||
<!-- vSphere / vCenters -->
|
||
{% if visible.vsphere %}
|
||
<div class="card overflow-hidden">
|
||
<button @click="open = open === 'vsphere' ? '' : 'vsphere'" class="w-full flex items-center justify-between p-4 hover:bg-cyber-border/20 transition-colors">
|
||
<div class="flex items-center gap-3">
|
||
<span class="text-cyber-accent font-bold">vSphere / vCenters</span>
|
||
<span class="badge badge-gray">Snapshots</span>
|
||
<span class="text-xs text-gray-500">{{ vcenters|selectattr('is_active')|list|length }} actif(s)</span>
|
||
</div>
|
||
<span class="text-gray-500 text-lg" x-text="open === 'vsphere' ? '▼' : '▶'"></span>
|
||
</button>
|
||
<div x-show="open === 'vsphere'" class="border-t border-cyber-border p-4 space-y-4">
|
||
{% if editable.vsphere %}
|
||
<form method="POST" action="/settings/vsphere" class="space-y-3">
|
||
<h4 class="text-xs text-cyber-accent font-bold uppercase">Credentials vSphere (communs)</h4>
|
||
<div class="grid grid-cols-2 gap-3">
|
||
<div>
|
||
<label class="text-xs text-gray-500">Utilisateur</label>
|
||
<input type="text" name="vsphere_user" value="{{ vals.vsphere_user }}" class="w-full">
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">Mot de passe</label>
|
||
<input type="password" name="vsphere_pass" value="{{ vals.vsphere_pass }}" class="w-full">
|
||
</div>
|
||
</div>
|
||
<button type="submit" class="btn-primary px-4 py-2 text-sm">Sauvegarder credentials</button>
|
||
</form>
|
||
{% endif %}
|
||
|
||
<div>
|
||
<h4 class="text-xs text-cyber-accent font-bold uppercase mb-2">vCenters enregistres</h4>
|
||
<table class="w-full table-cyber text-sm">
|
||
<thead><tr>
|
||
<th class="text-left p-2">Nom</th>
|
||
<th class="text-left p-2">Endpoint</th>
|
||
<th class="p-2">Datacenter</th>
|
||
<th class="text-left p-2">Description</th>
|
||
<th class="p-2">Responsable</th>
|
||
<th class="p-2">Actif</th>
|
||
{% if editable.vsphere %}<th class="p-2">Action</th>{% endif %}
|
||
</tr></thead>
|
||
<tbody>
|
||
{% for vc in vcenters %}
|
||
<tr>
|
||
<td class="p-2">{{ vc.name }}</td>
|
||
<td class="p-2 font-mono text-xs text-cyber-accent">{{ vc.endpoint }}</td>
|
||
<td class="p-2 text-center text-xs">{{ vc.datacenter or '-' }}</td>
|
||
<td class="p-2 text-xs text-gray-400">{{ vc.description or '-' }}</td>
|
||
<td class="p-2 text-center text-xs">{{ vc.responsable or '-' }}</td>
|
||
<td class="p-2 text-center"><span class="badge {% if vc.is_active %}badge-green{% else %}badge-red{% endif %}">{{ 'Oui' if vc.is_active else 'Non' }}</span></td>
|
||
{% if editable.vsphere %}
|
||
<td class="p-2 text-center">
|
||
{% if vc.is_active %}
|
||
<form method="POST" action="/settings/vcenter/{{ vc.id }}/delete" style="display:inline">
|
||
<button type="submit" class="btn-sm bg-red-900/30 text-cyber-red" onclick="return confirm('Desactiver ce vCenter ?')">Desactiver</button>
|
||
</form>
|
||
{% endif %}
|
||
</td>
|
||
{% endif %}
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
{% if editable.vsphere %}
|
||
<form method="POST" action="/settings/vcenter/add" class="space-y-3 pt-2 border-t border-cyber-border">
|
||
<h4 class="text-xs text-cyber-accent font-bold uppercase">Ajouter un vCenter</h4>
|
||
<div class="grid grid-cols-3 gap-3">
|
||
<div>
|
||
<label class="text-xs text-gray-500">Nom</label>
|
||
<input type="text" name="vc_name" placeholder="vCenter Senlis" class="w-full" required>
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">Endpoint (FQDN)</label>
|
||
<input type="text" name="vc_endpoint" placeholder="vcenter01.sanef.groupe" class="w-full font-mono text-xs" required>
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">Datacenter</label>
|
||
<input type="text" name="vc_datacenter" placeholder="DC-Senlis" class="w-full">
|
||
</div>
|
||
</div>
|
||
<div class="grid grid-cols-2 gap-3">
|
||
<div>
|
||
<label class="text-xs text-gray-500">Description</label>
|
||
<input type="text" name="vc_description" placeholder="Gestion + hors-prod" class="w-full">
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">Responsable</label>
|
||
<input type="text" name="vc_responsable" class="w-full">
|
||
</div>
|
||
</div>
|
||
<button type="submit" class="btn-primary px-4 py-2 text-sm">Ajouter</button>
|
||
</form>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- Teams Channels -->
|
||
{% if visible.vsphere %}
|
||
<div class="card overflow-hidden">
|
||
<button @click="open = open === 'teams' ? '' : 'teams'" class="w-full flex items-center justify-between p-4 hover:bg-cyber-border/20 transition-colors">
|
||
<div class="flex items-center gap-3">
|
||
<span class="text-cyber-accent font-bold">Canaux Microsoft Teams</span>
|
||
<span class="badge badge-gray">Notifications</span>
|
||
<span class="text-xs text-gray-500">{{ teams_channels|selectattr('is_active')|list|length }} actif(s){% if teams_channels|selectattr('is_default')|list|length %} · 1 défaut{% endif %}</span>
|
||
</div>
|
||
<span class="text-gray-500 text-lg" x-text="open === 'teams' ? '▼' : '▶'"></span>
|
||
</button>
|
||
<div x-show="open === 'teams'" class="border-t border-cyber-border p-4 space-y-4">
|
||
<p class="text-xs text-gray-500">
|
||
Configurez ici les webhooks Teams (Workflows / Incoming) utilisés pour
|
||
annoncer les interventions de patching. Chaque application ou serveur
|
||
peut pointer vers son canal dédié ; le canal "défaut" est utilisé en fallback.
|
||
</p>
|
||
|
||
<div>
|
||
<h4 class="text-xs text-cyber-accent font-bold uppercase mb-2">Canaux enregistrés</h4>
|
||
<table class="w-full table-cyber text-sm">
|
||
<thead><tr>
|
||
<th class="text-left p-2">Nom</th>
|
||
<th class="text-left p-2">Webhook URL</th>
|
||
<th class="text-left p-2">Description</th>
|
||
<th class="p-2">Défaut</th>
|
||
<th class="p-2">Actif</th>
|
||
{% if editable.vsphere %}<th class="p-2">Actions</th>{% endif %}
|
||
</tr></thead>
|
||
<tbody>
|
||
{% for tc in teams_channels %}
|
||
<tr>
|
||
<td class="p-2 font-bold">{{ tc.name }}</td>
|
||
<td class="p-2 font-mono text-[10px] text-cyber-accent" style="max-width:380px; overflow:hidden; text-overflow:ellipsis;" title="{{ tc.webhook_url }}">{{ tc.webhook_url[:60] }}{% if tc.webhook_url|length > 60 %}…{% endif %}</td>
|
||
<td class="p-2 text-xs text-gray-400">{{ tc.description or '-' }}</td>
|
||
<td class="p-2 text-center"><span class="badge {% if tc.is_default %}badge-green{% else %}badge-gray{% endif %}">{{ 'Oui' if tc.is_default else '-' }}</span></td>
|
||
<td class="p-2 text-center"><span class="badge {% if tc.is_active %}badge-green{% else %}badge-red{% endif %}">{{ 'Oui' if tc.is_active else 'Non' }}</span></td>
|
||
{% if editable.vsphere %}
|
||
<td class="p-2 text-center whitespace-nowrap">
|
||
<button type="button" class="btn-sm bg-cyber-blue/20 text-cyber-blue text-xs"
|
||
onclick="testTeams({{ tc.id }}, '{{ tc.name|e }}')">Test</button>
|
||
<form method="POST" action="/settings/teams-channel/{{ tc.id }}/delete" style="display:inline">
|
||
<button type="submit" class="btn-sm bg-red-900/30 text-cyber-red text-xs" onclick="return confirm('Supprimer ce canal Teams ?')">Suppr</button>
|
||
</form>
|
||
</td>
|
||
{% endif %}
|
||
</tr>
|
||
{% else %}
|
||
<tr><td colspan="6" class="p-3 text-gray-500 text-center">Aucun canal Teams configuré.</td></tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
{% if editable.vsphere %}
|
||
<form method="POST" action="/settings/teams-channel/add" class="space-y-3 pt-2 border-t border-cyber-border">
|
||
<h4 class="text-xs text-cyber-accent font-bold uppercase">Ajouter un canal Teams</h4>
|
||
<div class="grid grid-cols-2 gap-3">
|
||
<div>
|
||
<label class="text-xs text-gray-500">Nom</label>
|
||
<input type="text" name="tc_name" placeholder="SECOPS Patching" class="w-full" required>
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">
|
||
<input type="checkbox" name="tc_is_default" value="1" class="mr-1"> Défaut (un seul canal par défaut)
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">Webhook URL (Workflows ou Incoming Webhook)</label>
|
||
<input type="url" name="tc_webhook_url" placeholder="https://prod-XX.westeurope.logic.azure.com:443/workflows/..." class="w-full font-mono text-xs" required>
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">Description (optionnelle)</label>
|
||
<input type="text" name="tc_description" placeholder="Canal de l'équipe SECOPS pour annonces patching" class="w-full">
|
||
</div>
|
||
<button type="submit" class="btn-primary px-4 py-2 text-sm">Ajouter</button>
|
||
</form>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- Server Clusters -->
|
||
{% if visible.vsphere %}
|
||
<div class="card overflow-hidden">
|
||
<button @click="open = open === 'clusters' ? '' : 'clusters'" class="w-full flex items-center justify-between p-4 hover:bg-cyber-border/20 transition-colors">
|
||
<div class="flex items-center gap-3">
|
||
<span class="text-cyber-accent font-bold">Groupes de serveurs (clusters)</span>
|
||
<span class="badge badge-gray">Patching</span>
|
||
<span class="text-xs text-gray-500">{{ server_clusters|length }} défini(s)</span>
|
||
</div>
|
||
<span class="text-gray-500 text-lg" x-text="open === 'clusters' ? '▼' : '▶'"></span>
|
||
</button>
|
||
<div x-show="open === 'clusters'" class="border-t border-cyber-border p-4 space-y-4">
|
||
<p class="text-xs text-gray-500">
|
||
Les clusters permettent de patcher plusieurs serveurs liés (ex : DB master + slaves,
|
||
HAProxy + backends) avec un ordre de redémarrage. Le champ
|
||
<code class="text-cyber-accent">cluster_order</code> sur chaque serveur fixe la
|
||
séquence (1 = premier patché). Stratégie <em>sequential</em> = un par un (recommandé)
|
||
ou <em>parallel</em> = tous en même temps.
|
||
</p>
|
||
|
||
<div>
|
||
<h4 class="text-xs text-cyber-accent font-bold uppercase mb-2">Clusters enregistrés</h4>
|
||
<table class="w-full table-cyber text-sm">
|
||
<thead><tr>
|
||
<th class="text-left p-2">Nom</th>
|
||
<th class="text-left p-2">Description</th>
|
||
<th class="p-2">Stratégie</th>
|
||
<th class="p-2"># Serveurs</th>
|
||
<th class="p-2">Actif</th>
|
||
{% if editable.vsphere %}<th class="p-2">Actions</th>{% endif %}
|
||
</tr></thead>
|
||
<tbody>
|
||
{% for sc in server_clusters %}
|
||
<tr>
|
||
<td class="p-2 font-bold">{{ sc.name }}</td>
|
||
<td class="p-2 text-xs text-gray-400">{{ sc.description or '-' }}</td>
|
||
<td class="p-2 text-center text-xs">{{ sc.reboot_strategy }}</td>
|
||
<td class="p-2 text-center">
|
||
<a href="#" onclick="showClusterServers({{ sc.id }}, '{{ sc.name|e }}'); return false;"
|
||
class="text-cyber-accent hover:underline">{{ sc.server_count }}</a>
|
||
</td>
|
||
<td class="p-2 text-center"><span class="badge {% if sc.is_active %}badge-green{% else %}badge-red{% endif %}">{{ 'Oui' if sc.is_active else 'Non' }}</span></td>
|
||
{% if editable.vsphere %}
|
||
<td class="p-2 text-center whitespace-nowrap">
|
||
<form method="POST" action="/settings/server-cluster/{{ sc.id }}/delete" style="display:inline">
|
||
<button type="submit" class="btn-sm bg-red-900/30 text-cyber-red text-xs"
|
||
onclick="return confirm('{{ sc.server_count }} serveur(s) rattaché(s).\nLe cluster sera désactivé si utilisé, ou supprimé si vide. OK ?');">Suppr</button>
|
||
</form>
|
||
</td>
|
||
{% endif %}
|
||
</tr>
|
||
{% else %}
|
||
<tr><td colspan="6" class="p-3 text-gray-500 text-center">Aucun cluster configuré.</td></tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
<div id="cluster-servers-detail" class="mt-3 p-3 bg-cyber-border/10 rounded text-xs hidden">
|
||
<div class="flex justify-between items-center mb-2">
|
||
<span id="cluster-servers-title" class="text-cyber-accent font-bold"></span>
|
||
<button onclick="document.getElementById('cluster-servers-detail').classList.add('hidden')"
|
||
class="text-gray-500 hover:text-cyber-accent">✕</button>
|
||
</div>
|
||
<div id="cluster-servers-list"></div>
|
||
</div>
|
||
</div>
|
||
|
||
{% if editable.vsphere %}
|
||
<form method="POST" action="/settings/server-cluster/add" class="space-y-3 pt-2 border-t border-cyber-border">
|
||
<h4 class="text-xs text-cyber-accent font-bold uppercase">Ajouter un cluster</h4>
|
||
<div class="grid grid-cols-3 gap-3">
|
||
<div>
|
||
<label class="text-xs text-gray-500">Nom (unique)</label>
|
||
<input type="text" name="sc_name" placeholder="HAproxy FL" class="w-full" required>
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">Description</label>
|
||
<input type="text" name="sc_description" placeholder="Cluster HAproxy + backends Flux Libre" class="w-full">
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">Stratégie reboot</label>
|
||
<select name="sc_reboot_strategy" class="w-full">
|
||
<option value="sequential" selected>Séquentiel (un par un)</option>
|
||
<option value="parallel">Parallèle (tous ensemble)</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<button type="submit" class="btn-primary px-4 py-2 text-sm">Ajouter</button>
|
||
</form>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- Sécurité -->
|
||
{% if visible.security %}
|
||
<div class="card overflow-hidden">
|
||
{{ section_header("security", "Sécurité", "Réseau", "badge-red") }}
|
||
<div x-show="open === 'security'" class="border-t border-cyber-border p-4 space-y-4">
|
||
<!-- Réseaux autorisés -->
|
||
<h4 class="text-xs text-cyber-accent font-bold uppercase">Réseaux autorisés (nginx ACL)</h4>
|
||
<table class="w-full table-cyber text-sm">
|
||
<thead><tr>
|
||
<th class="text-left p-2">CIDR</th>
|
||
<th class="text-left p-2">Description</th>
|
||
<th class="p-2">Actif</th>
|
||
{% if editable.security %}<th class="p-2">Actions</th>{% endif %}
|
||
</tr></thead>
|
||
<tbody>
|
||
{% for n in allowed_nets %}
|
||
<tr class="{% if not n.is_active %}opacity-40{% endif %}">
|
||
<td class="p-2 font-mono text-cyber-accent">{{ n.cidr }}</td>
|
||
<td class="p-2 text-xs text-gray-400">{{ n.description or '-' }}</td>
|
||
<td class="p-2 text-center"><span class="badge {% if n.is_active %}badge-green{% else %}badge-red{% endif %}">{{ 'Oui' if n.is_active else 'Non' }}</span></td>
|
||
{% if editable.security %}
|
||
<td class="p-2 text-center">
|
||
<div class="flex gap-1 justify-center">
|
||
<form method="POST" action="/settings/network/{{ n.id }}/toggle" style="display:inline">
|
||
<button class="btn-sm bg-cyber-border text-gray-400">{{ 'Désactiver' if n.is_active else 'Activer' }}</button>
|
||
</form>
|
||
<form method="POST" action="/settings/network/{{ n.id }}/delete" style="display:inline">
|
||
<button class="btn-sm bg-red-900/30 text-cyber-red" onclick="return confirm('Supprimer ce réseau ?')">Suppr</button>
|
||
</form>
|
||
</div>
|
||
</td>
|
||
{% endif %}
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
|
||
{% if editable.security %}
|
||
<form method="POST" action="/settings/network/add" class="flex gap-3 items-end mt-2">
|
||
<div>
|
||
<label class="text-xs text-gray-500">CIDR</label>
|
||
<input type="text" name="cidr" placeholder="10.0.0.0/24" class="text-xs py-1 px-2 w-40 font-mono" required>
|
||
</div>
|
||
<div class="flex-1">
|
||
<label class="text-xs text-gray-500">Description</label>
|
||
<input type="text" name="description" placeholder="VPN nomade" class="text-xs py-1 px-2 w-full">
|
||
</div>
|
||
<button type="submit" class="btn-primary px-3 py-1 text-sm">Ajouter</button>
|
||
</form>
|
||
{% endif %}
|
||
|
||
<p class="text-xs text-gray-600 mt-2">Le fichier <code>/etc/nginx/patchcenter_acl.conf</code> est régénéré automatiquement à chaque modification. Nginx est rechargé.</p>
|
||
|
||
<!-- Paramètres sécurité -->
|
||
<h4 class="text-xs text-cyber-accent font-bold uppercase mt-4">Paramètres</h4>
|
||
<form method="POST" action="/settings/security" class="space-y-3">
|
||
<div class="grid grid-cols-2 gap-3">
|
||
<div>
|
||
<label class="text-xs text-gray-500">Timeout session (minutes)</label>
|
||
<input type="number" name="security_session_timeout" value="{{ vals.security_session_timeout or '60' }}" class="w-full" {% if not editable.security %}disabled{% endif %}>
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">Max tentatives login (rate limit/min)</label>
|
||
<input type="number" name="security_max_login_attempts" value="{{ vals.security_max_login_attempts or '5' }}" class="w-full" {% if not editable.security %}disabled{% endif %}>
|
||
</div>
|
||
</div>
|
||
{% if editable.security %}<button type="submit" class="btn-primary px-4 py-2 text-sm">Sauvegarder</button>{% endif %}
|
||
</form>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- Splunk Remote Log -->
|
||
{% if visible.splunk %}
|
||
<div class="card overflow-hidden">
|
||
{{ section_header("splunk", "Splunk — Remote Log", "HEC", "badge-yellow") }}
|
||
<div x-show="open === 'splunk'" class="border-t border-cyber-border p-4">
|
||
<form method="POST" action="/settings/splunk" class="space-y-3">
|
||
<div>
|
||
<label class="text-xs text-gray-500">URL HEC (HTTP Event Collector)</label>
|
||
<input type="text" name="splunk_hec_url" value="{{ vals.splunk_hec_url }}" placeholder="https://splunk.sanef.fr:8088/services/collector" class="w-full" {% if not editable.splunk %}disabled{% endif %}>
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">Token HEC</label>
|
||
<input type="password" name="splunk_hec_token" value="{{ vals.splunk_hec_token }}" class="w-full" {% if not editable.splunk %}disabled{% endif %}>
|
||
</div>
|
||
<div class="grid grid-cols-2 gap-3">
|
||
<div>
|
||
<label class="text-xs text-gray-500">Index</label>
|
||
<input type="text" name="splunk_index" value="{{ vals.splunk_index }}" placeholder="patchcenter" class="w-full" {% if not editable.splunk %}disabled{% endif %}>
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">Sourcetype</label>
|
||
<input type="text" name="splunk_sourcetype" value="{{ vals.splunk_sourcetype }}" placeholder="patchcenter:audit" class="w-full" {% if not editable.splunk %}disabled{% endif %}>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">Verifier SSL (true/false)</label>
|
||
<input type="text" name="splunk_verify_ssl" value="{{ vals.splunk_verify_ssl }}" placeholder="true" class="w-full" {% if not editable.splunk %}disabled{% endif %}>
|
||
</div>
|
||
<p class="text-xs text-gray-600">Envoie les evenements de patching vers Splunk via HEC.</p>
|
||
{% if editable.splunk %}<button type="submit" class="btn-primary px-4 py-2 text-sm">Sauvegarder</button>{% endif %}
|
||
</form>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- Teams Notifications -->
|
||
{% if visible.teams %}
|
||
<div class="card overflow-hidden">
|
||
{{ section_header("teams", "Teams — Notifications", "Webhook + SharePoint", "badge-blue") }}
|
||
<div x-show="open === 'teams'" class="border-t border-cyber-border p-4">
|
||
<form method="POST" action="/settings/teams" class="space-y-3">
|
||
<h4 class="text-xs text-cyber-accent font-bold uppercase">Canal Teams (Webhook direct)</h4>
|
||
<div>
|
||
<label class="text-xs text-gray-500">Webhook URL</label>
|
||
<input type="text" name="teams_webhook_url" value="{{ vals.teams_webhook_url }}" placeholder="https://outlook.office.com/webhook/..." class="w-full font-mono text-xs" {% if not editable.teams %}disabled{% endif %}>
|
||
</div>
|
||
<h4 class="text-xs text-cyber-accent font-bold uppercase mt-4">Conversation groupe (SharePoint + Power Automate)</h4>
|
||
<div>
|
||
<label class="text-xs text-gray-500">SharePoint Site URL</label>
|
||
<input type="text" name="teams_sp_site_url" value="{{ vals.teams_sp_site_url }}" placeholder="https://sanef.sharepoint.com/sites/SecOps" class="w-full font-mono text-xs" {% if not editable.teams %}disabled{% endif %}>
|
||
</div>
|
||
<div class="grid grid-cols-2 gap-3">
|
||
<div>
|
||
<label class="text-xs text-gray-500">Library</label>
|
||
<input type="text" name="teams_sp_library" value="{{ vals.teams_sp_library }}" placeholder="Documents partages" class="w-full" {% if not editable.teams %}disabled{% endif %}>
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">Folder</label>
|
||
<input type="text" name="teams_sp_folder" value="{{ vals.teams_sp_folder }}" placeholder="PatchCenter" class="w-full" {% if not editable.teams %}disabled{% endif %}>
|
||
</div>
|
||
</div>
|
||
<div class="grid grid-cols-2 gap-3">
|
||
<div>
|
||
<label class="text-xs text-gray-500">Tenant ID (Azure AD)</label>
|
||
<input type="text" name="teams_sp_tenant_id" value="{{ vals.teams_sp_tenant_id }}" class="w-full font-mono text-xs" {% if not editable.teams %}disabled{% endif %}>
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">App Client ID</label>
|
||
<input type="text" name="teams_sp_client_id" value="{{ vals.teams_sp_client_id }}" class="w-full font-mono text-xs" {% if not editable.teams %}disabled{% endif %}>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500">App Client Secret</label>
|
||
<input type="password" name="teams_sp_client_secret" value="{{ vals.teams_sp_client_secret }}" class="w-full" {% if not editable.teams %}disabled{% endif %}>
|
||
</div>
|
||
<p class="text-xs text-gray-600">Power Automate : depose JSON sur SharePoint → lit + poste dans la conversation → supprime.</p>
|
||
{% if editable.teams %}<button type="submit" class="btn-primary px-4 py-2 text-sm">Sauvegarder</button>{% endif %}
|
||
</form>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- iTop CMDB -->
|
||
{% if visible.itop %}
|
||
<div class="card overflow-hidden">
|
||
<button @click="open = open === 'itop' ? '' : 'itop'" class="w-full flex items-center justify-between p-4 hover:bg-cyber-border/20 transition-colors">
|
||
<div class="flex items-center gap-3">
|
||
<span class="text-gray-400 font-bold">iTop CMDB</span>
|
||
<span class="badge badge-green">Configuré</span>
|
||
</div>
|
||
<span class="text-gray-500 text-lg" x-text="open === 'itop' ? '▼' : '▶'"></span>
|
||
</button>
|
||
<div x-show="open === 'itop'" class="border-t border-cyber-border p-4">
|
||
<form method="POST" action="/settings/itop" class="space-y-3">
|
||
{% for key, label, is_secret in sections.itop %}
|
||
<div>
|
||
<label class="text-xs text-gray-500">{{ label }}</label>
|
||
<input type="{{ 'password' if is_secret else 'text' }}" name="{{ key }}" value="{{ vals[key] }}" class="w-full" {% if not editable.itop %}disabled{% endif %}>
|
||
</div>
|
||
{% endfor %}
|
||
<div class="text-xs text-gray-600 space-y-1 mt-2">
|
||
<div>- Import serveurs + metadata</div>
|
||
<div>- Sync responsables / referents</div>
|
||
<div>- Lien applications / clusters</div>
|
||
<div>- Enrichissement domaine / environnement</div>
|
||
</div>
|
||
{% if editable.itop %}<button type="submit" class="btn-primary px-4 py-2 text-sm">Sauvegarder</button>{% endif %}
|
||
</form>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- iTop Contacts (filtre des teams à synchroniser) -->
|
||
{% if visible.itop_contacts %}
|
||
<div class="card overflow-hidden">
|
||
<button @click="open = open === 'itop_contacts' ? '' : 'itop_contacts'" class="w-full flex items-center justify-between p-4 hover:bg-cyber-border/20 transition-colors">
|
||
<div class="flex items-center gap-3">
|
||
<span class="text-gray-400 font-bold">iTop Contacts — Périmètre</span>
|
||
<span class="badge badge-blue">Filtre des teams</span>
|
||
</div>
|
||
<span class="text-gray-500 text-lg" x-text="open === 'itop_contacts' ? '▼' : '▶'"></span>
|
||
</button>
|
||
<div x-show="open === 'itop_contacts'" class="border-t border-cyber-border p-4">
|
||
<form method="POST" action="/settings/itop_contacts" class="space-y-3">
|
||
{% for key, label, is_secret in sections.itop_contacts %}
|
||
<div>
|
||
<label class="text-xs text-gray-500">{{ label }}</label>
|
||
<input type="text" name="{{ key }}" value="{{ vals[key] }}" placeholder="SecOps, iPOP, Externe, DSI, Admin DSI" class="w-full font-mono text-xs" {% if not editable.itop_contacts %}disabled{% endif %}>
|
||
</div>
|
||
{% endfor %}
|
||
<div class="text-xs text-gray-600 mt-2">
|
||
Seuls les contacts appartenant à ces teams iTop seront synchronisés dans PatchCenter. Si vide, défaut : SecOps, iPOP, Externe, DSI, Admin DSI.
|
||
</div>
|
||
{% if editable.itop_contacts %}<button type="submit" class="btn-primary px-4 py-2 text-sm">Sauvegarder</button>{% endif %}
|
||
</form>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- LDAP/AD -->
|
||
{% if visible.ldap %}
|
||
<div class="card overflow-hidden">
|
||
<button @click="open = open === 'ldap' ? '' : 'ldap'" class="w-full flex items-center justify-between p-4 hover:bg-cyber-border/20 transition-colors">
|
||
<div class="flex items-center gap-3">
|
||
<span class="text-gray-400 font-bold">LDAP / Active Directory</span>
|
||
<span class="badge {% if vals.ldap_enabled == 'true' %}badge-green{% else %}badge-gray{% endif %}">{{ 'Activé' if vals.ldap_enabled == 'true' else 'Désactivé' }}</span>
|
||
<span class="text-xs text-gray-500">{{ vals.ldap_server or '' }}</span>
|
||
</div>
|
||
<span class="text-gray-500 text-lg" x-text="open === 'ldap' ? '▼' : '▶'"></span>
|
||
</button>
|
||
<div x-show="open === 'ldap'" class="border-t border-cyber-border p-4">
|
||
<form method="POST" action="/settings/ldap" class="space-y-3">
|
||
{% for key, label, is_secret in sections.ldap %}
|
||
<div>
|
||
<label class="text-xs text-gray-500">{{ label }}</label>
|
||
<input type="{{ 'password' if is_secret else 'text' }}" name="{{ key }}" value="{{ vals[key] }}" class="w-full" {% if not editable.ldap %}disabled{% endif %}>
|
||
</div>
|
||
{% endfor %}
|
||
<div class="text-xs text-gray-600 mt-2">
|
||
Une fois configuré et activé, le choix <b>Local / LDAP</b> apparaîtra sur la page de connexion. Les users peuvent aussi être forcés en LDAP via le champ "Auth" dans /users.
|
||
</div>
|
||
<div class="flex gap-2 items-center">
|
||
{% if editable.ldap %}<button type="submit" class="btn-primary px-4 py-2 text-sm">Sauvegarder</button>{% endif %}
|
||
<button type="button" onclick="testLdap()" class="btn-sm bg-cyber-border text-gray-300 px-4 py-2">Tester la connexion</button>
|
||
<span id="ldap-test-result" class="text-xs ml-2"></span>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
</div>
|
||
|
||
<script>
|
||
function testLdap() {
|
||
var out = document.getElementById('ldap-test-result');
|
||
out.textContent = 'Test en cours...';
|
||
out.className = 'text-xs ml-2 text-gray-400';
|
||
fetch('/settings/ldap/test', {method: 'POST', credentials: 'same-origin'})
|
||
.then(function(r){ return r.json(); })
|
||
.then(function(d){
|
||
if (d.ok) { out.textContent = '✓ ' + (d.msg || 'OK'); out.className = 'text-xs ml-2 text-cyber-green'; }
|
||
else { out.textContent = '✗ ' + (d.msg || 'Erreur'); out.className = 'text-xs ml-2 text-cyber-red'; }
|
||
})
|
||
.catch(function(e){ out.textContent = '✗ ' + e.message; out.className = 'text-xs ml-2 text-cyber-red'; });
|
||
}
|
||
|
||
function showClusterServers(scId, scName) {
|
||
var detail = document.getElementById('cluster-servers-detail');
|
||
var title = document.getElementById('cluster-servers-title');
|
||
var list = document.getElementById('cluster-servers-list');
|
||
title.textContent = 'Cluster : ' + scName;
|
||
list.innerHTML = '<span class="text-gray-500">Chargement…</span>';
|
||
detail.classList.remove('hidden');
|
||
fetch('/settings/server-cluster/' + scId + '/servers', {credentials: 'same-origin'})
|
||
.then(function(r){ return r.json(); })
|
||
.then(function(d){
|
||
if (!d.ok) { list.innerHTML = '<span class="text-cyber-red">' + (d.msg||'Erreur') + '</span>'; return; }
|
||
if (!d.servers.length) { list.innerHTML = '<span class="text-gray-500">Aucun serveur dans ce cluster.</span>'; return; }
|
||
var rows = d.servers.map(function(s){
|
||
var ord = (s.order !== null && s.order !== undefined) ? s.order : '–';
|
||
return '<tr><td class="p-1 text-center">' + ord + '</td>'
|
||
+ '<td class="p-1 font-mono text-cyber-accent">' + s.hostname + '</td>'
|
||
+ '<td class="p-1 text-gray-400">' + (s.machine_type || '-') + '</td>'
|
||
+ '<td class="p-1 text-gray-400">' + (s.etat || '-') + '</td></tr>';
|
||
}).join('');
|
||
list.innerHTML = '<table class="w-full text-xs">'
|
||
+ '<thead><tr class="text-cyber-accent border-b border-cyber-border">'
|
||
+ '<th class="p-1">Ordre</th><th class="text-left p-1">Hostname</th>'
|
||
+ '<th class="text-left p-1">Type</th><th class="text-left p-1">État</th></tr></thead>'
|
||
+ '<tbody>' + rows + '</tbody></table>';
|
||
})
|
||
.catch(function(e){ list.innerHTML = '<span class="text-cyber-red">' + e.message + '</span>'; });
|
||
}
|
||
|
||
function testTeams(tcId, tcName) {
|
||
fetch('/settings/teams-channel/' + tcId + '/test', {method: 'POST', credentials: 'same-origin'})
|
||
.then(function(r){ return r.json(); })
|
||
.then(function(d){
|
||
if (d.ok) alert('✓ Test envoyé sur "' + tcName + '" — vérifie le canal Teams (HTTP ' + d.status + ')');
|
||
else alert('✗ Échec test "' + tcName + '" — HTTP ' + (d.status || '?') + '\n' + (d.detail || d.msg || ''));
|
||
})
|
||
.catch(function(e){ alert('✗ Erreur réseau : ' + e.message); });
|
||
}
|
||
</script>
|
||
{% endblock %}
|