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:
@@ -22,26 +22,15 @@ class RedactingFormatter(logging.Formatter):
|
||||
def format(self, record: logging.LogRecord) -> str:
|
||||
"""Formate un enregistrement de log en masquant les secrets.
|
||||
|
||||
Le message et chaque argument textuel de l'enregistrement sont rédigés
|
||||
avant le formatage final effectué par :class:`logging.Formatter`.
|
||||
La rédaction est appliquée à la chaîne finale (message, arguments et
|
||||
traceback inclus) produite par :class:`logging.Formatter`.
|
||||
|
||||
:param record: Enregistrement de log à formater.
|
||||
:return: Message formaté, avec les secrets remplacés par ``REDACTED``.
|
||||
:rtype: str
|
||||
"""
|
||||
record.msg = redact_secrets(str(record.msg))
|
||||
args = record.args
|
||||
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)
|
||||
formatted = super().format(record)
|
||||
return redact_secrets(formatted)
|
||||
|
||||
|
||||
def setup_logging(level: str = "INFO") -> None:
|
||||
|
||||
@@ -10,10 +10,26 @@ from __future__ import annotations
|
||||
import re
|
||||
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]+")
|
||||
_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,
|
||||
)
|
||||
_REDACTED = "REDACTED"
|
||||
@@ -21,15 +37,30 @@ _REDACTED_URL = "REDACTED_URL"
|
||||
|
||||
|
||||
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``).
|
||||
:return: URL avec les paramètres sensibles remplacés par ``REDACTED``,
|
||||
Les informations d'authentification du netloc (``utilisateur:motdepasse@hôte``)
|
||||
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.
|
||||
:rtype: str
|
||||
"""
|
||||
try:
|
||||
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)
|
||||
redacted_query = [
|
||||
(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.
|
||||
|
||||
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,
|
||||
sans distinction de casse.
|
||||
isolées de type ``cle=valeur`` ou ``cle:valeur`` (ex: ``icalsecurise=XXX``,
|
||||
``"token": "XXX"``) sont masquées, sans distinction de casse.
|
||||
|
||||
:param text: Texte pouvant contenir des URLs ou des secrets en clair.
|
||||
:return: Texte avec les secrets remplacés par ``REDACTED``.
|
||||
:rtype: str
|
||||
"""
|
||||
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:
|
||||
|
||||
Reference in New Issue
Block a user