From 894f5d137a4f8742bed907fd32853fa584bfb3cd Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 13 Sep 2026 00:16:25 +0200 Subject: [PATCH 1/2] =?UTF-8?q?fix(security):=20d=C3=A9tecter=20les=20PIN?= =?UTF-8?q?=20Pronote=20litt=C3=A9raux?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/check_secrets.py | 39 +++++++++++++++++++----- tests/unit/test_check_secrets.py | 52 ++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 8 deletions(-) diff --git a/scripts/check_secrets.py b/scripts/check_secrets.py index 155f6ac..4d63985 100644 --- a/scripts/check_secrets.py +++ b/scripts/check_secrets.py @@ -26,15 +26,15 @@ _TEXT_SUFFIXES = frozenset( {".conf", ".ini", ".json", ".md", ".py", ".service", ".timer", ".toml", ".txt", ".yaml", ".yml"} ) _LITERAL_SECRET_RE = re.compile( - r"(?ix)\b[a-z0-9_]*(?:api[_-]?key|access[_-]?token|auth(?:orization)?|icalsecurise|password|secret|token)" - r"\s*[:=]\s*['\"][^'\"\r\n]{3,}['\"]" + r"(?ix)\b[a-z0-9_]*(?:api[_-]?key|access[_-]?token|auth(?:orization)?|icalsecurise|password|pin|secret|token)" + r"\s*[:=]\s*['\"](?P[^'\"\r\n]{3,})['\"]" ) _UNQUOTED_SECRET_RE = re.compile( - r"(?ix)\b[a-z0-9_]*(?:api[_-]?key|access[_-]?token|auth(?:orization)?|icalsecurise|password|secret|token)" - r"\s*[:=]\s*[a-z0-9][a-z0-9._~+/-]{2,}" + r"(?ix)\b[a-z0-9_]*(?:api[_-]?key|access[_-]?token|auth(?:orization)?|icalsecurise|password|pin|secret|token)" + r"\s*[:=]\s*(?P[a-z0-9][a-z0-9._~+/-]{2,})" ) _URL_SECRET_RE = re.compile( - r"(?ix)[?&](?:api[_-]?key|access[_-]?token|auth(?:orization)?|icalsecurise|password|secret|token)" + r"(?ix)[?&](?:api[_-]?key|access[_-]?token|auth(?:orization)?|icalsecurise|password|pin|secret|token)" r"=([^&#\s]{3,})" ) _URL_PLACEHOLDER_RE = re.compile( @@ -47,6 +47,13 @@ _URL_PLACEHOLDER_RE = re.compile( r")$" ) _EXTRA_NAMES = frozenset({"pronote_sync"}) +_ASSIGNMENT_PLACEHOLDER_RE = re.compile( + r"(?ix)^(?:" + r"<(?:pin|secret|valeur|value|token|jeton)>|" + r"(?:change|replace|your)[_-]?(?:me|here|value|valeur|pin|password|secret)|" + r"(?:placeholder|example|local-not-required)" + r")$" +) @dataclass(frozen=True) @@ -68,6 +75,16 @@ CommandRunner = Callable[..., subprocess.CompletedProcess[str]] ContentProvider = Callable[[Path], str | None] +def _is_assignment_placeholder(value: str) -> bool: + """Indique si une valeur d'affectation est un placeholder documentaire. + + :param value: Valeur extraite d'une affectation sensible. + :return: ``True`` si la valeur ne représente pas un secret réel. + :rtype: bool + """ + return _ASSIGNMENT_PLACEHOLDER_RE.fullmatch(value.strip()) is not None + + def _is_candidate(path: Path) -> bool: """Indique si un chemin peut être analysé comme fichier texte. @@ -188,9 +205,15 @@ def find_secrets( for number, line in enumerate(content.splitlines(), start=1): if _ALLOWLIST_MARKER in line: continue - is_literal_secret = _LITERAL_SECRET_RE.search(line) or ( - relative_path.suffix in _UNQUOTED_CONFIG_SUFFIXES - and _UNQUOTED_SECRET_RE.search(line) + literal_match = _LITERAL_SECRET_RE.search(line) + unquoted_match = ( + _UNQUOTED_SECRET_RE.search(line) + if relative_path.suffix in _UNQUOTED_CONFIG_SUFFIXES + else None + ) + is_literal_secret = any( + match is not None and not _is_assignment_placeholder(match.group("value")) + for match in (literal_match, unquoted_match) ) if is_literal_secret: findings.append(SecretFinding(relative_path, number, "affectation-litterale")) diff --git a/tests/unit/test_check_secrets.py b/tests/unit/test_check_secrets.py index 110b38d..a35d502 100644 --- a/tests/unit/test_check_secrets.py +++ b/tests/unit/test_check_secrets.py @@ -236,6 +236,58 @@ def test_main_detects_prefixed_secret_assignment( assert sentinel not in output +def test_main_detects_pronote_pin_assignments_without_disclosing_value( + secret_checker: ModuleType, tmp_path: Path, capsys: CaptureFixture[str] +) -> None: + """Détecte les PIN Pronote littéraux et non quotés sans afficher leur valeur. + + :param secret_checker: Module du script sous test. + :param tmp_path: Répertoire temporaire représentant un dépôt. + :param capsys: Fixture de capture de sortie. + :return: None + """ + literal_pin = "pin-literal-sentinel" + unquoted_pin = "pin-unquoted-sentinel" + (tmp_path / "settings.py").write_text(f'PRONOTE_QR_PIN = "{literal_pin}"\n', encoding="utf-8") + (tmp_path / "settings.yaml").write_text( + f"PRONOTE_ACCOUNT_PIN: {unquoted_pin}\n", encoding="utf-8" + ) + + assert secret_checker.main([], root=tmp_path) == 1 + output = capsys.readouterr().out + assert "settings.py:1" in output + assert "settings.yaml:1" in output + assert literal_pin not in output + assert unquoted_pin not in output + + +@pytest.mark.parametrize( + "line", + [ + 'PRONOTE_QR_PIN = ""', + "# PRONOTE_ACCOUNT_PIN doit rester dans le fichier d'environnement local", + ], +) +def test_main_ignores_pronote_pin_placeholders_and_descriptions( + secret_checker: ModuleType, + tmp_path: Path, + capsys: CaptureFixture[str], + line: str, +) -> None: + """Ignore les placeholders et descriptions de PIN sans affectation réelle. + + :param secret_checker: Module du script sous test. + :param tmp_path: Répertoire temporaire représentant un dépôt. + :param capsys: Fixture de capture de sortie. + :param line: Ligne documentaire à analyser. + :return: None + """ + (tmp_path / "guide.py").write_text(line + "\n", encoding="utf-8") + + assert secret_checker.main([], root=tmp_path) == 0 + assert "OK:" in capsys.readouterr().out + + def test_main_detects_short_secret_assignment( secret_checker: ModuleType, tmp_path: Path, capsys: CaptureFixture[str] ) -> None: From 7387f9a78db3a4ce426291d8298f7957839a97c9 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 13 Sep 2026 00:17:07 +0200 Subject: [PATCH 2/2] fix(security): limiter PIN aux affectations --- scripts/check_secrets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/check_secrets.py b/scripts/check_secrets.py index 4d63985..6e2b04c 100644 --- a/scripts/check_secrets.py +++ b/scripts/check_secrets.py @@ -34,7 +34,7 @@ _UNQUOTED_SECRET_RE = re.compile( r"\s*[:=]\s*(?P[a-z0-9][a-z0-9._~+/-]{2,})" ) _URL_SECRET_RE = re.compile( - r"(?ix)[?&](?:api[_-]?key|access[_-]?token|auth(?:orization)?|icalsecurise|password|pin|secret|token)" + r"(?ix)[?&](?:api[_-]?key|access[_-]?token|auth(?:orization)?|icalsecurise|password|secret|token)" r"=([^&#\s]{3,})" ) _URL_PLACEHOLDER_RE = re.compile(