docs+fix: renforcement de l'architecture agentique et corrections de sécurité
AGENTS.md :
- Rôles d'agents renforcés : @coder ne valide pas, @debugger ne code pas,
@verifier ne modifie pas, etc.
- Table « Séparation des rôles » : tâche → agent responsable → ne pas confier à
- Workflow étape 5 : délégation explicite à @verifier pour la validation
GUIDE_DEV_PYTHON.md :
- 7 notes « Décision d'implémentation » ajoutées aux sections concernées :
3.1.1 (XMPP_RECIPIENT→XMPP_TO, vars ajoutées), 3.1.2 (SYNC_PAST_DAYS→AppSettings),
3.2 (Pydantic v2 style, defaults corrigés), 4.2.1 (redact_exception module function),
4.2.2 (getLevelNamesMapping), 5.1.5 (normalize_pronote_uid, usedforsecurity=False),
6 (ConfigDict, StrEnum, alias _date, external_info Optional)
Sécurité (audit @security-auditor, corrections @coder, validation @verifier) :
- RedactingFormatter : redaction APRÈS formatage (corrige TypeError %s + fuite traceback)
- redact_url : masquage des credentials dans userinfo URL (HTTP Basic Auth)
- redact_secrets : patterns étendus (api_key, access_token, authorization, auth)
- redact_secrets : support JSON-style « key: value » avec guillemets
Validations (@verifier) :
- ruff check : PASS | mypy strict : PASS | bandit : PASS (0 issue)
- %s formatting : OK (password=REDACTED, pas de TypeError)
- Traceback redaction : OK (icalsecurise=REDACTED)
- URL userinfo : OK (user:REDACTED@host)
- JSON-style redaction : OK ({"token": "REDACTED"})
- Régression red-to-green : OK (historical HEAD reproduction)
Co-authored-by: OpenCode/orchestrator <opencode-orchestrator@agents.invalid>
This commit is contained in:
@@ -90,6 +90,10 @@
|
|||||||
{
|
{
|
||||||
"path": "detect_secrets.filters.allowlist.is_line_allowlisted"
|
"path": "detect_secrets.filters.allowlist.is_line_allowlisted"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "detect_secrets.filters.common.is_baseline_file",
|
||||||
|
"filename": ".secrets.baseline"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies",
|
"path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies",
|
||||||
"min_level": 2
|
"min_level": 2
|
||||||
@@ -136,9 +140,9 @@
|
|||||||
"filename": "GUIDE_DEV_PYTHON.md",
|
"filename": "GUIDE_DEV_PYTHON.md",
|
||||||
"hashed_secret": "90bd1b48e958257948487b90bee080ba5ed00caa",
|
"hashed_secret": "90bd1b48e958257948487b90bee080ba5ed00caa",
|
||||||
"is_verified": false,
|
"is_verified": false,
|
||||||
"line_number": 5073
|
"line_number": 5112
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"generated_at": "2026-09-05T17:53:24Z"
|
"generated_at": "2026-09-05T21:51:55Z"
|
||||||
}
|
}
|
||||||
|
|||||||
44
AGENTS.md
44
AGENTS.md
@@ -189,22 +189,38 @@ Cette section s'applique uniquement lorsque le travail est exécuté avec le sys
|
|||||||
|
|
||||||
Les rôles d'agents disponibles pour ce projet sont les suivants :
|
Les rôles d'agents disponibles pour ce projet sont les suivants :
|
||||||
|
|
||||||
- `@architect` : Arbitrages d'architecture et choix techniques structurants pour le pipeline `pronote-sync`.
|
- `@architect` : Arbitrages d'architecture et choix techniques structurants. **Ne produit pas de code.**
|
||||||
- `@coder` : Opérations de développement et changements de code dans le projet.
|
- `@coder` : Écrit et modifie du code, de la configuration et des scripts. **Ne valide pas** (ruff, mypy, pytest) — c'est le rôle de `@verifier`. **Ne diagnostique pas** — c'est le rôle de `@debugger`.
|
||||||
- `@debugger` : Reproduction d'un symptôme et établissement de sa cause profonde (ex. : échec de synchronisation, repli iCal/pronotepy).
|
- `@debugger` : Reproduit un symptôme et établit sa cause profonde. **Ne modifie pas le code.**
|
||||||
- `@explorer` : Exploration du dépôt en lecture seule et fourniture de contexte factuel.
|
- `@explorer` : Explore le dépôt en lecture seule. **Ne modifie rien, n'exécute pas de commandes.**
|
||||||
- `@orchestrator` : Compréhension globale du projet, définition des jalons, coordination et garantie du résultat.
|
- `@orchestrator` : Compréhension globale, définition des jalons, coordination et garantie du résultat. **N'écrit pas de code.**
|
||||||
- `@planner` : Transformation d'une demande complexe en unités exécutables avec frontières et dépendances claires.
|
- `@planner` : Transforme une demande complexe en unités exécutables. **Ne dirige aucun technicien.**
|
||||||
- `@reviewer` : Revues indépendantes de correction, régression, contrats et maintenabilité.
|
- `@reviewer` : Revues indépendantes de correction, régression, contrats et maintenabilité. **Ne modifie pas le code.**
|
||||||
- `@security-auditor` : Audit indépendant d'une surface de sécurité désignée (ex. : gestion des secrets, masquage des données).
|
- `@security-auditor` : Audit indépendant d'une surface de sécurité. **Ne modifie pas le code.**
|
||||||
- `@tech-writer` : Rédaction et maintenance de documentation technique exacte et vérifiable.
|
- `@tech-writer` : Rédige et maintient la documentation. **N'écrit pas de code applicatif.**
|
||||||
- `@test-engineer` : Conception, écriture et exécution de tests ciblés (unitaires, intégration, mocks).
|
- `@test-engineer` : Conçoit, écrit et exécute des tests ciblés. **N'écrit pas de code de production.**
|
||||||
- `@ui-designer` : conception et implémentation d'interfaces Web et terminal.
|
- `@ui-designer` : Conçoit et implémente les interfaces Web et terminal.
|
||||||
- `@verifier` : Vérification indépendante du comportement livré, des régressions et du respect des conventions (idempotence, mode dégradé).
|
- `@verifier` : Vérifie indépendamment le comportement livré, les régressions et le respect des conventions (ruff, mypy, pytest, bandit, idempotence, mode dégradé). **Ne modifie pas le code.**
|
||||||
- `@web-explorer` : Recherche et extraction de sources Web vérifiables (ex. : documentation Pronote, CalDAV, XMPP).
|
- `@web-explorer` : Recherche et extrait des sources Web vérifiables. **Ne modifie pas le dépôt.**
|
||||||
|
|
||||||
> **Note** : Ne pas utiliser `@coder` pour les tâches de documentation (`@tech-writer`) ni pour les tests (`@test-engineer`).
|
> **Note** : Ne pas utiliser `@coder` pour les tâches de documentation (`@tech-writer`) ni pour les tests (`@test-engineer`).
|
||||||
|
|
||||||
|
### Séparation des rôles
|
||||||
|
|
||||||
|
| Type de tâche | Agent responsable | Ne pas confier à |
|
||||||
|
|---|---|---|
|
||||||
|
| Écrire/modifier du code | `@coder` | `@verifier`, `@explorer` |
|
||||||
|
| Valider (ruff, mypy, pytest, bandit) | `@verifier` | `@coder` |
|
||||||
|
| Diagnostiquer un bug | `@debugger` | `@coder` |
|
||||||
|
| Écrire un test | `@test-engineer` | `@coder` |
|
||||||
|
| Rédiger de la documentation | `@tech-writer` | `@coder` |
|
||||||
|
| Explorer le dépôt (lecture) | `@explorer` | `@coder`, `@verifier` |
|
||||||
|
| Arbitrage technique structurant | `@architect` | `@coder`, `@planner` |
|
||||||
|
| Revue de code | `@reviewer` | `@coder`, `@verifier` |
|
||||||
|
| Audit de sécurité | `@security-auditor` | `@coder`, `@verifier` |
|
||||||
|
| Recherche web | `@web-explorer` | `@explorer` |
|
||||||
|
| Découpage de travail complexe | `@planner` | `@coder` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 10. Workflow de modification
|
## 10. Workflow de modification
|
||||||
@@ -213,7 +229,7 @@ Les rôles d'agents disponibles pour ce projet sont les suivants :
|
|||||||
2. Préserver les changements existants de l'utilisateur.
|
2. Préserver les changements existants de l'utilisateur.
|
||||||
3. Pour une correction, reproduire d'abord le défaut avec un test automatisé lorsque c'est raisonnable.
|
3. Pour une correction, reproduire d'abord le défaut avec un test automatisé lorsque c'est raisonnable.
|
||||||
4. Faire une modification étroite et cohérente, en respectant les conventions du projet (idempotence, mode dégradé, repli iCal/pronotepy).
|
4. Faire une modification étroite et cohérente, en respectant les conventions du projet (idempotence, mode dégradé, repli iCal/pronotepy).
|
||||||
5. Vérifier le comportement nominal et les cas d'erreur, notamment :
|
5. Faire vérifier le comportement par `@verifier` (ruff, mypy, pytest, bandit) et les cas d'erreur, notamment :
|
||||||
- Succès de la synchronisation Pronote → CalDAV/XMPP.
|
- Succès de la synchronisation Pronote → CalDAV/XMPP.
|
||||||
- Repli vers iCal en cas d'échec de `pronotepy`.
|
- Repli vers iCal en cas d'échec de `pronotepy`.
|
||||||
- Gestion des erreurs explicites.
|
- Gestion des erreurs explicites.
|
||||||
|
|||||||
@@ -271,6 +271,10 @@ Le projet utilise **`pydantic-settings`** pour valider et charger la configurati
|
|||||||
| `XMPP_PASSWORD` | Mot de passe XMPP. | `SecretStr` (masqué) | `SecretStr` |
|
| `XMPP_PASSWORD` | Mot de passe XMPP. | `SecretStr` (masqué) | `SecretStr` |
|
||||||
| `XMPP_RECIPIENT` | Destinataire XMPP (ex: `parent@example.com`). | `parent@example.com` | `str` |
|
| `XMPP_RECIPIENT` | Destinataire XMPP (ex: `parent@example.com`). | `parent@example.com` | `str` |
|
||||||
|
|
||||||
|
> ⚠️ **Décision d'implémentation** :
|
||||||
|
> `XMPP_RECIPIENT` a été renommé en `XMPP_TO` dans l'implémentation (aligné avec §10.2.1).
|
||||||
|
> Des variables XMPP supplémentaires ont été ajoutées : `XMPP_ENABLED`, `XMPP_HOST`, `XMPP_PORT`, `XMPP_RESOURCE`, `XMPP_USE_TLS`, `XMPP_TIMEOUT`.
|
||||||
|
> Une section `BLOG_ENABLED` et `BLOG_RSS_URL` a été ajoutée dans `.env.example`.
|
||||||
|
|
||||||
#### 3.1.2 Variables optionnelles
|
#### 3.1.2 Variables optionnelles
|
||||||
|
|
||||||
@@ -281,6 +285,10 @@ Le projet utilise **`pydantic-settings`** pour valider et charger la configurati
|
|||||||
| `PRONOTE_MESSAGES_SOURCE` | Source pour les messages (`pronotepy` uniquement). | `pronotepy` | `Literal` |
|
| `PRONOTE_MESSAGES_SOURCE` | Source pour les messages (`pronotepy` uniquement). | `pronotepy` | `Literal` |
|
||||||
| `SYNC_PAST_DAYS` | Nombre de jours dans le passé pour la sync CalDAV. | `7` | `int` |
|
| `SYNC_PAST_DAYS` | Nombre de jours dans le passé pour la sync CalDAV. | `7` | `int` |
|
||||||
| `SYNC_FUTURE_DAYS` | Nombre de jours dans le futur pour la sync CalDAV. | `30` | `int` |
|
| `SYNC_FUTURE_DAYS` | Nombre de jours dans le futur pour la sync CalDAV. | `30` | `int` |
|
||||||
|
|
||||||
|
> ⚠️ **Décision d'implémentation** :
|
||||||
|
> Ces variables sont désormais dans `AppSettings` (et non `CalDAVSettings`) car `CalDAVSettings` utilise `env_prefix="CALDAV_"`, ce qui nécessiterait `CALDAV_SYNC_PAST_DAYS`.
|
||||||
|
> Leur placement dans `AppSettings` (sans préfixe) garantit un mappage correct avec `SYNC_PAST_DAYS` / `SYNC_FUTURE_DAYS`.
|
||||||
| `THEORETICAL_AGENDA_PATH` | Chemin vers le fichier iCal/CSV de l'agenda théorique. | `None` | `str \| None`|
|
| `THEORETICAL_AGENDA_PATH` | Chemin vers le fichier iCal/CSV de l'agenda théorique. | `None` | `str \| None`|
|
||||||
| `AI_ENABLED` | Activer la synthèse IA. | `False` | `bool` |
|
| `AI_ENABLED` | Activer la synthèse IA. | `False` | `bool` |
|
||||||
| `AI_BASE_URL` | URL de base pour l'API IA (ex: OpenAI compatible). | `None` | `str \| None`|
|
| `AI_BASE_URL` | URL de base pour l'API IA (ex: OpenAI compatible). | `None` | `str \| None`|
|
||||||
@@ -335,6 +343,15 @@ LOG_LEVEL=INFO
|
|||||||
|
|
||||||
### 3.2 Modèle Pydantic pour la configuration
|
### 3.2 Modèle Pydantic pour la configuration
|
||||||
|
|
||||||
|
> ⚠️ **Décision d'implémentation** :
|
||||||
|
> L'implémentation utilise le style moderne de Pydantic v2 : `model_config = ConfigDict(frozen=True)` au lieu de `class Config`, pas de `json_encoders` (la sérialisation ISO est native en v2), `str | None` au lieu de `Optional[str]`, `list[str]` au lieu de `List[str]`.
|
||||||
|
> `AISettings.enabled` a pour valeur par défaut `False` (et non `True` comme indiqué dans le bloc de code).
|
||||||
|
> `XmppSettings` est entièrement défini en §10.2.3 avec tous les champs optionnels (valeurs par défaut) pour que `Settings()` fonctionne sans `.env`.
|
||||||
|
> `BlogSettings` a été ajouté (§5 bis.9.2) avec `enabled=False` et `rss_url` par défaut.
|
||||||
|
> `sync_past_days` et `sync_future_days` sont dans `AppSettings`, et non `CalDAVSettings`.
|
||||||
|
> `CalDAVSettings.calendar_path` a pour valeur par défaut `"/pronote-sync/"` (et non `"/pronote-digest/"`).
|
||||||
|
> `XmppSettings.resource` a pour valeur par défaut `"pronote-sync"` (et non `"pronote-digest"`).
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from typing import Literal, Optional
|
from typing import Literal, Optional
|
||||||
from pydantic import SecretStr, Field
|
from pydantic import SecretStr, Field
|
||||||
@@ -452,6 +469,11 @@ Toutes les exceptions externes (HTTP, Pronote, CalDAV, XMPP, IA) **doivent** êt
|
|||||||
|
|
||||||
#### 4.2.1 Masquage des URLs (`redaction.py`)
|
#### 4.2.1 Masquage des URLs (`redaction.py`)
|
||||||
|
|
||||||
|
> ⚠️ **Décision d'implémentation** :
|
||||||
|
> `redact_exception` est implémenté comme une **fonction au niveau du module** dans `utils/redaction.py`, et non comme une méthode de `RedactingFormatter` (contrairement à §4.2.2 où elle apparaît comme une méthode).
|
||||||
|
> `redact_url` utilise `urlsplit`/`urlunsplit`/`parse_qsl` au lieu de `urlparse`/`urlunparse`/`parse_qs`.
|
||||||
|
> La correspondance des clés sensibles est insensible à la casse.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
import re
|
import re
|
||||||
from urllib.parse import urlparse, urlunparse, parse_qs, urlencode
|
from urllib.parse import urlparse, urlunparse, parse_qs, urlencode
|
||||||
@@ -506,6 +528,11 @@ def redact_secrets(text: str) -> str:
|
|||||||
|
|
||||||
#### 4.2.2 Configuration des logs (`logging.py`)
|
#### 4.2.2 Configuration des logs (`logging.py`)
|
||||||
|
|
||||||
|
> ⚠️ **Décision d'implémentation** :
|
||||||
|
> `redact_exception` est une fonction au niveau du module dans `redaction.py`, et non une méthode de `RedactingFormatter`.
|
||||||
|
> `setup_logging` utilise `logging.getLevelNamesMapping()` (Python 3.11+) au lieu de `getattr(logging, ...)`.
|
||||||
|
> `RedactingFormatter.format` gère à la fois les `record.args` de type tuple et dict.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
@@ -1684,6 +1711,10 @@ homeworks = collect_homeworks(lessons, target_date)
|
|||||||
|
|
||||||
#### 5.1.5 Normalisation des UID
|
#### 5.1.5 Normalisation des UID
|
||||||
|
|
||||||
|
> ⚠️ **Décision d'implémentation** :
|
||||||
|
> La fonction est nommée `normalize_pronote_uid` (et non `normalize_uid` comme référencé dans TODO.md).
|
||||||
|
> `generate_deterministic_uid` utilise `hashlib.sha1(payload, usedforsecurity=False)` pour satisfaire la règle bandit B324 (le hachage n'est pas utilisé pour la sécurité).
|
||||||
|
|
||||||
Les UID Pronote contiennent des **suffixes temporels** qui changent à chaque export. Exemple réel :
|
Les UID Pronote contiennent des **suffixes temporels** qui changent à chaque export. Exemple réel :
|
||||||
```
|
```
|
||||||
UID:Cours-16027-1-20260904T120218Z-Index-Education
|
UID:Cours-16027-1-20260904T120218Z-Index-Education
|
||||||
@@ -2360,6 +2391,14 @@ class PronoteFetcher:
|
|||||||
|
|
||||||
## 6. Modèle de données Pydantic
|
## 6. Modèle de données Pydantic
|
||||||
|
|
||||||
|
> ⚠️ **Décision d'implémentation (M3)** :
|
||||||
|
> Tous les modèles utilisent `model_config = ConfigDict(frozen=True)` (Pydantic v2), et non `class Config: frozen = True`.
|
||||||
|
> Pas de `json_encoders` : la sérialisation ISO pour `datetime`/`date` est native dans Pydantic v2.
|
||||||
|
> Les énumérations utilisent `enum.StrEnum` (Python 3.11+) au lieu de `(str, Enum)` (règle ruff UP042).
|
||||||
|
> Dans `HomeworkBlock`, le champ `date` utilise un alias d'import `_date` (`from datetime import date as _date`) pour éviter un conflit de nom/champ dans Pydantic v2. Même chose pour `time` → `_time` dans `TheoreticalLesson`.
|
||||||
|
> `XmppMessage.external_info` est de type `ExternalInfo | None` avec une valeur par défaut `None` (et non `default_factory=ExternalInfo`) : le pipeline passe `None` lorsqu'il n'y a pas d'informations externes.
|
||||||
|
> Les modèles sont répartis en 10 modules domaines (agenda, homework, message, blog, diff, pronote, sync, synthesis, xmpp) avec `__init__.py` réexportant les 22 noms via `__all__`.
|
||||||
|
|
||||||
### 6.1 Principes
|
### 6.1 Principes
|
||||||
- **Modèles distincts par domaine** : Ne pas créer un unique modèle fourre-tout. Chaque étape du pipeline utilise des modèles dédiés (décision architecturale).
|
- **Modèles distincts par domaine** : Ne pas créer un unique modèle fourre-tout. Chaque étape du pipeline utilise des modèles dédiés (décision architecturale).
|
||||||
- **Validation stricte** : Utiliser Pydantic pour valider les données dès leur création.
|
- **Validation stricte** : Utiliser Pydantic pour valider les données dès leur création.
|
||||||
|
|||||||
@@ -22,26 +22,15 @@ class RedactingFormatter(logging.Formatter):
|
|||||||
def format(self, record: logging.LogRecord) -> str:
|
def format(self, record: logging.LogRecord) -> str:
|
||||||
"""Formate un enregistrement de log en masquant les secrets.
|
"""Formate un enregistrement de log en masquant les secrets.
|
||||||
|
|
||||||
Le message et chaque argument textuel de l'enregistrement sont rédigés
|
La rédaction est appliquée à la chaîne finale (message, arguments et
|
||||||
avant le formatage final effectué par :class:`logging.Formatter`.
|
traceback inclus) produite par :class:`logging.Formatter`.
|
||||||
|
|
||||||
:param record: Enregistrement de log à formater.
|
:param record: Enregistrement de log à formater.
|
||||||
:return: Message formaté, avec les secrets remplacés par ``REDACTED``.
|
:return: Message formaté, avec les secrets remplacés par ``REDACTED``.
|
||||||
:rtype: str
|
:rtype: str
|
||||||
"""
|
"""
|
||||||
record.msg = redact_secrets(str(record.msg))
|
formatted = super().format(record)
|
||||||
args = record.args
|
return redact_secrets(formatted)
|
||||||
if args:
|
|
||||||
if isinstance(args, tuple):
|
|
||||||
record.args = tuple(
|
|
||||||
redact_secrets(arg) if isinstance(arg, str) else arg for arg in args
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
record.args = {
|
|
||||||
key: redact_secrets(value) if isinstance(value, str) else value
|
|
||||||
for key, value in args.items()
|
|
||||||
}
|
|
||||||
return super().format(record)
|
|
||||||
|
|
||||||
|
|
||||||
def setup_logging(level: str = "INFO") -> None:
|
def setup_logging(level: str = "INFO") -> None:
|
||||||
|
|||||||
@@ -10,10 +10,26 @@ from __future__ import annotations
|
|||||||
import re
|
import re
|
||||||
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
|
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
|
||||||
|
|
||||||
_SENSITIVE_QUERY_KEYS = frozenset({"icalsecurise", "token", "key", "password", "secret"})
|
_SENSITIVE_QUERY_KEYS = frozenset(
|
||||||
|
{
|
||||||
|
"icalsecurise",
|
||||||
|
"token",
|
||||||
|
"key",
|
||||||
|
"password",
|
||||||
|
"secret",
|
||||||
|
"api_key",
|
||||||
|
"apikey",
|
||||||
|
"access_token",
|
||||||
|
"auth",
|
||||||
|
"authorization",
|
||||||
|
}
|
||||||
|
)
|
||||||
_URL_PATTERN = re.compile(r"https?://[^\s]+")
|
_URL_PATTERN = re.compile(r"https?://[^\s]+")
|
||||||
_ISOLATED_SECRET_PATTERN = re.compile(
|
_ISOLATED_SECRET_PATTERN = re.compile(
|
||||||
r"\b(icalsecurise|token|password|secret|key)\s*=\s*[^\s&]+",
|
r"\b(icalsecurise|access_token|api_key|apikey|authorization|token|password|secret|key|auth)"
|
||||||
|
r"(\s*['\"]?\s*[:=]\s*)"
|
||||||
|
r"(['\"]?)"
|
||||||
|
r"([^\s&'\"]+)",
|
||||||
re.IGNORECASE,
|
re.IGNORECASE,
|
||||||
)
|
)
|
||||||
_REDACTED = "REDACTED"
|
_REDACTED = "REDACTED"
|
||||||
@@ -21,15 +37,30 @@ _REDACTED_URL = "REDACTED_URL"
|
|||||||
|
|
||||||
|
|
||||||
def redact_url(url: str) -> str:
|
def redact_url(url: str) -> str:
|
||||||
"""Masque les paramètres sensibles dans une URL.
|
"""Masque les identifiants et les paramètres sensibles d'une URL.
|
||||||
|
|
||||||
:param url: URL pouvant contenir des paramètres sensibles (ex: ``icalsecurise``).
|
Les informations d'authentification du netloc (``utilisateur:motdepasse@hôte``)
|
||||||
:return: URL avec les paramètres sensibles remplacés par ``REDACTED``,
|
sont masquées, ainsi que les paramètres sensibles de la requête
|
||||||
|
(ex: ``icalsecurise``).
|
||||||
|
|
||||||
|
:param url: URL pouvant contenir des informations sensibles (ex: ``icalsecurise``).
|
||||||
|
:return: URL avec les éléments sensibles remplacés par ``REDACTED``,
|
||||||
ou ``REDACTED_URL`` si le traitement échoue.
|
ou ``REDACTED_URL`` si le traitement échoue.
|
||||||
:rtype: str
|
:rtype: str
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
parts = urlsplit(url)
|
parts = urlsplit(url)
|
||||||
|
if parts.username is not None or parts.password is not None:
|
||||||
|
# Netloc sûr : utilisateur:REDACTED@hôte:port. Le deux-point est
|
||||||
|
# conservé même en l'absence de mot de passe explicite.
|
||||||
|
userinfo = parts.username or ""
|
||||||
|
userinfo += ":REDACTED"
|
||||||
|
host = parts.hostname or ""
|
||||||
|
if parts.port is not None:
|
||||||
|
netloc = f"{userinfo}@{host}:{parts.port}"
|
||||||
|
else:
|
||||||
|
netloc = f"{userinfo}@{host}"
|
||||||
|
parts = parts._replace(netloc=netloc)
|
||||||
query: list[tuple[str, str]] = parse_qsl(parts.query, keep_blank_values=True)
|
query: list[tuple[str, str]] = parse_qsl(parts.query, keep_blank_values=True)
|
||||||
redacted_query = [
|
redacted_query = [
|
||||||
(key, _REDACTED if key.lower() in _SENSITIVE_QUERY_KEYS else value)
|
(key, _REDACTED if key.lower() in _SENSITIVE_QUERY_KEYS else value)
|
||||||
@@ -44,15 +75,15 @@ def redact_secrets(text: str) -> str:
|
|||||||
"""Masque les secrets présents dans un texte arbitraire.
|
"""Masque les secrets présents dans un texte arbitraire.
|
||||||
|
|
||||||
Les URLs sont d'abord traitées par :func:`redact_url`, puis les affectations
|
Les URLs sont d'abord traitées par :func:`redact_url`, puis les affectations
|
||||||
isolées de type ``cle=valeur`` (ex: ``icalsecurise=XXX``) sont masquées,
|
isolées de type ``cle=valeur`` ou ``cle:valeur`` (ex: ``icalsecurise=XXX``,
|
||||||
sans distinction de casse.
|
``"token": "XXX"``) sont masquées, sans distinction de casse.
|
||||||
|
|
||||||
:param text: Texte pouvant contenir des URLs ou des secrets en clair.
|
:param text: Texte pouvant contenir des URLs ou des secrets en clair.
|
||||||
:return: Texte avec les secrets remplacés par ``REDACTED``.
|
:return: Texte avec les secrets remplacés par ``REDACTED``.
|
||||||
:rtype: str
|
:rtype: str
|
||||||
"""
|
"""
|
||||||
redacted = _URL_PATTERN.sub(lambda match: redact_url(match.group(0)), text)
|
redacted = _URL_PATTERN.sub(lambda match: redact_url(match.group(0)), text)
|
||||||
return _ISOLATED_SECRET_PATTERN.sub(r"\1=REDACTED", redacted)
|
return _ISOLATED_SECRET_PATTERN.sub(r"\1\2\3REDACTED", redacted)
|
||||||
|
|
||||||
|
|
||||||
def redact_exception(exc: Exception) -> str:
|
def redact_exception(exc: Exception) -> str:
|
||||||
|
|||||||
Reference in New Issue
Block a user