GRCA100 Scoring Engine — Mode d'emploi complet
grca100_scoring.py · Version 2025.1.0
Source : MIT AIRI Navigator · 831 atténuations · 5 axes · 23 sous-catégories
Auteur : Erol GIRAUDY / UGAIA · AUDIT-SERVICES
1. Présentation en 60 secondes
grca100_scoring.py est le moteur de calcul autonome du projet GRCA100. Il lit directement le fichier Excel GRCA100_Matrice_Complète_MIT831.xlsx, calcule les scores de conformité IA en temps réel, et produit soit un rapport coloré en console, soit un fichier JSON exploitable par n'importe quel pipeline ou dashboard.
Ce qu'il fait que le fichier Excel ne fait pas :
- Exécutable en ligne de commande, sans ouvrir Excel
- Filtrable par rôle (RSSI, DPO, DSI, COMEX) ou par axe GRCA100
- Intégrable dans des scripts PowerShell, des pipelines CI/CD, des webhooks
- Exportable en JSON structuré pour Power BI, Grafana, Tableau, API REST
- Planifiable (cron, Task Scheduler) pour un monitoring continu automatique
- Versionnable dans Git — le script est du texte pur, l'Excel ne l'est pas
2. Prérequis
Python
Python 3.8 minimum. Vérification :
python --version
# ou
python3 --version
Bibliothèques
pip install openpyxl pandas
Vérification rapide :
python -c "import openpyxl, pandas; print('OK')"
Fichier Excel
Le fichier GRCA100_Matrice_Complète_MIT831.xlsx doit être accessible. Le script le détecte automatiquement s'il est dans le même répertoire.
3. Installation
Placez les deux fichiers dans le même répertoire :
/votre-dossier-audit/
├── GRCA100_Matrice_Complète_MIT831.xlsx ← votre matrice
└── grca100_scoring.py ← ce script
Aucune installation supplémentaire. Pas de dépendances externes au-delà de openpyxl et pandas.
4. Commandes disponibles
4.1 Rapport complet (usage standard)
# Auto-détection du fichier GRCA100*.xlsx dans le répertoire courant
python grca100_scoring.py
# Fichier explicite
python grca100_scoring.py GRCA100_Matrice_Complète_MIT831.xlsx
Sortie : rapport complet coloré avec score global, scores par axe, détail des 23 sous-catégories, alertes, et top 5 priorités.
4.2 Export JSON
python grca100_scoring.py GRCA100_Matrice.xlsx --json
Génère : GRCA100_Matrice_scores.json dans le même répertoire.
Structure JSON exportée :
{
"metadata": {
"version": "2025.1.0",
"generated_at": "2025-06-15T09:00:00",
"filtre_role": null,
"filtre_axe": null,
"total_mesures": 815
},
"score_global": {
"total": 815,
"conformes": 47,
"non_conformes": 12,
"na": 23,
"a_evaluer": 733,
"evaluees": 792,
"score_pct": 5.9,
"score_pond": 0.0118,
"niveau": "🔴 NON-CONFORME",
"message": "Risque majeur..."
},
"axes": {
"Gouvernance": { "score_pct": ..., "conformes": ..., "evaluees": ..., "niveau": ... },
"Sécurité": { ... },
"Conformité": { ... },
"Souveraineté":{ ... },
"Résilience": { ... }
},
"sous_categories": [
{
"code": "3.1",
"sous_categorie": "Tests et audits",
"axe": "Résilience",
"criticite": "C4",
"score_m1": 0.3,
"score_pct": 0.0,
"score_pond": 0.0,
"impact_potentiel": 15.24,
"niveau": "🔴 NON-CONFORME",
...
}
],
"top_priorites": [ ... ],
"alertes": [ ... ]
}
4.3 Filtrage par rôle
# Vue RSSI uniquement (522 mesures sur 815 impliquent le RSSI)
python grca100_scoring.py GRCA100_Matrice.xlsx --role RSSI
# Vue DPO
python grca100_scoring.py GRCA100_Matrice.xlsx --role DPO
# Vue DSI
python grca100_scoring.py GRCA100_Matrice.xlsx --role DSI
# Vue COMEX
python grca100_scoring.py GRCA100_Matrice.xlsx --role COMEX
Le filtre cherche dans les colonnes Rôle_Principal ET Rôles_Secondaires — il capture donc toutes les mesures où le rôle est impliqué, même en secondaire.
4.4 Filtrage par axe GRCA100
python grca100_scoring.py GRCA100_Matrice.xlsx --axe Sécurité
python grca100_scoring.py GRCA100_Matrice.xlsx --axe Gouvernance
python grca100_scoring.py GRCA100_Matrice.xlsx --axe Conformité
python grca100_scoring.py GRCA100_Matrice.xlsx --axe Résilience
python grca100_scoring.py GRCA100_Matrice.xlsx --axe Souveraineté
4.5 Combinaisons
# Rapport RSSI sur l'axe Sécurité, exporté en JSON
python grca100_scoring.py GRCA100_Matrice.xlsx --role RSSI --axe Sécurité --json
# Top 10 priorités pour le DPO
python grca100_scoring.py GRCA100_Matrice.xlsx --role DPO --top 10
# Vue COMEX complète avec export JSON
python grca100_scoring.py GRCA100_Matrice.xlsx --role COMEX --json
4.6 Aide intégrée
python grca100_scoring.py --help
python grca100_scoring.py --version
5. Logique de scoring — Ce que calcule le script
Score Global (%)
Score% = Conformes ÷ (Total − NA)
Les mesures "À évaluer" sont incluses au dénominateur : elles font baisser le score tant qu'elles ne sont pas qualifiées. C'est intentionnel — cela crée une pression opérationnelle à qualifier rapidement les mesures.
Score Pondéré
Score_Pondéré = Score% × Score_M1 × Poids_Criticité
| Criticité | Poids | Score_M1 typique |
|---|---|---|
| C1 — Faible | 0.10 | 0.15 |
| C2 — Modéré | 0.20 | 0.15 – 0.20 |
| C3 — Élevé | 0.30 | 0.20 – 0.25 |
| C4 — Critique | 0.40 | 0.25 – 0.30 |
Le score pondéré permet de comparer les sous-catégories entre elles en tenant compte de leur importance stratégique relative.
Niveaux de conformité
| Score | Niveau | Signification |
|---|---|---|
| ≥ 85% | 🟢 CONFORME | Conformité robuste. Surveillance recommandée. |
| 65–84% | 🟡 EN COURS | Plan d'action en cours. |
| 40–64% | 🟠 PARTIEL | Lacunes significatives. Prioriser C4. |
| < 40% | 🔴 NON-CONFORME | Risque majeur. Plan d'urgence requis. |
Calcul du Top N Priorités
Impact_potentiel = nb_à_évaluer × Poids_Criticité × Score_M1
Cet indicateur identifie les sous-catégories qui, si qualifiées rapidement, auront le plus d'impact sur le score global pondéré.
6. Workflow recommandé — 5 étapes d'audit
Étape 1 — Baseline initial
python grca100_scoring.py GRCA100_Matrice.xlsx --json
Générez et archivez le JSON de départ. C'est votre t=0.
Étape 2 — Assignation par rôle
python grca100_scoring.py GRCA100_Matrice.xlsx --role RSSI > rapport_RSSI.txt
python grca100_scoring.py GRCA100_Matrice.xlsx --role DPO > rapport_DPO.txt
python grca100_scoring.py GRCA100_Matrice.xlsx --role DSI > rapport_DSI.txt
Transmettez chaque rapport au responsable concerné. Il sait exactement quelles mesures lui appartiennent.
Étape 3 — Saisie dans Excel
L'équipe renseigne la colonne O (Statut) dans l'onglet AuditInterne : Conforme / Non-Conforme / NA / À évaluer.
Étape 4 — Mesure de la progression
python grca100_scoring.py GRCA100_Matrice.xlsx
Relancez après chaque session de qualification. Le score monte en temps réel.
Étape 5 — Rapport COMEX
python grca100_scoring.py GRCA100_Matrice.xlsx --role COMEX --json
Le JSON alimente un dashboard Power BI ou une présentation synthétique.
7. Intégration dans un pipeline automatisé
PowerShell (Windows — monitoring hebdomadaire)
# weekly_grca100.ps1
$json = python grca100_scoring.py GRCA100_Matrice.xlsx --json 2>&1
$scores = Get-Content "GRCA100_Matrice_scores.json" | ConvertFrom-Json
$score = $scores.score_global.score_pct
$niveau = $scores.score_global.niveau
# Envoi Teams
$body = @{
text = "📊 GRCA100 Weekly Report | Score: $score% | Niveau: $niveau"
} | ConvertTo-Json
Invoke-RestMethod -Uri $env:TEAMS_WEBHOOK -Method POST -Body $body -ContentType "application/json"
Bash/Cron (Linux — exécution automatique)
# Ajout au crontab : exécution chaque lundi à 8h
0 8 * * 1 cd /opt/audit/grca100 && python3 grca100_scoring.py --json >> logs/grca100_$(date +%Y%m%d).log 2>&1
Python CI/CD (vérification de seuil minimum)
import subprocess, json, sys
result = subprocess.run(
["python3", "grca100_scoring.py", "GRCA100_Matrice.xlsx", "--json"],
capture_output=True, text=True
)
with open("GRCA100_Matrice_scores.json") as f:
data = json.load(f)
score = data["score_global"]["score_pct"]
SEUIL_MINIMUM = 65 # EN COURS minimum requis
if score < SEUIL_MINIMUM:
print(f"❌ GRCA100 score {score}% < seuil {SEUIL_MINIMUM}%")
sys.exit(1) # Fail la pipeline CI/CD
print(f"✅ GRCA100 score {score}% ≥ seuil {SEUIL_MINIMUM}%")
Intégration API REST (FastAPI)
from fastapi import FastAPI
import subprocess, json
app = FastAPI()
@app.get("/grca100/score")
def get_score():
subprocess.run(["python3", "grca100_scoring.py", "GRCA100_Matrice.xlsx", "--json"])
with open("GRCA100_Matrice_scores.json") as f:
return json.load(f)
8. Avantages dans le cadre du projet GRCA100
| Besoin | Solution Excel seule | Solution Excel + Script |
|---|---|---|
| Calcul des scores | ✅ Formules onglet Scoring | ✅ Temps réel, sans ouvrir Excel |
| Filtrage par rôle | ⚠️ Filtre manuel colonne J | ✅ --role RSSI en une commande |
| Monitoring automatique | ❌ Impossible | ✅ Cron/Task Scheduler |
| Intégration dashboard | ❌ Copier-coller | ✅ JSON → Power BI / Grafana |
| Versionning | ❌ Fichier binaire | ✅ Script texte dans Git |
| Audit trail | ❌ Pas de log | ✅ Log horodaté à chaque exécution |
| Pipeline CI/CD | ❌ Impossible | ✅ Exit code selon seuil de conformité |
| Rapport multi-rôles | ⚠️ Un onglet, filtrage manuel | ✅ 4 rapports en 4 commandes |
| Portabilité | ❌ Nécessite Office/LibreOffice | ✅ Python 3.8+, pip install, done |
9. Interprétation des résultats — Erreurs fréquentes à ne pas commettre
Score à 0% ne signifie pas "non conforme" tant que les statuts sont tous "À évaluer". C'est l'état initial normal. Un score à 0% avec 0 Non-Conformes est différent d'un score à 0% avec des Non-Conformes.
Le score pondéré est comparatif, pas absolu. Il sert à comparer les sous-catégories entre elles, pas à calculer un pourcentage de conformité réglementaire.
Les mesures NA réduisent le dénominateur, ce qui améliore le score. Vérifiez que les NA sont légitimes (mesure hors périmètre) et non utilisés pour "gonfler" le score.
L'impact_potentiel dans le Top N mesure ce que vous pourriez gagner sur le score pondéré global en qualifiant ces mesures en priorité — pas leur niveau de risque absolu.
10. Structure des fichiers produits
/votre-dossier-audit/
├── GRCA100_Matrice_Complète_MIT831.xlsx ← matrice source (ne pas modifier A-N)
├── grca100_scoring.py ← ce script
├── GRCA100_Matrice_Complète_MIT831_scores.json ← export JSON (généré par --json)
└── logs/
├── grca100_20250601.log ← rapport console archivé
└── grca100_20250608.log
11. Dépannage
| Erreur | Cause probable | Solution |
|---|---|---|
Aucun fichier GRCA100*.xlsx trouvé |
Script et fichier pas dans le même répertoire | cd /chemin/vers/dossier puis relancer |
Onglet 'AuditInterne' introuvable |
Fichier Excel corrompu ou mauvais fichier | Vérifier que c'est bien le GRCA100_Matrice |
Colonnes manquantes |
En-têtes modifiés dans AuditInterne | Restaurer les en-têtes ligne 3 de l'onglet |
ModuleNotFoundError: openpyxl |
Bibliothèque non installée | pip install openpyxl pandas |
Statuts ⚠ non reconnus |
Valeurs saisies hors dropdown (espaces, majuscules) | Utiliser le dropdown Excel colonne O |
| JSON non généré | Pas le flag --json |
Ajouter --json à la commande |
12. Référence des arguments
python grca100_scoring.py [fichier] [options]
Arguments positionnels :
fichier Chemin vers le fichier XLSX (optionnel : auto-détection)
Options :
--json Exporter les résultats en JSON
--role ROLE Filtrer par rôle : DSI, DPO, RSSI, COMEX
--axe AXE Filtrer par axe : Gouvernance, Sécurité, Conformité,
Souveraineté, Résilience
--top N Nombre de priorités à afficher (défaut : 5)
--version Afficher la version du script
--help Afficher cette aide
GRCA100 · AUDIT-SERVICES · MIT AIRI Navigator · airi-navigator.com/mitigations · Version 2025
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
╔══════════════════════════════════════════════════════════════════════════════╗
║ GRCA100 — Scoring Engine · Conformité IA · MIT 831 ║
║ Version 2025 · AUDIT-SERVICES · Erol GIRAUDY / UGAIA ║
║ Source : MIT AIRI Navigator — airi-navigator.com/mitigations ║
╚══════════════════════════════════════════════════════════════════════════════╝
USAGE :
python grca100_scoring.py # auto-détection xlsx
python grca100_scoring.py GRCA100_Matrice.xlsx # fichier explicite
python grca100_scoring.py GRCA100_Matrice.xlsx --json # export JSON
python grca100_scoring.py GRCA100_Matrice.xlsx --role RSSI # filtrage rôle
python grca100_scoring.py GRCA100_Matrice.xlsx --axe Sécurité # filtrage axe
python grca100_scoring.py GRCA100_Matrice.xlsx --top 10 # top N priorités
PRÉREQUIS :
pip install openpyxl pandas
"""
import sys
import os
import glob
import json
import argparse
import datetime
from pathlib import Path
try:
import pandas as pd
import openpyxl
except ImportError as e:
print(f"\n❌ Dépendance manquante : {e}")
print(" Installez avec : pip install openpyxl pandas")
sys.exit(1)
# ─────────────────────────────────────────────────────────────────────────────
# CONSTANTES
# ─────────────────────────────────────────────────────────────────────────────
VERSION = "2025.1.0"
STATUTS_VALIDES = ["Conforme", "Non-Conforme", "NA", "À évaluer"]
POIDS_CRITICITE = {"C1": 0.10, "C2": 0.20, "C3": 0.30, "C4": 0.40}
NIVEAUX = [
(85, "🟢 CONFORME", "Conformité robuste. Maintien et surveillance recommandés."),
(65, "🟡 EN COURS", "Conformité partielle. Plan d'action en cours."),
(40, "🟠 PARTIEL", "Lacunes significatives. Prioriser les mesures C4."),
(0, "🔴 NON-CONFORME", "Risque majeur. Plan d'urgence requis. Signalement possible."),
]
# Couleurs ANSI (désactivées si terminal ne supporte pas)
USE_COLOR = sys.stdout.isatty() or os.getenv("FORCE_COLOR")
C = {
"reset": "\033[0m" if USE_COLOR else "",
"bold": "\033[1m" if USE_COLOR else "",
"red": "\033[91m" if USE_COLOR else "",
"orange": "\033[93m" if USE_COLOR else "",
"yellow": "\033[33m" if USE_COLOR else "",
"green": "\033[92m" if USE_COLOR else "",
"blue": "\033[94m" if USE_COLOR else "",
"cyan": "\033[96m" if USE_COLOR else "",
"grey": "\033[90m" if USE_COLOR else "",
"white": "\033[97m" if USE_COLOR else "",
}
AXES_COULEURS = {
"Gouvernance": C["blue"],
"Sécurité": C["red"],
"Conformité": C["cyan"],
"Souveraineté": C["yellow"],
"Résilience": C["green"],
}
# ─────────────────────────────────────────────────────────────────────────────
# DÉTECTION DU FICHIER
# ─────────────────────────────────────────────────────────────────────────────
def detecter_fichier(chemin_arg=None):
"""
Retourne le chemin du fichier XLSX à analyser.
Priorité : argument CLI > auto-détection GRCA100*.xlsx dans le répertoire courant.
"""
if chemin_arg:
if not os.path.isfile(chemin_arg):
_erreur(f"Fichier introuvable : {chemin_arg}")
return chemin_arg
patterns = ["GRCA100*.xlsx", "grca100*.xlsx"]
for p in patterns:
matches = glob.glob(p)
if matches:
return sorted(matches)[0]
_erreur(
"Aucun fichier GRCA100*.xlsx trouvé dans le répertoire courant.\n"
" Spécifiez le chemin : python grca100_scoring.py mon_fichier.xlsx"
)
# ─────────────────────────────────────────────────────────────────────────────
# CHARGEMENT DES DONNÉES
# ─────────────────────────────────────────────────────────────────────────────
def charger_donnees(chemin_xlsx):
"""
Lit l'onglet AuditInterne.
Les 2 premières lignes sont des titres/sous-titres, les en-têtes sont en ligne 3.
Retourne un DataFrame propre et validé.
"""
try:
df = pd.read_excel(chemin_xlsx, sheet_name="AuditInterne", header=2)
except ValueError:
_erreur(
"Onglet 'AuditInterne' introuvable dans le fichier.\n"
" Vérifiez que le fichier est bien un GRCA100_Matrice_Complète_MIT831.xlsx."
)
colonnes_requises = [
"ID_Mesure", "Catégorie_MIT", "Sous-Catégorie_MIT", "Code_Sous-Cat",
"Criticité", "Score_M1", "Axe_GRCA100", "Rôle_Principal",
"Rôles_Secondaires", "Commande_GRCA100", "Statut"
]
manquantes = [c for c in colonnes_requises if c not in df.columns]
if manquantes:
_erreur(f"Colonnes manquantes dans AuditInterne : {manquantes}")
# Nettoyage
df = df.dropna(subset=["ID_Mesure"]).copy()
df["Statut"] = df["Statut"].fillna("À évaluer").str.strip()
df["Criticité"] = df["Criticité"].fillna("C3").str.strip()
df["Score_M1"] = pd.to_numeric(df["Score_M1"], errors="coerce").fillna(0.20)
df["Axe_GRCA100"] = df["Axe_GRCA100"].fillna("Non défini").str.strip()
# Validation des statuts
invalides = df[~df["Statut"].isin(STATUTS_VALIDES)]
if not invalides.empty:
print(f"{C['yellow']}⚠ {len(invalides)} statut(s) non reconnu(s) → traités comme 'À évaluer'{C['reset']}")
df.loc[~df["Statut"].isin(STATUTS_VALIDES), "Statut"] = "À évaluer"
# Ajout du poids criticité
df["Poids_Crit"] = df["Criticité"].map(POIDS_CRITICITE).fillna(0.20)
return df
# ─────────────────────────────────────────────────────────────────────────────
# MOTEUR DE SCORING
# ─────────────────────────────────────────────────────────────────────────────
def calculer_score_ligne(groupe):
"""
Pour un groupe de mesures (sous-catégorie ou axe) :
- score_pct = Conforme / (Total - NA) [0–100]
- score_pond = score_pct × Score_M1_moyen × Poids_Crit_moyen
"""
total = len(groupe)
conformes = (groupe["Statut"] == "Conforme").sum()
nc = (groupe["Statut"] == "Non-Conforme").sum()
na = (groupe["Statut"] == "NA").sum()
a_evaluer = (groupe["Statut"] == "À évaluer").sum()
evaluees = total - na
score_pct = round((conformes / evaluees * 100) if evaluees > 0 else 0.0, 1)
score_m1 = groupe["Score_M1"].mean()
poids_crit = groupe["Poids_Crit"].mean()
score_pond = round(score_pct * score_m1 * poids_crit, 4)
return {
"total": int(total),
"conformes": int(conformes),
"non_conformes": int(nc),
"na": int(na),
"a_evaluer": int(a_evaluer),
"evaluees": int(evaluees),
"score_pct": score_pct,
"score_m1": round(score_m1, 3),
"poids_crit": round(poids_crit, 3),
"score_pond": score_pond,
}
def calculer_niveau(score_pct):
"""Retourne (emoji_niveau, libellé_court, message, couleur_ansi)."""
for seuil, label, message in NIVEAUX:
if score_pct >= seuil:
if "CONFORME" in label and "NON" not in label:
couleur = C["green"]
elif "EN COURS" in label:
couleur = C["yellow"]
elif "PARTIEL" in label:
couleur = C["orange"]
else:
couleur = C["red"]
return label, message, couleur
return "🔴 NON-CONFORME", NIVEAUX[-1][2], C["red"]
def scorer(df, filtre_role=None, filtre_axe=None):
"""
Calcule l'intégralité des scores GRCA100.
Retourne un dict structuré exploitable en console ou JSON.
"""
df_work = df.copy()
# Filtrage optionnel
if filtre_role:
mask = (
df_work["Rôle_Principal"].str.contains(filtre_role, case=False, na=False) |
df_work["Rôles_Secondaires"].str.contains(filtre_role, case=False, na=False)
)
df_work = df_work[mask]
if df_work.empty:
_erreur(f"Aucune mesure trouvée pour le rôle : {filtre_role}")
if filtre_axe:
df_work = df_work[df_work["Axe_GRCA100"].str.contains(filtre_axe, case=False, na=False)]
if df_work.empty:
_erreur(f"Aucune mesure trouvée pour l'axe : {filtre_axe}")
# ── Score global ──────────────────────────────────────────────────────────
global_scores = calculer_score_ligne(df_work)
niveau_g, msg_g, _ = calculer_niveau(global_scores["score_pct"])
global_scores.update({"niveau": niveau_g, "message": msg_g})
# ── Scores par axe ────────────────────────────────────────────────────────
axes = {}
for axe, grp in df_work.groupby("Axe_GRCA100"):
s = calculer_score_ligne(grp)
niveau, msg, _ = calculer_niveau(s["score_pct"])
s.update({"niveau": niveau, "message": msg})
axes[axe] = s
# ── Scores par sous-catégorie ─────────────────────────────────────────────
sous_cats = []
for (code, nom, axe, crit, m1), grp in df_work.groupby(
["Code_Sous-Cat", "Sous-Catégorie_MIT", "Axe_GRCA100", "Criticité", "Score_M1"]
):
s = calculer_score_ligne(grp)
niveau, msg, _ = calculer_niveau(s["score_pct"])
impact = round(s["a_evaluer"] * s["poids_crit"] * s["score_m1"], 4)
sous_cats.append({
"code": code,
"sous_categorie": nom,
"axe": axe,
"criticite": crit,
"score_m1": m1,
**s,
"niveau": niveau,
"message": msg,
"impact_potentiel": impact,
})
# Tri par score croissant (les plus critiques en premier)
sous_cats.sort(key=lambda x: (x["score_pct"], -x["impact_potentiel"]))
# ── Top N prioritaires (mesures À évaluer avec impact max) ───────────────
top_priorites = sorted(
[s for s in sous_cats if s["a_evaluer"] > 0],
key=lambda x: -x["impact_potentiel"]
)
# ── Alertes (score < 40%) ─────────────────────────────────────────────────
alertes = [s for s in sous_cats if s["score_pct"] < 40 and s["evaluees"] > 0]
return {
"metadata": {
"version": VERSION,
"generated_at": datetime.datetime.now().isoformat(),
"filtre_role": filtre_role,
"filtre_axe": filtre_axe,
"total_mesures": len(df_work),
},
"score_global": global_scores,
"axes": axes,
"sous_categories": sous_cats,
"top_priorites": top_priorites,
"alertes": alertes,
}
# ─────────────────────────────────────────────────────────────────────────────
# AFFICHAGE CONSOLE
# ─────────────────────────────────────────────────────────────────────────────
def barre_progression(pct, largeur=40):
"""Génère une barre de progression ASCII colorée."""
_, _, couleur = calculer_niveau(pct)
rempli = int(largeur * pct / 100)
vide = largeur - rempli
barre = f"{couleur}{'█' * rempli}{'░' * vide}{C['reset']}"
return f"[{barre}] {couleur}{pct:5.1f}%{C['reset']}"
def sep(char="═", largeur=78):
return f"{C['grey']}{char * largeur}{C['reset']}"
def afficher_rapport(resultats, top_n=5):
"""Affiche le rapport complet en console."""
meta = resultats["metadata"]
globl = resultats["score_global"]
axes = resultats["axes"]
sc = resultats["sous_categories"]
top = resultats["top_priorites"][:top_n]
alrt = resultats["alertes"]
print()
print(sep("═"))
print(f" {C['bold']}{C['white']}GRCA100 — RAPPORT DE CONFORMITÉ IA{C['reset']} "
f"{C['grey']}v{VERSION} · {meta['generated_at'][:10]}{C['reset']}")
print(f" {C['grey']}Source : MIT AIRI Navigator · 831 atténuations · 5 axes · 23 sous-catégories{C['reset']}")
if meta["filtre_role"]:
print(f" {C['yellow']}⚑ Filtrage rôle : {meta['filtre_role']}{C['reset']}")
if meta["filtre_axe"]:
print(f" {C['yellow']}⚑ Filtrage axe : {meta['filtre_axe']}{C['reset']}")
print(sep("═"))
# ── Score global ──────────────────────────────────────────────────────────
print()
print(f" {C['bold']}SCORE GLOBAL GRCA100{C['reset']}")
print(f" {barre_progression(globl['score_pct'])}")
niv, msg, coul = calculer_niveau(globl["score_pct"])
print(f" {coul}{C['bold']}{niv}{C['reset']} — {msg}")
print()
print(f" {'Mesures totales':<22} {globl['total']:>5}")
print(f" {C['green']}{'Conformes':<22}{C['reset']} {globl['conformes']:>5} "
f"({globl['conformes']/max(globl['evaluees'],1)*100:.0f}% des évaluées)")
print(f" {C['red']}{'Non-Conformes':<22}{C['reset']} {globl['non_conformes']:>5}")
print(f" {C['grey']}{'NA (hors calcul)':<22}{C['reset']} {globl['na']:>5}")
print(f" {C['yellow']}{'À évaluer':<22}{C['reset']} {globl['a_evaluer']:>5}")
print(f" {'Score pondéré global':<22} {globl['score_pond']:>8.4f}")
print()
# ── Scores par axe ────────────────────────────────────────────────────────
print(sep("─"))
print(f"\n {C['bold']}SCORES PAR AXE GRCA100{C['reset']}\n")
for axe in ["Gouvernance", "Sécurité", "Conformité", "Souveraineté", "Résilience"]:
if axe not in axes:
continue
a = axes[axe]
coul_axe = AXES_COULEURS.get(axe, C["white"])
niv, _, _ = calculer_niveau(a["score_pct"])
print(f" {coul_axe}{C['bold']}{axe:<16}{C['reset']} "
f"{barre_progression(a['score_pct'], largeur=28)} "
f"{C['grey']}{a['conformes']}/{a['evaluees']} évaluées "
f"pond.={a['score_pond']:.4f}{C['reset']}")
print()
# ── Détail sous-catégories ────────────────────────────────────────────────
print(sep("─"))
print(f"\n {C['bold']}DÉTAIL — 23 SOUS-CATÉGORIES (classées par score croissant){C['reset']}\n")
print(f" {'Code':<6} {'Sous-catégorie':<46} {'Crit':<5} {'Score%':>7} {'Conf/Ev':>8} {'À eval':>6} Niveau")
print(f" {'-'*6} {'-'*46} {'-'*5} {'-'*7} {'-'*8} {'-'*6} {'─'*14}")
for s in sc:
coul_axe = AXES_COULEURS.get(s["axe"], C["white"])
_, _, coul = calculer_niveau(s["score_pct"])
alerte = f" {C['red']}◄ ALERTE{C['reset']}" if s["score_pct"] < 40 and s["evaluees"] > 0 else ""
print(
f" {coul_axe}{s['code']:<6}{C['reset']} "
f"{s['sous_categorie'][:46]:<46} "
f"{C['grey']}{s['criticite']:<5}{C['reset']} "
f"{coul}{s['score_pct']:>7.1f}%{C['reset']} "
f"{s['conformes']:>4}/{s['evaluees']:<4} "
f"{C['yellow']}{s['a_evaluer']:>6}{C['reset']} "
f"{s['niveau'][:14]}"
f"{alerte}"
)
print()
# ── Alertes ───────────────────────────────────────────────────────────────
if alrt:
print(sep("─"))
print(f"\n {C['red']}{C['bold']}🚨 ALERTES — Sous-catégories < 40% (risque majeur){C['reset']}\n")
for a in alrt:
print(f" {C['red']}▶ [{a['code']}] {a['sous_categorie']} → {a['score_pct']}% "
f"({a['non_conformes']} Non-Conformes){C['reset']}")
print()
# ── Top N priorités ───────────────────────────────────────────────────────
print(sep("─"))
print(f"\n {C['bold']}TOP {top_n} PRIORITÉS — Sous-catégories à qualifier en urgence{C['reset']}")
print(f" {C['grey']}(Classées par : nb_à_évaluer × poids_criticité × Score_M1){C['reset']}\n")
for i, s in enumerate(top, 1):
coul_axe = AXES_COULEURS.get(s["axe"], C["white"])
print(
f" {C['bold']}#{i}{C['reset']} {coul_axe}[{s['code']}] {s['sous_categorie']}{C['reset']}\n"
f" Axe: {s['axe']} | Criticité: {s['criticite']} | Score_M1: {s['score_m1']}\n"
f" À évaluer: {C['yellow']}{s['a_evaluer']}{C['reset']} mesures | "
f"Impact potentiel: {C['orange']}{s['impact_potentiel']:.4f}{C['reset']}\n"
)
print(sep("═"))
print(f" {C['grey']}GRCA100 · AUDIT-SERVICES · MIT AIRI Navigator · airi-navigator.com/mitigations{C['reset']}")
print(sep("═"))
print()
# ─────────────────────────────────────────────────────────────────────────────
# EXPORT JSON
# ─────────────────────────────────────────────────────────────────────────────
def exporter_json(resultats, chemin_xlsx):
"""Exporte les résultats dans un fichier _scores.json."""
base = Path(chemin_xlsx).stem
chemin_json = f"{base}_scores.json"
# Sérialisation des types numpy/pandas
def serialiser(obj):
if hasattr(obj, "item"):
return obj.item()
raise TypeError(f"Type non sérialisable : {type(obj)}")
with open(chemin_json, "w", encoding="utf-8") as f:
json.dump(resultats, f, ensure_ascii=False, indent=2, default=serialiser)
print(f"\n{C['green']}✅ Export JSON généré : {chemin_json}{C['reset']}")
print(f" Taille : {os.path.getsize(chemin_json):,} octets\n")
return chemin_json
# ─────────────────────────────────────────────────────────────────────────────
# UTILITAIRES
# ─────────────────────────────────────────────────────────────────────────────
def _erreur(message):
print(f"\n{C['red']}❌ ERREUR : {message}{C['reset']}\n")
sys.exit(1)
# ─────────────────────────────────────────────────────────────────────────────
# POINT D'ENTRÉE
# ─────────────────────────────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(
description="GRCA100 Scoring Engine — Conformité IA · MIT 831 atténuations",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Exemples :
python grca100_scoring.py
python grca100_scoring.py GRCA100_Matrice_Complète_MIT831.xlsx
python grca100_scoring.py GRCA100_Matrice.xlsx --json
python grca100_scoring.py GRCA100_Matrice.xlsx --role RSSI
python grca100_scoring.py GRCA100_Matrice.xlsx --axe Sécurité --json
python grca100_scoring.py GRCA100_Matrice.xlsx --top 10
"""
)
parser.add_argument("fichier", nargs="?", help="Chemin vers le fichier XLSX GRCA100")
parser.add_argument("--json", action="store_true", help="Exporter les résultats en JSON")
parser.add_argument("--role", type=str, default=None, metavar="ROLE",
help="Filtrer par rôle : DSI, DPO, RSSI, COMEX")
parser.add_argument("--axe", type=str, default=None, metavar="AXE",
help="Filtrer par axe GRCA100 : Gouvernance, Sécurité, Conformité, Souveraineté, Résilience")
parser.add_argument("--top", type=int, default=5, metavar="N",
help="Nombre de priorités à afficher (défaut : 5)")
parser.add_argument("--version", action="version", version=f"GRCA100 Scoring Engine v{VERSION}")
args = parser.parse_args()
# ── Résolution du fichier ─────────────────────────────────────────────────
chemin = detecter_fichier(args.fichier)
print(f"\n{C['grey']}📂 Fichier : {chemin}{C['reset']}")
# ── Chargement ────────────────────────────────────────────────────────────
print(f"{C['grey']}⏳ Chargement des données...{C['reset']}")
df = charger_donnees(chemin)
print(f"{C['green']}✓ {len(df)} mesures chargées{C['reset']}")
# ── Scoring ───────────────────────────────────────────────────────────────
print(f"{C['grey']}⚙ Calcul des scores en cours...{C['reset']}")
resultats = scorer(df, filtre_role=args.role, filtre_axe=args.axe)
# ── Affichage ─────────────────────────────────────────────────────────────
afficher_rapport(resultats, top_n=args.top)
# ── Export JSON optionnel ─────────────────────────────────────────────────
if args.json:
exporter_json(resultats, chemin)
if __name__ == "__main__":
main()
Aucun commentaire:
Enregistrer un commentaire