#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Script pour générer un rapport factorisé des vulnérabilités critiques suivant la structure définie dans Remarques.md. """ import os import sys import re import yaml from networkx.drawing.nx_agraph import read_dot from pathlib import Path # Chemins de base BASE_DIR = Path(__file__).resolve().parent CORPUS_DIR = BASE_DIR.parent / "Corpus" CONFIG_PATH = BASE_DIR / "config.yml" THRESHOLDS_PATH = BASE_DIR.parent / "assets" / "config.yaml" REFERENCE_GRAPH_PATH = BASE_DIR.parent / "schema.txt" def load_config(config_path, thresholds_path=THRESHOLDS_PATH): """Charge la configuration depuis les fichiers YAML.""" # Charger la configuration principale if not os.path.exists(config_path): print(f"Fichier de configuration introuvable: {config_path}") sys.exit(1) with open(config_path, 'r', encoding='utf-8') as f: config = yaml.safe_load(f) # Vérifier les chemins essentiels required_paths = ['graphe_path', 'template_path', 'corpus_path'] for path in required_paths: if path not in config: print(f"Configuration incomplète: {path} manquant") sys.exit(1) # Convertir les chemins relatifs en chemins absolus for path in required_paths: config[path] = os.path.join(os.path.dirname(config_path), config[path]) # Charger les seuils if os.path.exists(thresholds_path): with open(thresholds_path, 'r', encoding='utf-8') as f: thresholds = yaml.safe_load(f) config['thresholds'] = thresholds.get('seuils', {}) else: print(f"Fichier de seuils introuvable: {thresholds_path}") # Valeurs par défaut si le fichier n'existe pas config['thresholds'] = { "IHH": {"vert": {"max": 15}, "orange": {"min": 15, "max": 25}, "rouge": {"min": 25}}, "ISG": {"vert": {"max": 40}, "orange": {"min": 40, "max": 60}, "rouge": {"min": 60}}, "ICS": {"vert": {"max": 0.30}, "orange": {"min": 0.30, "max": 0.60}, "rouge": {"min": 0.60}}, "IVC": {"vert": {"max": 5}, "orange": {"min": 5, "max": 15}, "rouge": {"min": 15}} } return config def determine_threshold_color(value, index_type, thresholds=None): """ Détermine la couleur du seuil en fonction du type d'indice et de sa valeur. Utilise les seuils de config.yaml si disponibles. """ # Valeurs par défaut si les seuils ne sont pas fournis default_thresholds = { "IHH": {"vert": {"max": 15}, "orange": {"min": 15, "max": 25}, "rouge": {"min": 25}}, "ISG": {"vert": {"max": 40}, "orange": {"min": 40, "max": 60}, "rouge": {"min": 60}}, "ICS": {"vert": {"max": 0.30}, "orange": {"min": 0.30, "max": 0.60}, "rouge": {"min": 0.60}}, "IVC": {"vert": {"max": 5}, "orange": {"min": 5, "max": 15}, "rouge": {"min": 15}} } # Utiliser les seuils fournis ou les valeurs par défaut thresholds = thresholds or default_thresholds # Récupérer les seuils pour cet indice if index_type in thresholds: index_thresholds = thresholds[index_type] # Déterminer la couleur if "vert" in index_thresholds and "max" in index_thresholds["vert"] and \ index_thresholds["vert"]["max"] is not None and value < index_thresholds["vert"]["max"]: suffix = get_suffix_for_index(index_type, "vert") return "Vert", suffix elif "orange" in index_thresholds and "min" in index_thresholds["orange"] and "max" in index_thresholds["orange"] and \ index_thresholds["orange"]["min"] is not None and index_thresholds["orange"]["max"] is not None and \ index_thresholds["orange"]["min"] <= value < index_thresholds["orange"]["max"]: suffix = get_suffix_for_index(index_type, "orange") return "Orange", suffix elif "rouge" in index_thresholds and "min" in index_thresholds["rouge"] and \ index_thresholds["rouge"]["min"] is not None and value >= index_thresholds["rouge"]["min"]: suffix = get_suffix_for_index(index_type, "rouge") return "Rouge", suffix # Fallback à l'ancienne méthode if index_type == "IHH": if value < 15: return "Vert", "Faible" elif value < 25: return "Orange", "Modérée" else: return "Rouge", "Élevée" elif index_type == "ISG": if value < 40: return "Vert", "Stable" elif value < 60: return "Orange", "Intermédiaire" else: return "Rouge", "Instable" elif index_type == "ICS": if value < 0.3: return "Vert", "Facile" elif value < 0.6: return "Orange", "Moyenne" else: return "Rouge", "Difficile" elif index_type == "IVC": if value < 5: return "Vert", "Faible" elif value < 15: return "Orange", "Modérée" else: return "Rouge", "Forte" return "Non déterminé", "" def get_suffix_for_index(index_type, color): """Retourne le suffixe approprié pour chaque indice et couleur.""" suffixes = { "IHH": {"vert": "Faible", "orange": "Modérée", "rouge": "Élevée"}, "ISG": {"vert": "Stable", "orange": "Intermédiaire", "rouge": "Instable"}, "ICS": {"vert": "Facile", "orange": "Moyenne", "rouge": "Difficile"}, "IVC": {"vert": "Faible", "orange": "Modérée", "rouge": "Forte"} } if index_type in suffixes and color in suffixes[index_type]: return suffixes[index_type][color] return "" def get_weight_for_color(color): """Retourne le poids correspondant à une couleur.""" weights = { "Vert": 1, "Orange": 2, "Rouge": 3 } return weights.get(color, 0) def old_find_corpus_file(pattern, base_path=None): """ Trouve un fichier dans le corpus qui correspond au motif, indépendamment des préfixes numériques. Args: pattern: Le motif de recherche (slug) base_path: Le chemin de base pour la recherche (optionnel) Returns: Le chemin relatif du fichier trouvé ou None si aucun fichier n'est trouvé """ if base_path: search_path = os.path.join(CORPUS_DIR, base_path) else: search_path = CORPUS_DIR print(f"Recherche: '{pattern}' dans {search_path}") # Vérifier d'abord si le chemin exact existe (pour les chemins complets spécifiés) exact_path = os.path.join(CORPUS_DIR, pattern) if not pattern.startswith(os.path.sep) else pattern if os.path.exists(exact_path) and exact_path.endswith('.md'): print(f"Trouvé chemin exact: {exact_path}") return os.path.relpath(exact_path, CORPUS_DIR) # Sinon, transformer le motif en expression régulière # Remplacer les tirets et les espaces par des caractères génériques regex_pattern = pattern.replace('-', '[-_ ]').replace(' ', '[-_ ]') # Chercher récursivement dans le répertoire for root, dirs, files in os.walk(search_path): for file in files: if file.endswith('.md'): full_path = os.path.join(root, file) # Ignorer les préfixes numériques file_without_prefix = re.sub(r'^\d+[-_ ]*', '', file) # Rechercher le motif dans le nom du fichier if re.search(regex_pattern, file_without_prefix, re.IGNORECASE): rel_path = os.path.relpath(full_path, CORPUS_DIR) print(f"Trouvé via regex: {rel_path}") return rel_path # Si toujours pas trouvé, essayer une recherche plus approfondie print(f"Fichier non trouvé: '{pattern}' dans {search_path}") # Essayer de lister les fichiers disponibles pour aider au débogage if base_path and os.path.exists(search_path): print(f"Fichiers disponibles dans {search_path}:") for file in os.listdir(search_path): if file.endswith('.md'): print(f" - {file}") return None def strip_prefix(name): """Supprime le préfixe numérique éventuel d'un nom de fichier ou de dossier.""" return re.sub(r'^\d+[-_ ]*', '', name).lower() def find_prefixed_directory(pattern, base_path=None): """ Recherche un sous-répertoire dont le nom (sans préfixe) correspond au pattern. Args: pattern: Nom du répertoire sans préfixe base_path: Répertoire de base où chercher Returns: Le chemin relatif du répertoire trouvé (avec préfixe) ou None """ if base_path: search_path = os.path.join(CORPUS_DIR, base_path) else: search_path = CORPUS_DIR if not os.path.exists(search_path): print(f"Chemin inexistant: {search_path}") return None for d in os.listdir(search_path): dir_path = os.path.join(search_path, d) if os.path.isdir(dir_path) and strip_prefix(d) == pattern.lower(): return os.path.relpath(dir_path, CORPUS_DIR) print(f"Aucun répertoire correspondant à: '{pattern}' trouvé dans {search_path}") return None def find_corpus_file(pattern, base_path=None): """ Recherche récursive dans le corpus d'un fichier en ignorant les préfixes numériques dans les dossiers et fichiers. Args: pattern: Chemin relatif type "sous-dossier/nom-fichier" base_path: Dossier de base à partir duquel chercher Returns: Chemin relatif du fichier trouvé ou None """ if base_path: search_path = os.path.join(CORPUS_DIR, base_path) else: search_path = CORPUS_DIR print(f"Recherche de: '{pattern}' dans {search_path}") if not os.path.exists(search_path): print(f"Chemin inexistant: {search_path}") return None if '/' not in pattern: # Recherche directe d'un fichier for file in os.listdir(search_path): if not file.endswith('.md'): continue if strip_prefix(os.path.splitext(file)[0]) == pattern.lower(): rel_path = os.path.relpath(os.path.join(search_path, file), CORPUS_DIR) print(f"Fichier trouvé: {rel_path}") return rel_path else: # Séparation du chemin en dossier/fichier first, rest = pattern.split('/', 1) matched_dir = find_prefixed_directory(first, base_path) if matched_dir: return find_corpus_file(rest, matched_dir) print(f"Aucun fichier correspondant à: '{pattern}' trouvé.") return None def read_corpus_file(file_path, remove_first_title=False, shift_titles=0): """ Lit un fichier du corpus et applique les transformations demandées. Args: file_path: Chemin relatif du fichier dans le corpus remove_first_title: Si True, supprime la première ligne de titre shift_titles: Nombre de niveaux à ajouter aux titres Returns: Le contenu du fichier avec les transformations appliquées """ full_path = os.path.join(CORPUS_DIR, file_path) if not os.path.exists(full_path): print(f"Fichier non trouvé: {full_path}") return f"Fichier non trouvé: {file_path}" # print(f"Lecture du fichier: {full_path}") with open(full_path, 'r', encoding='utf-8') as f: lines = f.readlines() # Supprimer la première ligne si c'est un titre et si demandé if remove_first_title and lines and lines[0].startswith('#'): # print(f"Suppression du titre: {lines[0].strip()}") lines = lines[1:] # Décaler les niveaux de titre si demandé if shift_titles > 0: for i in range(len(lines)): if lines[i].startswith('#'): lines[i] = '#' * shift_titles + lines[i] # Nettoyer les retours à la ligne superflus content = ''.join(lines) # Remplacer les triples retours à la ligne par des doubles content = re.sub(r'\n\n\n+', '\n\n', content) # Supprimer les retours à la ligne en fin de contenu content = content.rstrip('\n') + '\n' return content def parse_graphs(config): """ Charge et analyse les graphes DOT (analyse et référence). """ # Charger le graphe à analyser graphe_path = config['graphe_path'] if not os.path.exists(graphe_path): print(f"Fichier de graphe à analyser introuvable: {graphe_path}") sys.exit(1) # Charger le graphe de référence reference_path = REFERENCE_GRAPH_PATH if not os.path.exists(reference_path): print(f"Fichier de graphe de référence introuvable: {reference_path}") sys.exit(1) try: # Charger les graphes avec NetworkX graph = read_dot(graphe_path) ref_graph = read_dot(reference_path) # Convertir les attributs en types appropriés pour les deux graphes for g in [graph, ref_graph]: for node, attrs in g.nodes(data=True): for key, value in list(attrs.items()): # Convertir les valeurs numériques if key in ['niveau', 'ihh_acteurs', 'ihh_pays', 'isg', 'ivc']: try: if key in ['isg', 'ivc', 'ihh_acteurs', 'ihh_pays', 'niveau']: attrs[key] = int(value.strip('"')) else: attrs[key] = float(value.strip('"')) except (ValueError, TypeError): # Garder la valeur originale si la conversion échoue pass elif key == 'label': # Nettoyer les guillemets des étiquettes attrs[key] = value.strip('"') # Convertir les attributs des arêtes for u, v, attrs in g.edges(data=True): for key, value in list(attrs.items()): if key in ['ics', 'cout', 'delai', 'technique']: try: attrs[key] = float(value.strip('"')) except (ValueError, TypeError): pass elif key == 'label' and '%' in value: # Extraire le pourcentage try: percentage = value.strip('"').replace('%', '') attrs['percentage'] = float(percentage) except (ValueError, TypeError): pass return graph, ref_graph except Exception as e: print(f"Erreur lors de l'analyse des graphes: {str(e)}") sys.exit(1) def extract_data_from_graph(graph, ref_graph): """ Extrait toutes les données pertinentes des graphes DOT. """ data = { "products": {}, # Produits finaux (N0) "components": {}, # Composants (N1) "minerals": {}, # Minerais (N2) "operations": {}, # Opérations (N10) "countries": {}, # Pays (N11) "geo_countries": {}, # Pays géographiques (N99) "actors": {} # Acteurs (N12) } # Extraire tous les pays géographiques du graphe de référence for node, attrs in ref_graph.nodes(data=True): if attrs.get('niveau') == 99: country_name = attrs.get('label', node) isg_value = attrs.get('isg', 0) data["geo_countries"][country_name] = { "id": node, "isg": isg_value } # Extraire les nœuds du graphe à analyser for node, attrs in graph.nodes(data=True): level = attrs.get('niveau', -1) label = attrs.get('label', node) if level == 0: # Produit final data["products"][node] = { "label": label, "components": [], "assembly": None } elif level == 1: # Composant data["components"][node] = { "label": label, "minerals": [], "manufacturing": None } elif level == 2: # Minerai data["minerals"][node] = { "label": label, "ivc": attrs.get('ivc', 0), "extraction": None, "treatment": None, "ics_values": {} } elif level == 10: # Opération op_type = label.lower() data["operations"][node] = { "label": label, "type": op_type, "ihh_acteurs": attrs.get('ihh_acteurs', 0), "ihh_pays": attrs.get('ihh_pays', 0), "countries": {} } elif level == 11: # Pays data["countries"][node] = { "label": label, "actors": {}, "geo_country": None, "market_share": 0 } elif level == 12: # Acteur data["actors"][node] = { "label": label, "country": None, "market_share": 0 } # Extraire les relations et attributs des arêtes for source, target, edge_attrs in graph.edges(data=True): if source not in graph.nodes or target not in graph.nodes: continue source_level = graph.nodes[source].get('niveau', -1) target_level = graph.nodes[target].get('niveau', -1) # Extraire part de marché market_share = 0 if 'percentage' in edge_attrs: market_share = edge_attrs['percentage'] elif 'label' in edge_attrs and '%' in edge_attrs['label']: try: market_share = float(edge_attrs['label'].strip('"').replace('%', '')) except (ValueError, TypeError): pass # Relations produit → composant if source_level == 0 and target_level == 1: if target not in data["products"][source]["components"]: data["products"][source]["components"].append(target) # Relations produit → opération (assemblage) elif source_level == 0 and target_level == 10: if graph.nodes[target].get('label', '').lower() == 'assemblage': data["products"][source]["assembly"] = target # Relations composant → minerai avec ICS elif source_level == 1 and target_level == 2: if target not in data["components"][source]["minerals"]: data["components"][source]["minerals"].append(target) # Stocker l'ICS s'il est présent if 'ics' in edge_attrs: ics_value = edge_attrs['ics'] data["minerals"][target]["ics_values"][source] = ics_value # Relations composant → opération (fabrication) elif source_level == 1 and target_level == 10: if graph.nodes[target].get('label', '').lower() == 'fabrication': data["components"][source]["manufacturing"] = target # Relations minerai → opération (extraction/traitement) elif source_level == 2 and target_level == 10: op_label = graph.nodes[target].get('label', '').lower() if 'extraction' in op_label: data["minerals"][source]["extraction"] = target elif 'traitement' in op_label: data["minerals"][source]["treatment"] = target # Relations opération → pays avec part de marché elif source_level == 10 and target_level == 11: data["operations"][source]["countries"][target] = market_share data["countries"][target]["market_share"] = market_share # Relations pays → acteur avec part de marché elif source_level == 11 and target_level == 12: data["countries"][source]["actors"][target] = market_share data["actors"][target]["market_share"] = market_share data["actors"][target]["country"] = source # Relations pays → pays géographique elif source_level == 11 and target_level == 99: country_name = graph.nodes[target].get('label', '') data["countries"][source]["geo_country"] = country_name return data def calculate_vulnerabilities(data, config): """ Calcule les vulnérabilités combinées pour toutes les opérations et minerais. """ thresholds = config.get('thresholds', {}) results = { "ihh_isg_combined": {}, # Pour chaque opération "ics_ivc_combined": {}, # Pour chaque minerai "chains": [] # Pour stocker tous les chemins possibles } # 1. Calculer ISG_combiné pour chaque opération for op_id, operation in data["operations"].items(): isg_weighted_sum = 0 total_share = 0 # Parcourir chaque pays impliqué dans l'opération for country_id, share in operation["countries"].items(): country = data["countries"][country_id] geo_country = country.get("geo_country") if geo_country and geo_country in data["geo_countries"]: isg_value = data["geo_countries"][geo_country]["isg"] isg_weighted_sum += isg_value * share total_share += share # Calculer la moyenne pondérée isg_combined = 0 if total_share > 0: isg_combined = isg_weighted_sum / total_share # Déterminer couleurs et poids ihh_value = operation["ihh_pays"] ihh_color, ihh_suffix = determine_threshold_color(ihh_value, "IHH", thresholds) isg_color, isg_suffix = determine_threshold_color(isg_combined, "ISG", thresholds) # Calculer poids combiné ihh_weight = get_weight_for_color(ihh_color) isg_weight = get_weight_for_color(isg_color) combined_weight = ihh_weight * isg_weight # Déterminer vulnérabilité combinée if combined_weight in [6, 9]: vulnerability = "ÉLEVÉE à CRITIQUE" elif combined_weight in [3, 4]: vulnerability = "MOYENNE" else: # 1, 2 vulnerability = "FAIBLE" # Stocker résultats results["ihh_isg_combined"][op_id] = { "ihh_value": ihh_value, "ihh_color": ihh_color, "ihh_suffix": ihh_suffix, "isg_combined": isg_combined, "isg_color": isg_color, "isg_suffix": isg_suffix, "combined_weight": combined_weight, "vulnerability": vulnerability } # 2. Calculer ICS_moyen pour chaque minerai for mineral_id, mineral in data["minerals"].items(): ics_values = list(mineral["ics_values"].values()) ics_average = 0 if ics_values: ics_average = sum(ics_values) / len(ics_values) ivc_value = mineral.get("ivc", 0) # Déterminer couleurs et poids ics_color, ics_suffix = determine_threshold_color(ics_average, "ICS", thresholds) ivc_color, ivc_suffix = determine_threshold_color(ivc_value, "IVC", thresholds) # Calculer poids combiné ics_weight = get_weight_for_color(ics_color) ivc_weight = get_weight_for_color(ivc_color) combined_weight = ics_weight * ivc_weight # Déterminer vulnérabilité combinée if combined_weight in [6, 9]: vulnerability = "ÉLEVÉE à CRITIQUE" elif combined_weight in [3, 4]: vulnerability = "MOYENNE" else: # 1, 2 vulnerability = "FAIBLE" # Stocker résultats results["ics_ivc_combined"][mineral_id] = { "ics_average": ics_average, "ics_color": ics_color, "ics_suffix": ics_suffix, "ivc_value": ivc_value, "ivc_color": ivc_color, "ivc_suffix": ivc_suffix, "combined_weight": combined_weight, "vulnerability": vulnerability } # 3. Identifier tous les chemins et leurs vulnérabilités for product_id, product in data["products"].items(): for component_id in product["components"]: component = data["components"][component_id] for mineral_id in component["minerals"]: mineral = data["minerals"][mineral_id] # Collecter toutes les vulnérabilités dans ce chemin path_vulnerabilities = [] # Assemblage (si présent) assembly_id = product["assembly"] if assembly_id and assembly_id in results["ihh_isg_combined"]: path_vulnerabilities.append({ "type": "assemblage", "vulnerability": results["ihh_isg_combined"][assembly_id]["vulnerability"], "operation_id": assembly_id }) # Fabrication (si présent) manufacturing_id = component["manufacturing"] if manufacturing_id and manufacturing_id in results["ihh_isg_combined"]: path_vulnerabilities.append({ "type": "fabrication", "vulnerability": results["ihh_isg_combined"][manufacturing_id]["vulnerability"], "operation_id": manufacturing_id }) # Minerai (ICS+IVC) if mineral_id in results["ics_ivc_combined"]: path_vulnerabilities.append({ "type": "minerai", "vulnerability": results["ics_ivc_combined"][mineral_id]["vulnerability"], "mineral_id": mineral_id }) # Extraction (si présent) extraction_id = mineral["extraction"] if extraction_id and extraction_id in results["ihh_isg_combined"]: path_vulnerabilities.append({ "type": "extraction", "vulnerability": results["ihh_isg_combined"][extraction_id]["vulnerability"], "operation_id": extraction_id }) # Traitement (si présent) treatment_id = mineral["treatment"] if treatment_id and treatment_id in results["ihh_isg_combined"]: path_vulnerabilities.append({ "type": "traitement", "vulnerability": results["ihh_isg_combined"][treatment_id]["vulnerability"], "operation_id": treatment_id }) # Classifier le chemin path_info = { "product": product_id, "component": component_id, "mineral": mineral_id, "vulnerabilities": path_vulnerabilities } # Déterminer le niveau de risque du chemin critical_count = path_vulnerabilities.count({"vulnerability": "ÉLEVÉE à CRITIQUE"}) medium_count = path_vulnerabilities.count({"vulnerability": "MOYENNE"}) if any(v["vulnerability"] == "ÉLEVÉE à CRITIQUE" for v in path_vulnerabilities): path_info["risk_level"] = "critique" elif medium_count >= 3: path_info["risk_level"] = "majeur" elif any(v["vulnerability"] == "MOYENNE" for v in path_vulnerabilities): path_info["risk_level"] = "moyen" else: path_info["risk_level"] = "faible" results["chains"].append(path_info) return results def generate_introduction_section(data): """ Génère la section d'introduction du rapport. """ products = [p["label"] for p in data["products"].values()] components = [c["label"] for c in data["components"].values()] minerals = [m["label"] for m in data["minerals"].values()] template = [] template.append("## Introduction\n") template.append("Ce rapport analyse les vulnérabilités de la chaîne de fabrication du numérique pour :\n") template.append("* les produits finaux : " + ", ".join(products)) template.append("* les composants : " + ", ".join(components)) template.append("* les minerais : " + ", ".join(minerals) + "\n") return "\n".join(template) def generate_methodology_section(): """ Génère la section méthodologie du rapport. """ template = [] template.append("## Méthodologie d'analyse des risques\n") template.append("### Indices et seuils\n") template.append("La méthode d'évaluation intègre 4 indices et leurs combinaisons pour identifier les chemins critiques.\n") # IHH template.append("#### IHH (Herfindahl-Hirschmann) : concentration géographiques ou industrielle d'une opération\n") # Essayer d'abord avec le chemin exact ihh_context_file = "Criticités/Fiche technique IHH/00-contexte-et-objectif.md" if os.path.exists(os.path.join(CORPUS_DIR, ihh_context_file)): template.append(read_corpus_file(ihh_context_file, remove_first_title=True)) else: # Fallback à la recherche par motif ihh_context_file = find_corpus_file("contexte-et-objectif", "Criticités/Fiche technique IHH") if ihh_context_file: template.append(read_corpus_file(ihh_context_file, remove_first_title=True)) # Essayer d'abord avec le chemin exact ihh_calc_file = "Criticités/Fiche technique IHH/01-mode-de-calcul/_intro.md" if os.path.exists(os.path.join(CORPUS_DIR, ihh_calc_file)): template.append(read_corpus_file(ihh_calc_file, remove_first_title=True)) else: # Fallback à la recherche par motif ihh_calc_file = find_corpus_file("mode-de-calcul/_intro", "Criticités/Fiche technique IHH") if ihh_calc_file: template.append(read_corpus_file(ihh_calc_file, remove_first_title=True)) template.append(" * Seuils : <15 = Vert (Faible), 15-25 = Orange (Modérée), >25 = Rouge (Élevée)\n") # ISG template.append("#### ISG (Stabilité Géopolitique) : stabilité des pays\n") # Essayer d'abord avec le chemin exact isg_context_file = "Criticités/Fiche technique ISG/00-contexte-et-objectif.md" if os.path.exists(os.path.join(CORPUS_DIR, isg_context_file)): template.append(read_corpus_file(isg_context_file, remove_first_title=True)) else: # Fallback à la recherche par motif isg_context_file = find_corpus_file("contexte-et-objectif", "Criticités/Fiche technique ISG") if isg_context_file: template.append(read_corpus_file(isg_context_file, remove_first_title=True)) # Essayer d'abord avec le chemin exact isg_calc_file = "Criticités/Fiche technique ISG/01-mode-de-calcul/_intro.md" if os.path.exists(os.path.join(CORPUS_DIR, isg_calc_file)): template.append(read_corpus_file(isg_calc_file, remove_first_title=True)) else: # Fallback à la recherche par motif isg_calc_file = find_corpus_file("mode-de-calcul/_intro", "Criticités/Fiche technique ISG") if isg_calc_file: template.append(read_corpus_file(isg_calc_file, remove_first_title=True)) template.append(" * Seuils : <40 = Vert (Stable), 40-60 = Orange, >60 = Rouge (Instable)\n") # ICS template.append("#### ICS (Criticité de Substituabilité) : capacité à remplacer / substituer un élément\n") # Essayer d'abord avec le chemin exact ics_context_file = "Criticités/Fiche technique ICS/00-contexte-et-objectif.md" if os.path.exists(os.path.join(CORPUS_DIR, ics_context_file)): template.append(read_corpus_file(ics_context_file, remove_first_title=True)) else: # Fallback à la recherche par motif ics_context_file = find_corpus_file("contexte-et-objectif", "Criticités/Fiche technique ICS") if ics_context_file: template.append(read_corpus_file(ics_context_file, remove_first_title=True)) # Essayer d'abord avec le chemin exact ics_calc_file = "Criticités/Fiche technique ICS/01-mode-de-calcul/_intro.md" if os.path.exists(os.path.join(CORPUS_DIR, ics_calc_file)): template.append(read_corpus_file(ics_calc_file, remove_first_title=True)) else: # Fallback à la recherche par motif ics_calc_file = find_corpus_file("mode-de-calcul/_intro", "Criticités/Fiche technique ICS") if ics_calc_file: template.append(read_corpus_file(ics_calc_file, remove_first_title=True)) template.append(" * Seuils : <0.3 = Vert (Facile), 0.3-0.6 = Orange (Moyenne), >0.6 = Rouge (Difficile)\n") # IVC template.append("#### IVC (Vulnérabilité de Concurrence) : pression concurrentielle avec d'autres secteurs\n") # Essayer d'abord avec le chemin exact ivc_context_file = "Criticités/Fiche technique IVC/00-contexte-et-objectif.md" if os.path.exists(os.path.join(CORPUS_DIR, ivc_context_file)): template.append(read_corpus_file(ivc_context_file, remove_first_title=True)) else: # Fallback à la recherche par motif ivc_context_file = find_corpus_file("contexte-et-objectif", "Criticités/Fiche technique IVC") if ivc_context_file: template.append(read_corpus_file(ivc_context_file, remove_first_title=True)) # Essayer d'abord avec le chemin exact ivc_calc_file = "Criticités/Fiche technique IVC/01-mode-de-calcul/_intro.md" if os.path.exists(os.path.join(CORPUS_DIR, ivc_calc_file)): template.append(read_corpus_file(ivc_calc_file, remove_first_title=True)) else: # Fallback à la recherche par motif ivc_calc_file = find_corpus_file("mode-de-calcul/_intro", "Criticités/Fiche technique IVC") if ivc_calc_file: template.append(read_corpus_file(ivc_calc_file, remove_first_title=True)) template.append(" * Seuils : <5 = Vert (Faible), 5-15 = Orange (Modérée), >15 = Rouge (Forte)\n") # Combinaison des indices template.append("### Combinaison des indices\n") # IHH et ISG template.append("**IHH et ISG**\n") template.append("Ces deux indices s'appliquent à toutes les opérations et se combinent dans l'évaluation du risque (niveau d'impact et probabilité de survenance) :\n") template.append("* l'IHH donne le niveau d'impact => une forte concentration implique un fort impact si le risque est avéré") template.append("* l'ISG donne la probabilité de survenance => plus les pays sont instables (et donc plus l'ISG est élevé) et plus la survenance du risque est élevée\n") template.append("Pour évaluer le risque pour une opération, les ISG des pays sont pondérés par les parts de marché respectives pour donner un ISG combiné dont le calcul est :") template.append("ISG_combiné = (Somme des ISG des pays multipliée par leur part de marché) / Sommes de leur part de marché\n") template.append("On établit alors une matrice (Vert = 1, Orange = 2, Rouge = 3) et en faisant le produit des poids de l'ISG combiné et de l'IHH\n") template.append("| ISG combiné / IHH | Vert | Orange | Rouge |") template.append("| :-- | :-- | :-- | :-- |") template.append("| Vert | 1 | 2 | 3 |") template.append("| Orange | 2 | 4 | 6 |") template.append("| Rouge | 3 | 6 | 9 |\n") template.append("Les vulnérabilités se classent en trois niveaux pour chaque opération :\n") template.append("* Vulnérabilité combinée élevée à critique : poids 6 et 9") template.append("* Vulnérabilité combinée moyenne : poids 3 et 4") template.append("* Vulnérabilité combinée faible : poids 1 et 2\n") # ICS et IVC template.append("**ICS et IVC**\n") template.append("Ces deux indices se combinent dans l'évaluation du risque pour un minerai :\n") template.append("* l'ICS donne le niveau d'impact => une faible substituabilité (et donc un ICS élevé) implique un fort impact si le risque est avéré ; l'ICS est associé à la relation entre un composant et un minerai") template.append("* l'IVC donne la probabilité de l'impact => une forte concurrence intersectorielle (IVC élevé) implique une plus forte probabilité de survenance\n") template.append("Par simplification, on intègre un ICS moyen d'un minerai comme étant la moyenne des ICS pour chacun des composants dans lesquels il intervient.\n") template.append("On établit alors une matrice (Vert = 1, Orange = 2, Rouge = 3) et en faisant le produit des poids de l'ICS moyen et de l'IVC.\n") template.append("| ICS_moyen / IVC | Vert | Orange | Rouge |") template.append("| :-- | :-- | :-- | :-- |") template.append("| Vert | 1 | 2 | 3 |") template.append("| Orange | 2 | 4 | 6 |") template.append("| Rouge | 3 | 6 | 9 |\n") template.append("Les vulnérabilités se classent en trois niveaux pour chaque minerai :\n") template.append("* Vulnérabilité combinée élevée à critique : poids 6 et 9") template.append("* Vulnérabilité combinée moyenne : poids 3 et 4") template.append("* Vulnérabilité combinée faible : poids 1 et 2\n") return "\n".join(template) def generate_operations_section(data, results, config): """ Génère la section détaillant les opérations (assemblage, fabrication, extraction, traitement). """ template = [] template.append("## Détails des opérations\n") # 1. Traiter les produits finaux (assemblage) for product_id, product in data["products"].items(): if product["assembly"]: template.append(f"### {product['label']} et Assemblage\n") # Récupérer la présentation synthétique product_slug = product['label'].lower().replace(' ', '-') presentation_file = find_corpus_file("présentation-synthétique", f"Assemblage/Fiche assemblage {product_slug}") if presentation_file: template.append(read_corpus_file(presentation_file, remove_first_title=True)) template.append("") # Récupérer les principaux assembleurs assembleurs_file = find_corpus_file("principaux-assembleurs", f"Assemblage/Fiche assemblage {product_slug}") if assembleurs_file: template.append(read_corpus_file(assembleurs_file, shift_titles=2)) template.append("") # ISG des pays impliqués assembly_id = product["assembly"] operation = data["operations"][assembly_id] template.append("##### ISG des pays impliqués\n") template.append("| Pays | Part de marché | ISG | Criticité |") template.append("| :-- | :-- | :-- | :-- |") isg_weighted_sum = 0 total_share = 0 for country_id, share in operation["countries"].items(): country = data["countries"][country_id] geo_country = country.get("geo_country") if geo_country and geo_country in data["geo_countries"]: isg_value = data["geo_countries"][geo_country]["isg"] color, suffix = determine_threshold_color(isg_value, "ISG", config.get('thresholds')) template.append(f"| {country['label']} | {share}% | {isg_value} | {color} ({suffix}) |") isg_weighted_sum += isg_value * share total_share += share # Calculer ISG combiné if total_share > 0: isg_combined = isg_weighted_sum / total_share color, suffix = determine_threshold_color(isg_combined, "ISG", config.get('thresholds')) template.append(f"\n**ISG combiné: {isg_combined:.0f} - {color} ({suffix})**") # IHH ihh_file = find_corpus_file("indice-de-herfindahl-hirschmann", f"Assemblage/Fiche assemblage {product_slug}/matrice-des-risques") if ihh_file: template.append(read_corpus_file(ihh_file, shift_titles=1)) template.append("\n") # Vulnérabilité combinée if assembly_id in results["ihh_isg_combined"]: combined = results["ihh_isg_combined"][assembly_id] template.append(f"#### Vulnérabilité combinée IHH-ISG\n") template.append(f"* IHH: {combined['ihh_value']} - {combined['ihh_color']} ({combined['ihh_suffix']})") template.append(f"* ISG combiné: {combined['isg_combined']:.0f} - {combined['isg_color']} ({combined['isg_suffix']})") template.append(f"* Poids combiné: {combined['combined_weight']}") template.append(f"* Niveau de vulnérabilité: **{combined['vulnerability']}**\n") # 2. Traiter les composants (fabrication) for component_id, component in data["components"].items(): if component["manufacturing"]: template.append(f"### {component['label']} et Fabrication\n") # Récupérer la présentation synthétique component_slug = component['label'].lower().replace(' ', '-') presentation_file = find_corpus_file("présentation-synthétique", f"Fabrication/Fiche fabrication {component_slug}") if presentation_file: template.append(read_corpus_file(presentation_file, remove_first_title=True)) template.append("\n") # Récupérer les principaux fabricants fabricants_file = find_corpus_file("principaux-fabricants", f"Fabrication/Fiche fabrication {component_slug}") if fabricants_file: template.append(read_corpus_file(fabricants_file, shift_titles=2)) template.append("\n") # ISG des pays impliqués manufacturing_id = component["manufacturing"] operation = data["operations"][manufacturing_id] template.append("#### ISG des pays impliqués\n") template.append("| Pays | Part de marché | ISG | Criticité |\n") template.append("| :-- | :-- | :-- | :-- |\n") isg_weighted_sum = 0 total_share = 0 for country_id, share in operation["countries"].items(): country = data["countries"][country_id] geo_country = country.get("geo_country") if geo_country and geo_country in data["geo_countries"]: isg_value = data["geo_countries"][geo_country]["isg"] color, suffix = determine_threshold_color(isg_value, "ISG", config.get('thresholds')) template.append(f"| {country['label']} | {share}% | {isg_value} | {color} ({suffix}) |") isg_weighted_sum += isg_value * share total_share += share # Calculer ISG combiné if total_share > 0: isg_combined = isg_weighted_sum / total_share color, suffix = determine_threshold_color(isg_combined, "ISG", config.get('thresholds')) template.append(f"\n**ISG combiné: {isg_combined:.0f} - {color} ({suffix})**\n") # IHH ihh_file = find_corpus_file("indice-de-herfindahl-hirschmann", f"Fabrication/Fiche fabrication {component_slug}/matrice-des-risques") if ihh_file: template.append(read_corpus_file(ihh_file, shift_titles=1)) template.append("\n") # Vulnérabilité combinée if manufacturing_id in results["ihh_isg_combined"]: combined = results["ihh_isg_combined"][manufacturing_id] template.append(f"#### Vulnérabilité combinée IHH-ISG\n") template.append(f"* IHH: {combined['ihh_value']} - {combined['ihh_color']} ({combined['ihh_suffix']})") template.append(f"* ISG combiné: {combined['isg_combined']:.0f} - {combined['isg_color']} ({combined['isg_suffix']})") template.append(f"* Poids combiné: {combined['combined_weight']}") template.append(f"* Niveau de vulnérabilité: **{combined['vulnerability']}**\n") # 3. Traiter les minerais (détaillés dans une section séparée) return "\n".join(template) def generate_minerals_section(data, results, config): """ Génère la section détaillant les minerais et leurs opérations d'extraction et traitement. """ template = [] template.append("## Détails des minerais\n") for mineral_id, mineral in data["minerals"].items(): template.append(f"### {mineral['label']}\n") # Récupérer la présentation synthétique mineral_slug = mineral['label'].lower().replace(' ', '-') presentation_file = find_corpus_file("présentation-synthétique", f"Minerai/Fiche minerai {mineral_slug}") if presentation_file: template.append(read_corpus_file(presentation_file, remove_first_title=True)) template.append("\n") # ICS template.append("#### ICS\n") ics_intro_file = find_corpus_file("risque-de-substituabilité/_intro", f"Minerai/Fiche minerai {mineral_slug}") if ics_intro_file: template.append(read_corpus_file(ics_intro_file, remove_first_title=True)) template.append("\n") # Calcul de l'ICS moyen ics_values = list(mineral["ics_values"].values()) if ics_values: ics_average = sum(ics_values) / len(ics_values) color, suffix = determine_threshold_color(ics_average, "ICS", config.get('thresholds')) template.append("##### Valeurs d'ICS par composant\n") template.append("| Composant | ICS | Criticité |") template.append("| :-- | :-- | :-- |") for comp_id, ics_value in mineral["ics_values"].items(): comp_name = data["components"][comp_id]["label"] comp_color, comp_suffix = determine_threshold_color(ics_value, "ICS", config.get('thresholds')) template.append(f"| {comp_name} | {ics_value:.2f} | {comp_color} ({comp_suffix}) |") template.append(f"\n**ICS moyen : {ics_average:.2f} - {color} ({suffix})**\n") # IVC template.append("#### IVC\n") # Valeur IVC ivc_value = mineral.get("ivc", 0) color, suffix = determine_threshold_color(ivc_value, "IVC", config.get('thresholds')) template.append(f"**IVC: {ivc_value} - {color} ({suffix})**\n") # Récupérer toutes les sections de vulnérabilité de concurrence ivc_sections = [] ivc_dir = find_prefixed_directory("vulnérabilité-de-concurrence", f"Minerai/Fiche minerai {mineral_slug}") corpus_path = os.path.join(CORPUS_DIR, ivc_dir) if os.path.exists(os.path.join(CORPUS_DIR, ivc_dir)) else None if corpus_path: for file in sorted(os.listdir(corpus_path)): if file.endswith('.md') and "_intro.md" not in file and "sources" not in file: ivc_sections.append(os.path.join(ivc_dir, file)) # Inclure chaque section IVC for section_file in ivc_sections: content = read_corpus_file(section_file, remove_first_title=False) # Nettoyer les balises des fichiers IVC content = re.sub(r'', '', content) content = re.sub(r'```.*?```', '', content, flags=re.DOTALL) # Mettre le titre en italique s'il commence par un # (format Markdown pour titre) if content and '\n' in content: first_line, rest = content.split('\n', 1) if first_line.strip().startswith('#'): # Extraire le texte du titre sans les # et les espaces title_text = first_line.strip().lstrip('#').strip() content = f"*{title_text}*\n{rest.strip()}" # Ne pas ajouter de contenu vide if content.strip(): template.append(content.strip()) # ICS et IVC combinés if mineral_id in results["ics_ivc_combined"]: combined = results["ics_ivc_combined"][mineral_id] template.append(f"\n#### Vulnérabilité combinée ICS-IVC\n") template.append(f"* ICS moyen: {combined['ics_average']:.2f} - {combined['ics_color']} ({combined['ics_suffix']})") template.append(f"* IVC: {combined['ivc_value']} - {combined['ivc_color']} ({combined['ivc_suffix']})") template.append(f"* Poids combiné: {combined['combined_weight']}") template.append(f"* Niveau de vulnérabilité: **{combined['vulnerability']}**\n") # Extraction if mineral["extraction"]: template.append("#### Extraction\n") # Récupérer les principaux producteurs producers_file = find_corpus_file("principaux-producteurs-extraction", f"Minerai/Fiche minerai {mineral_slug}") if producers_file: template.append(read_corpus_file(producers_file, remove_first_title=True)) template.append("\n") # ISG des pays impliqués extraction_id = mineral["extraction"] operation = data["operations"][extraction_id] template.append("##### ISG des pays impliqués\n") template.append("| Pays | Part de marché | ISG | Criticité |") template.append("| :-- | :-- | :-- | :-- |") isg_weighted_sum = 0 total_share = 0 for country_id, share in operation["countries"].items(): country = data["countries"][country_id] geo_country = country.get("geo_country") if geo_country and geo_country in data["geo_countries"]: isg_value = data["geo_countries"][geo_country]["isg"] color, suffix = determine_threshold_color(isg_value, "ISG", config.get('thresholds')) template.append(f"| {country['label']} | {share}% | {isg_value} | {color} ({suffix}) |") isg_weighted_sum += isg_value * share total_share += share # Calculer ISG combiné if total_share > 0: isg_combined = isg_weighted_sum / total_share color, suffix = determine_threshold_color(isg_combined, "ISG", config.get('thresholds')) template.append(f"\n**ISG combiné: {isg_combined:.0f} - {color} ({suffix})**\n") # IHH extraction ihh_file = find_corpus_file("matrice-des-risques/indice-de-herfindahl-hirschmann-extraction", f"Minerai/Fiche minerai {mineral_slug}") if ihh_file: template.append(read_corpus_file(ihh_file, shift_titles=1)) template.append("\n") # Vulnérabilité combinée if extraction_id in results["ihh_isg_combined"]: combined = results["ihh_isg_combined"][extraction_id] template.append(f"##### Vulnérabilité combinée IHH-ISG pour l'extraction\n") template.append(f"* IHH: {combined['ihh_value']} - {combined['ihh_color']} ({combined['ihh_suffix']})") template.append(f"* ISG combiné: {combined['isg_combined']:.0f} - {combined['isg_color']} ({combined['isg_suffix']})") template.append(f"* Poids combiné: {combined['combined_weight']}") template.append(f"* Niveau de vulnérabilité: **{combined['vulnerability']}**\n") # Traitement if mineral["treatment"]: template.append("#### Traitement\n") # Récupérer les principaux producteurs producers_file = find_corpus_file("principaux-producteurs-traitement", f"Minerai/Fiche minerai {mineral_slug}") if producers_file: template.append(read_corpus_file(producers_file, remove_first_title=True)) template.append("\n") # ISG des pays impliqués treatment_id = mineral["treatment"] operation = data["operations"][treatment_id] template.append("##### ISG des pays impliqués\n") template.append("| Pays | Part de marché | ISG | Criticité |") template.append("| :-- | :-- | :-- | :-- |") isg_weighted_sum = 0 total_share = 0 for country_id, share in operation["countries"].items(): country = data["countries"][country_id] geo_country = country.get("geo_country") if geo_country and geo_country in data["geo_countries"]: isg_value = data["geo_countries"][geo_country]["isg"] color, suffix = determine_threshold_color(isg_value, "ISG", config.get('thresholds')) template.append(f"| {country['label']} | {share}% | {isg_value} | {color} ({suffix}) |") isg_weighted_sum += isg_value * share total_share += share # Calculer ISG combiné if total_share > 0: isg_combined = isg_weighted_sum / total_share color, suffix = determine_threshold_color(isg_combined, "ISG", config.get('thresholds')) template.append(f"\n**ISG combiné: {isg_combined:.0f} - {color} ({suffix})**\n") # IHH traitement ihh_file = find_corpus_file("matrice-des-risques/indice-de-herfindahl-hirschmann-traitement", f"Minerai/Fiche minerai {mineral_slug}") if ihh_file: template.append(read_corpus_file(ihh_file, shift_titles=1)) template.append("\n") # Vulnérabilité combinée if treatment_id in results["ihh_isg_combined"]: combined = results["ihh_isg_combined"][treatment_id] template.append(f"##### Vulnérabilité combinée IHH-ISG pour le traitement\n") template.append(f"* IHH: {combined['ihh_value']} - {combined['ihh_color']} ({combined['ihh_suffix']})") template.append(f"* ISG combiné: {combined['isg_combined']:.0f} - {combined['isg_color']} ({combined['isg_suffix']})") template.append(f"* Poids combiné: {combined['combined_weight']}") template.append(f"* Niveau de vulnérabilité: **{combined['vulnerability']}**\n") return "\n".join(template) def generate_critical_paths_section(data, results): """ Génère la section des chemins critiques. """ template = [] template.append("## Chemins critiques\n") # Récupérer les chaînes par niveau de risque critical_chains = [] major_chains = [] medium_chains = [] for chain in results["chains"]: if chain["risk_level"] == "critique": critical_chains.append(chain) elif chain["risk_level"] == "majeur": major_chains.append(chain) elif chain["risk_level"] == "moyen": medium_chains.append(chain) # 1. Chaînes critiques template.append("### Chaînes avec risque critique\n") template.append("*Ces chaînes comprennent au moins une vulnérabilité combinée élevée à critique*\n") if critical_chains: for chain in critical_chains: product_name = data["products"][chain["product"]]["label"] component_name = data["components"][chain["component"]]["label"] mineral_name = data["minerals"][chain["mineral"]]["label"] template.append(f"#### {product_name} → {component_name} → {mineral_name}\n") # Vulnérabilités template.append("**Vulnérabilités identifiées:**\n") for vuln in chain["vulnerabilities"]: vuln_type = vuln["type"].capitalize() vuln_level = vuln["vulnerability"] if vuln_type == "Minerai": mineral_id = vuln["mineral_id"] template.append(f"* {vuln_type} ({mineral_name}): {vuln_level}") if mineral_id in results["ics_ivc_combined"]: combined = results["ics_ivc_combined"][mineral_id] template.append(f" * ICS moyen: {combined['ics_average']:.2f} - {combined['ics_color']}") template.append(f" * IVC: {combined['ivc_value']} - {combined['ivc_color']}") else: op_id = vuln["operation_id"] op_label = data["operations"][op_id]["label"] template.append(f"* {vuln_type} ({op_label}): {vuln_level}") if op_id in results["ihh_isg_combined"]: combined = results["ihh_isg_combined"][op_id] template.append(f" * IHH: {combined['ihh_value']} - {combined['ihh_color']}") template.append(f" * ISG combiné: {combined['isg_combined']:.0f} - {combined['isg_color']}") template.append("\n") else: template.append("Aucune chaîne à risque critique identifiée.\n") # 2. Chaînes majeures template.append("### Chaînes avec risque majeur\n") template.append("*Ces chaînes comprennent au moins trois vulnérabilités combinées moyennes*\n") if major_chains: for chain in major_chains: product_name = data["products"][chain["product"]]["label"] component_name = data["components"][chain["component"]]["label"] mineral_name = data["minerals"][chain["mineral"]]["label"] template.append(f"#### {product_name} → {component_name} → {mineral_name}\n") # Vulnérabilités template.append("**Vulnérabilités identifiées:**\n") for vuln in chain["vulnerabilities"]: vuln_type = vuln["type"].capitalize() vuln_level = vuln["vulnerability"] if vuln_type == "Minerai": mineral_id = vuln["mineral_id"] template.append(f"* {vuln_type} ({mineral_name}): {vuln_level}\n") if mineral_id in results["ics_ivc_combined"]: combined = results["ics_ivc_combined"][mineral_id] template.append(f" * ICS moyen: {combined['ics_average']:.2f} - {combined['ics_color']}\n") template.append(f" * IVC: {combined['ivc_value']} - {combined['ivc_color']}\n") else: op_id = vuln["operation_id"] op_label = data["operations"][op_id]["label"] template.append(f"* {vuln_type} ({op_label}): {vuln_level}\n") if op_id in results["ihh_isg_combined"]: combined = results["ihh_isg_combined"][op_id] template.append(f" * IHH: {combined['ihh_value']} - {combined['ihh_color']}\n") template.append(f" * ISG combiné: {combined['isg_combined']:.0f} - {combined['isg_color']}\n") template.append("\n") else: template.append("Aucune chaîne à risque majeur identifiée.\n") # 3. Chaînes moyennes template.append("### Chaînes avec risque moyen\n") template.append("*Ces chaînes comprennent au moins une vulnérabilité combinée moyenne*\n") if medium_chains: for chain in medium_chains: product_name = data["products"][chain["product"]]["label"] component_name = data["components"][chain["component"]]["label"] mineral_name = data["minerals"][chain["mineral"]]["label"] template.append(f"#### {product_name} → {component_name} → {mineral_name}\n") # Vulnérabilités template.append("**Vulnérabilités identifiées:**\n") for vuln in chain["vulnerabilities"]: vuln_type = vuln["type"].capitalize() vuln_level = vuln["vulnerability"] if vuln_type == "Minerai": mineral_id = vuln["mineral_id"] template.append(f"* {vuln_type} ({mineral_name}): {vuln_level}\n") if mineral_id in results["ics_ivc_combined"]: combined = results["ics_ivc_combined"][mineral_id] template.append(f" * ICS moyen: {combined['ics_average']:.2f} - {combined['ics_color']}\n") template.append(f" * IVC: {combined['ivc_value']} - {combined['ivc_color']}\n") else: op_id = vuln["operation_id"] op_label = data["operations"][op_id]["label"] template.append(f"* {vuln_type} ({op_label}): {vuln_level}\n") if op_id in results["ihh_isg_combined"]: combined = results["ihh_isg_combined"][op_id] template.append(f" * IHH: {combined['ihh_value']} - {combined['ihh_color']}\n") template.append(f" * ISG combiné: {combined['isg_combined']:.0f} - {combined['isg_color']}\n") template.append("\n") else: template.append("Aucune chaîne à risque moyen identifiée.\n") return "\n".join(template) def generate_report(data, results, config): """ Génère le rapport complet structuré selon les spécifications. """ # Titre principal report = ["# Évaluation des vulnérabilités critiques\n"] # Section d'introduction report.append(generate_introduction_section(data)) # Section méthodologie report.append(generate_methodology_section()) # Section détails des opérations report.append(generate_operations_section(data, results, config)) # Section détails des minerais report.append(generate_minerals_section(data, results, config)) # Section chemins critiques report.append(generate_critical_paths_section(data, results)) return "\n".join(report) def write_report(report, config): """Écrit le rapport généré dans le fichier spécifié.""" template_path = config['template_path'] with open(template_path, 'w', encoding='utf-8') as f: f.write(report) print(f"Rapport généré avec succès: {template_path}") def main(): """Fonction principale du script.""" # Charger la configuration config = load_config(CONFIG_PATH) # Analyser les graphes graph, ref_graph = parse_graphs(config) # Extraire les données data = extract_data_from_graph(graph, ref_graph) # Calculer les vulnérabilités results = calculate_vulnerabilities(data, config) # Générer le rapport report = generate_report(data, results, config) report = re.sub(r'', '', report) report = re.sub(r'\n\n\n+', '\n\n', report) # Écrire le rapport write_report(report, config) if __name__ == "__main__": main()