patchcenter/app/templates/settings.html

756 lines
46 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% 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 }}' ? '&#9660;' : '&#9654;'"></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' ? '&#9660;' : '&#9654;'"></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' ? '&#9660;' : '&#9654;'"></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' ? '&#9660;' : '&#9654;'"></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' ? '&#9660;' : '&#9654;'"></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' ? '&#9660;' : '&#9654;'"></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' ? '&#9660;' : '&#9654;'"></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 %}