diff --git a/docs/audit/preuve-2026-08-13.md b/docs/audit/preuve-2026-08-13.md index ec6d3c1..83fa3b0 100644 --- a/docs/audit/preuve-2026-08-13.md +++ b/docs/audit/preuve-2026-08-13.md @@ -30,7 +30,7 @@ | P15 | Modele public socle valide | AFF-022, AFF-099 | ✅ OK | Modele public socle : domaines/serveurs/applications/bases valides. | | P16 | Inventaire Ansible complet (--list) | AFF-030 | ✅ OK | 14 hotes, 31 groupes (inventaire dechiffre et parse). | | P17 | Tous les modeles valident (registres + underlay) | AFF-022, AFF-099 | ✅ OK | Les 1 modele(s) decouvert(s) valident. | -| P18 | Gabarit de voute complet | AFF-026 | ✅ OK | Gabarit de voute complet : 24 secret(s) exige(s), tous presents. Voute reelle : 27 cle(s), aucun manque. | +| P18 | Gabarit de voute complet | AFF-026 | ✅ OK | Gabarit de voute complet : 26 secret(s) exige(s), tous presents. Voute reelle : 29 cle(s), aucun manque. | | P19 | Le GUI couvre le schema du plan | AFF-002, AFF-095 | ✅ OK | GUI : les 28 champ(s) des plans reels sont editables (2 plan(s) inspecte(s)), registres toleres : nomenclature. | | P20 | Adressage 100% derive du seed (aucun stocke) | AFF-001, AFF-003 | ✅ OK | 2 nomenclature(s) : adressage 100% derive du seed index. | | P21 | Federation : aucun index en collision | AFF-102 | ✅ OK | Federation coherente : 2 instance(s) federee(s), aucun index en collision. | @@ -44,7 +44,7 @@ | P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 23 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 12, socle-identite 2, web-sso 5) ; 2 lacune(s) nommee(s) : serveur_loki, serv | | P30 | SDN EVPN : zones, VNets et sous-reseaux derives | AFF-112 | ✅ OK | CONFORME : SDN EVPN, 2 zone(s), 12 VNet(s), 12 sous-reseau(x), aucune collision. | | P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 45 scripts expliques et atteignables, 93 cibles make documentees, 54 roles avec README. | -| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 35 exigence(s) de role, toutes satisfaites (118 cle(s) declaree(s) par l'instance). | +| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 35 exigence(s) de role, toutes satisfaites (126 cle(s) declaree(s) par l'instance). | | P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 32 revendication(s) de port, aucune collision entre roles co-localises (33 groupes). | | P34 | Chaque document declare son lecteur | — | ✅ OK | 40 document(s) declarent leur lecteur (16 genere(s) exempte(s)). | | P35 | Toute application exigeant une base en a une au plan | — | ✅ OK | 5 application(s) exigeant une base l'ont toutes (4 entree(s) au registre). | diff --git a/scripts/appliquer_opnsense.py b/scripts/appliquer_opnsense.py index 9752dba..b8c9b6f 100644 --- a/scripts/appliquer_opnsense.py +++ b/scripts/appliquer_opnsense.py @@ -29,6 +29,7 @@ import ssl import subprocess import sys import urllib.error +import urllib.parse import urllib.request from pathlib import Path @@ -63,11 +64,35 @@ class Frontiere: self.auth = "Basic " + base64.b64encode(f"{cle}:{secret}".encode()).decode() self.ctx = None if verifier else ssl._create_unverified_context() + @staticmethod + def _formulaire(corps: dict) -> bytes: + """Encode `{racine: {champ: valeur}}` en `racine[champ]=valeur`. + + POURQUOI PAS DU JSON (mesure du 2026-08-13, sur un OPNsense 24.7 neuf). + Le controleur d'OPNsense lit ses champs avec `hasPost()`. Quand le + corps arrive en `application/json` et que le boitier ne le decompose pas en + variables de POST, ce test est FAUX : le controleur renvoie `{"result": + "failed"}` NU — sans la moindre validation, donc sans dire ce qui manque. + Toute ecriture echouait ainsi : alias, regles, NAT, routes. Le meme corps en + formulaire encode passe du premier coup. + + C'est la forme que poste l'interface web elle-meme : aucune version + d'OPNsense ne peut la refuser, alors que le JSON depend du boitier. + """ + champs: dict[str, str] = {} + for racine, valeurs in corps.items(): + if isinstance(valeurs, dict): + for k, v in valeurs.items(): + champs[f"{racine}[{k}]"] = "" if v is None else str(v) + else: + champs[racine] = "" if valeurs is None else str(valeurs) + return urllib.parse.urlencode(champs).encode() + def __call__(self, chemin: str, corps: dict | None = None) -> dict: - data = json.dumps(corps).encode() if corps is not None else None + data = self._formulaire(corps) if corps is not None else None h = {"Authorization": self.auth} - if data: - h["Content-Type"] = "application/json" + if data is not None: + h["Content-Type"] = "application/x-www-form-urlencoded" req = urllib.request.Request(self.base + chemin, data=data, headers=h, method="POST" if data is not None else "GET") try: