Code/app/fiches/utils/fiche_utils.py
Stéphan Peccini cef9c9d67b
feat(security): Analyse et corrections de sécurité complète
Corrections de sécurité (Bandit):
- Fix vulnérabilité HIGH Jinja2 XSS avec documentation de sécurité
- Ajout timeouts sur toutes les requêtes HTTP (17 occurrences)
  * utils/gitea.py: 3 timeouts (10s)
  * app/fiches/interface.py: 1 timeout (10s)
  * scripts/auto_ingest.py: 2 timeouts (10-30s)
  * scripts/generer_analyse.py: 10 timeouts (10-120s)
  * Corpus/generer_analyse.py: 2 timeouts (30-120s)

Documentation et configuration:
- docs/SECURITY.md: Guide complet de sécurité avec analyse Bandit
- docs/GUIDE_GITDOC.md: Guide d'utilisation GitDoc pour autocommit
- Configuration GitDoc complète dans settings.json.example
- Configuration .gitignore pour .vscode/settings.json
- Recommandations extensions VSCode (SonarLint, Snyk, GitDoc)
- Mise à jour README avec statut sécurité

Résultats Bandit:
- 0 vulnérabilités HIGH
- 0 vulnérabilités MEDIUM
- Tests: 67 passent, couverture 16%

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-08 08:57:22 +01:00

181 lines
6.0 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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

"""fiche_utils.py  outils de lecture / rendu des fiches Markdown (indices et opérations)
Dépendances :
pip install python-frontmatter pyyaml jinja2
Usage :
from fiche_utils import load_seuils, render_fiche_markdown
seuils = load_seuils("config/indices_seuils.yaml")
markdown_rendered = render_fiche_markdown(raw_md_text, seuils)
"""
from __future__ import annotations
import os
import pathlib
import re
from datetime import datetime, timezone
import frontmatter
import jinja2
import yaml
from utils.gitea import recuperer_date_dernier_commit
def load_seuils(path: str | pathlib.Path = "config/indices_seuils.yaml") -> dict:
"""Charge le fichier YAML des seuils et renvoie le dict 'seuils'.
Args:
path (str | pathlib.Path, optional): Chemin vers le fichier des seuils. Defaults to "config/indices_seuils.yaml".
Returns:
Dict: Dictionnaire contenant les seuils.
"""
data = yaml.safe_load(pathlib.Path(path).read_text(encoding="utf-8"))
return data.get("seuils", {})
def _migrate_metadata(meta: dict) -> dict:
"""Normalise les clés YAML (ex : sheet_type → type_fiche).
Args:
meta (Dict): Métadonnées à normaliser.
Returns:
Dict: Métadonnées normalisées.
"""
keymap = {
"sheet_type": "type_fiche",
"indice_code": "indice_court", # si besoin
}
for old, new in keymap.items():
if old in meta and new not in meta:
meta[new] = meta.pop(old)
return meta
def render_fiche_markdown(
md_text: str,
seuils: dict,
license_path: str = "assets/licence.md"
) -> str:
"""Renvoie la fiche rendue (Markdown) avec les placeholders remplacés et le tableau de version.
Args:
md_text (str): Contenu Markdown brut.
seuils (Dict): Tableau des versions.
license_path (str, optional): Chemin vers le fichier de licence. Defaults to "assets/licence.md".
Returns:
str: Fiche Markdown rendue avec les placeholders remplacés et la table de version.
Note:
- Les licences sont ajoutées après le tableau de version.
- Les titres de niveau 2 doivent être présents pour l'insertion automatique de licence.
"""
post = frontmatter.loads(md_text)
meta = _migrate_metadata(dict(post.metadata))
body_template = post.content
# Instancie Jinja2 en 'StrictUndefined' pour signaler les placeholders manquants.
# SECURITY NOTE: autoescape=False est approprié ici car :
# 1. Le template source est du Markdown (pas du HTML)
# 2. Les données proviennent de fichiers contrôlés (frontmatter), pas d'entrées utilisateur web
# 3. L'autoescape HTML casserait la syntaxe Markdown
# 4. Le rendu final est converti en HTML par un convertisseur Markdown séparé
env = jinja2.Environment(
undefined=jinja2.StrictUndefined,
autoescape=False, # nosec B701 - Safe for Markdown templates with controlled inputs
trim_blocks=True,
lstrip_blocks=True,
)
tpl = env.from_string(body_template)
rendered_body = tpl.render(**meta, seuils=seuils)
# Option : ajoute automatiquement titre + tableau version si absent.
header = f"# {meta.get('indice', meta.get('titre',''))} ({meta.get('indice_court','')})"
if not re.search(r"^# ", rendered_body, flags=re.M):
rendered_body = f"""{header}
{rendered_body}"""
# Charger le contenu de la licence
try:
license_content = pathlib.Path(license_path).read_text(encoding="utf-8")
# Insérer la licence après le tableau de version et avant le premier titre h2
# Trouver la position du premier titre h2
h2_match = re.search(r"^## ", rendered_body, flags=re.M)
if h2_match:
h2_position = h2_match.start()
rendered_body = f"{rendered_body[:h2_position]}\n\n{license_content}\n\n{rendered_body[h2_position:]}"
else:
# S'il n'y a pas de titre h2, ajouter la licence à la fin
rendered_body = f"{rendered_body}\n\n{license_content}"
except Exception as e:
# En cas d'erreur lors de la lecture du fichier de licence, continuer sans l'ajouter
import streamlit as st
st.error(e)
return rendered_body
def fichier_plus_recent(
chemin_fichier: str|None,
reference: datetime
) -> bool:
"""Vérifie si un fichier est plus récent que la référence donnée.
Args:
chemin_fichier (str): Chemin vers le fichier à vérifier.
reference (datetime): Référence temporelle de comparaison.
Returns:
bool: True si le fichier est plus récent, False sinon.
"""
try:
modif = datetime.fromtimestamp(os.path.getmtime(chemin_fichier), tz=timezone.utc)
return modif > reference
except Exception:
return False
def doit_regenerer_fiche(
html_path: str,
fiche_type: str,
fiche_choisie: str,
commit_url: str,
fichiers_criticite: dict[str, str]
) -> bool:
"""Détermine si une fiche doit être regénérée.
Args:
html_path (str): Chemin vers le fichier HTML.
fiche_type (str): Type de la fiche.
fiche_choisie (str): Nom choisi pour la fiche.
commit_url (str): URL du dernier commit.
fichiers_criticite (Dict[str, str]): Dictionnaire des fichiers critiques.
Returns:
bool: True si la fiche doit être regénérée, False sinon.
"""
if not os.path.exists(html_path):
return True
local_mtime = datetime.fromtimestamp(os.path.getmtime(html_path), tz=timezone.utc)
remote_mtime = recuperer_date_dernier_commit(commit_url)
if remote_mtime is None or remote_mtime > local_mtime:
return True
if fichier_plus_recent(fichiers_criticite.get("IHH"), local_mtime):
return True
if fiche_type == "minerai" or "minerai" in fiche_choisie.lower():
if fichier_plus_recent(fichiers_criticite.get("IVC"), local_mtime):
return True
if fichier_plus_recent(fichiers_criticite.get("ICS"), local_mtime):
return True
return False