diff --git a/loopx/public_safe_text.py b/loopx/public_safe_text.py index 7096a36625..90bc089695 100644 --- a/loopx/public_safe_text.py +++ b/loopx/public_safe_text.py @@ -285,6 +285,26 @@ PRESENTATION_EXTENDED_WINDOWS_PATH_PATTERN, PRESENTATION_COLON_PREFIXED_WINDOWS_PATH_PATTERN, ) +# The provider-issued credential shapes, as one alternation body shared by every +# owner that has to recognize a token with no label at all. A prefix carries the +# provider's own issuer identity, so the shape is self-describing and a caller +# never has to guess a label; the length floor keeps ordinary prose out. Case +# folding is left to the consumer's compile flags or inline `(?i)`, so this +# string stays a plain alternation instead of owning one flag decision. +# +# Kept here rather than restated per caller: a scan that recognizes only some +# issuers certifies the rest as public-safe, and copies drift (Refs #5136). +PROVIDER_TOKEN_SHAPE_PATTERN_SOURCE = ( + r"\bgh[pousr]_[a-z0-9]{16,}\b|" + r"\bgithub_pat_[a-z0-9_]{20,}|" + r"\b(?:akia|asia)[a-z0-9]{16}\b|" + r"\bxox[baprs]-[a-z0-9-]{10,}|" + r"\baiza[a-z0-9_-]{20,}|" + r"\b(?:sk|rk)_(?:live|test)_[a-z0-9]{12,}|" + r"\bnpm_[a-z0-9]{20,}|" + r"\bpypi-[a-z0-9_-]{20,}|" + r"\beyj[a-z0-9_-]{10,}\.[a-z0-9_-]{10,}\.[a-z0-9_-]{10,}\b" +) PRESENTATION_PUBLIC_BOUNDARY_PATTERNS: tuple[ tuple[str, re.Pattern[str]], ... ] = ( @@ -311,31 +331,19 @@ re.IGNORECASE, ), ), + # An issuer prefix is its own label: a token carried in tool output or a + # pasted log rarely arrives as an assignment, so the assignment arm never + # sees it. Composed from the shared source the sibling scanners consume + # (#7636), case-folded like the presentation local-path arms. + ( + "provider token", + re.compile(r"(?i)(?:" + PROVIDER_TOKEN_SHAPE_PATTERN_SOURCE + r")"), + ), ) # Refs #5136: one definition for "this string carries a raw remote location". # Three validators each restated the same scheme list, and the canonical # public-safety owner had no counterpart, so a fourth caller had to invent one. REMOTE_LOCATION_SURFACE_PATTERN = re.compile(r"(?i)\b(?:https?|file|s3|gs|tos|hdfs)://") -# The provider-issued credential shapes, as one alternation body shared by every -# owner that has to recognize a token with no label at all. A prefix carries the -# provider's own issuer identity, so the shape is self-describing and a caller -# never has to guess a label; the length floor keeps ordinary prose out. Case -# folding is left to the consumer's compile flags or inline `(?i)`, so this -# string stays a plain alternation instead of owning one flag decision. -# -# Kept here rather than restated per caller: a scan that recognizes only some -# issuers certifies the rest as public-safe, and copies drift (Refs #5136). -PROVIDER_TOKEN_SHAPE_PATTERN_SOURCE = ( - r"\bgh[pousr]_[a-z0-9]{16,}\b|" - r"\bgithub_pat_[a-z0-9_]{20,}|" - r"\b(?:akia|asia)[a-z0-9]{16}\b|" - r"\bxox[baprs]-[a-z0-9-]{10,}|" - r"\baiza[a-z0-9_-]{20,}|" - r"\b(?:sk|rk)_(?:live|test)_[a-z0-9]{12,}|" - r"\bnpm_[a-z0-9]{20,}|" - r"\bpypi-[a-z0-9_-]{20,}|" - r"\beyj[a-z0-9_-]{10,}\.[a-z0-9_-]{10,}\.[a-z0-9_-]{10,}\b" -) SECRET_LIKE_SURFACE_PATTERN = re.compile( r"(?i)(?:\bbearer\s+[a-z0-9._~+/=-]{16,}|" r"\b(?:access|api|secret)[_-]?key[\"']?\s*[=:]\s*[\"']?[^\s`'\"<>]+|" diff --git a/tests/control_plane/test_presentation_public_safety.py b/tests/control_plane/test_presentation_public_safety.py index 7c30d72d94..d0b262aadc 100644 --- a/tests/control_plane/test_presentation_public_safety.py +++ b/tests/control_plane/test_presentation_public_safety.py @@ -75,3 +75,53 @@ def test_colon_prefixed_unix_paths_keep_the_existing_boundary() -> None: "ok": False, "warnings": ["absolute local path"], } + + +@pytest.mark.parametrize( + "token", + [ + "ghp_" + "A1b2C3d4E5f6G7h8I9j0K1l2M3n4", + "github_pat_11ABCDEFG0" + "a1B2c3D4e5F6", + "AKIA" + "7Q2Z3XJ5V6N8W1PL", + "ASIA" + "7Q2Z3XJ5V6N8W1PL", + "xoxb-" + "123456789-abcdef", + "AIzaSyA" + "1234567890abcdefghij", + "sk_live_51H8" + "aBcDeFgHiJkL", + "npm_" + "a1B2c3D4e5F6g7H8i9J0", + "pypi-AgEIcHlwaS5vcmc" + "AAAAAAAAAA", + "eyJhbGciOiJIUzI1NiJ9" + ".eyJzdWIiOiIxMjM0NTY3ODkwIn0" + ".SflKxwRJSMeKKF2QT4f", + ], +) +def test_unlabeled_provider_tokens_fail_the_boundary_scan(token: str) -> None: + text = f"CI log fragment: pushed with {token} to the registry mirror" + + assert public_safety.scan_public_boundary_text(text) == { + "ok": False, + "warnings": ["provider token"], + } + + +@pytest.mark.parametrize( + "text", + [ + "npm run build && npm test", + "gh pr list --limit 5", + "AWS key ids start AKIA and ASIA, then sixteen more characters", + "pypi- prefix alone is not a token", + "sk-proj-abcdefghijklmnop is an OpenAI project key shape", + ], +) +def test_ordinary_prose_and_unclaimed_shapes_stay_public(text: str) -> None: + assert public_safety.scan_public_boundary_text(text) == { + "ok": True, + "warnings": [], + } + + +def test_labeled_credential_assignment_keeps_its_own_warning() -> None: + text = "deploy used access_token=ghp_" + "A1b2C3d4E5f6G7h8I9j0K1l2M3n4" + + result = public_safety.scan_public_boundary_text(text) + + assert result["ok"] is False + assert result["warnings"] == ["credential assignment", "provider token"]