Le moteur de calcul autonome du projet GRCA100.

 

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

Atténuation des risques par IA 20 cadres de gouvernance et de normalisation de l’IA,

  Base de données d’atténuation des risques par IA 831 actions d’atténuation extraites de 20 cadres de gouvernance et de nor...