From aedb14de68ad71ead81c1644a4e7961cc1489a7b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 00:53:13 +0900 Subject: [PATCH 1/2] perf(redaction): skip invalid key rescans --- scripts/ci/redact_sensitive_log.py | 27 +++++++++++++++++++--- tests/test_opencode_security_boundaries.py | 24 +++++++++++++++++++ 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/scripts/ci/redact_sensitive_log.py b/scripts/ci/redact_sensitive_log.py index 16e89f264..fe9b77ab0 100644 --- a/scripts/ci/redact_sensitive_log.py +++ b/scripts/ci/redact_sensitive_log.py @@ -101,11 +101,32 @@ def _redact_assignments(text: str) -> str: cursor = 0 last_append = 0 while cursor < len(text): - match = _consume_sensitive_assignment(text, cursor) + candidate = SENSITIVE_KEY_RE.search(text, cursor) + if candidate is None: + break + + key_start = candidate.start() + while key_start > cursor and text[key_start - 1] in KEY_CHARS: + key_start -= 1 + key_end = candidate.end() + while key_end < len(text) and text[key_end] in KEY_CHARS: + key_end += 1 + + assignment_start = key_start + if assignment_start > cursor and text[assignment_start - 1] in "\"'": + assignment_start -= 1 + match = _consume_sensitive_assignment(text, assignment_start) + if match is None and assignment_start != key_start: + # Preserve the historical recovery for an unmatched opening quote. + assignment_start = key_start + match = _consume_sensitive_assignment(text, assignment_start) if match is None: - cursor += 1 + # Every sensitive-looking substring inside this key has the same + # assignment boundary, so evaluating the key again cannot succeed. + cursor = key_end continue - output.append(text[last_append:cursor]) + + output.append(text[last_append:assignment_start]) replacement, cursor = match output.append(replacement) last_append = cursor diff --git a/tests/test_opencode_security_boundaries.py b/tests/test_opencode_security_boundaries.py index 1b22706fa..0b59bd0fd 100644 --- a/tests/test_opencode_security_boundaries.py +++ b/tests/test_opencode_security_boundaries.py @@ -47,12 +47,14 @@ def test_sensitive_log_redaction_preserves_normal_diagnostics() -> None: """Ordinary failure reasons remain visible while credentials are removed.""" source = ( "build failed for package requests==2.31.0\n" + "token expired at: 2026-01-01\n" "Authorization: Bearer abc.def.ghi\n" "SERVICE_TOKEN=opaque_service_value_123456789\n" ) cleaned = redactor.redact_text(source) assert "build failed for package requests==2.31.0" in cleaned + assert "token expired at: 2026-01-01" in cleaned assert "abc.def.ghi" not in cleaned assert "opaque_service_value_123456789" not in cleaned assert cleaned.count(redactor.REDACTED) >= 2 @@ -84,11 +86,33 @@ def test_sensitive_log_redaction_assignment_parser_edges_remain_auditable() -> N for source, expected in cases.items(): assert redactor.redact_text(source) == expected + assert redactor._consume_sensitive_assignment("=password=value", 0) is None + assert redactor._consume_sensitive_assignment("visible=value", 0) is None assert redactor.redact_text('token="safe\\"inside" trailing') == ( f"token={redactor.REDACTED} trailing" ) +def test_sensitive_log_redaction_evaluates_each_invalid_key_once( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Repeated sensitive substrings in one invalid key do not cause rescans.""" + calls = 0 + consume = redactor._consume_sensitive_assignment + + def counted(text: str, start: int) -> tuple[str, int] | None: + """Count candidate parses while delegating to the real parser.""" + nonlocal calls + calls += 1 + return consume(text, start) + + monkeypatch.setattr(redactor, "_consume_sensitive_assignment", counted) + source = "token" * 2_000 + " without an assignment" + + assert redactor.redact_text(source) == source + assert calls == 1 + + def test_sensitive_log_redaction_scrubs_provider_token_shapes() -> None: """Provider-shaped tokens are removed even when they are not key/value assignments.""" source = "\n".join( From a32e394af3effca5c93a759912ad9f112a50a079 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 01:04:40 +0900 Subject: [PATCH 2/2] fix(redaction): preserve digit-prefixed secret keys --- scripts/ci/redact_sensitive_log.py | 2 ++ tests/test_opencode_security_boundaries.py | 1 + 2 files changed, 3 insertions(+) diff --git a/scripts/ci/redact_sensitive_log.py b/scripts/ci/redact_sensitive_log.py index fe9b77ab0..bc93e1a13 100644 --- a/scripts/ci/redact_sensitive_log.py +++ b/scripts/ci/redact_sensitive_log.py @@ -111,6 +111,8 @@ def _redact_assignments(text: str) -> str: key_end = candidate.end() while key_end < len(text) and text[key_end] in KEY_CHARS: key_end += 1 + while key_start < candidate.start() and text[key_start].isdigit(): + key_start += 1 assignment_start = key_start if assignment_start > cursor and text[assignment_start - 1] in "\"'": diff --git a/tests/test_opencode_security_boundaries.py b/tests/test_opencode_security_boundaries.py index 0b59bd0fd..5b0224e74 100644 --- a/tests/test_opencode_security_boundaries.py +++ b/tests/test_opencode_security_boundaries.py @@ -79,6 +79,7 @@ def test_sensitive_log_redaction_assignment_parser_edges_remain_auditable() -> N "token=": "token=", "token=,": "token=,", "9safe=value": "9safe=value", + "9password=value": f"9password={redactor.REDACTED}", '"token: value': f'"token: {redactor.REDACTED}', "token: ": "token: ", }