Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 26 additions & 3 deletions scripts/ci/redact_sensitive_log.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,34 @@ 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
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 "\"'":
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
Comment thread
seonghobae marked this conversation as resolved.
continue
output.append(text[last_append:cursor])

output.append(text[last_append:assignment_start])
replacement, cursor = match
output.append(replacement)
last_append = cursor
Comment thread
seonghobae marked this conversation as resolved.
Comment thread
seonghobae marked this conversation as resolved.
Expand Down
25 changes: 25 additions & 0 deletions tests/test_opencode_security_boundaries.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -77,18 +79,41 @@ 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: ",
}

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(
Expand Down
Loading