fix(hide-secrets): scan the first token of shell-style assignments regardless of what follows

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
yucheng 2026-09-11 21:46:22 +00:00
parent 3573154827
commit 04f21ba8ae
2 changed files with 26 additions and 9 deletions

View file

@ -451,9 +451,9 @@ _CONFIG_SECTION: Final = "litellm-prompt"
_ASSIGNMENT_LINE: Final = re.compile(r"[^\s\[#;:=][^:=]*[:=]")
_SHELL_OPERATORS: Final = ";&|"
_SHELL_ASSIGNMENT: Final = re.compile(r"(?P<key>[^\s\[#;:=](?:[^:=]*[^\s:=])?)=(?P<value>\S+)")
_SHELL_TRAILER: Final = re.compile(rf"\\|[{_SHELL_OPERATORS}]+|#.*|\S+=\S*")
_SHELL_OPERATORS: Final = ";&|"
_SCAN_SUFFIX: Final = ".py"
@ -491,6 +491,9 @@ def _classify_line(state: tuple[bool, str | None], numbered: tuple[int, str]) ->
stripped: Final = line.strip()
if not stripped or stripped[0] in "#;":
return open_option, None
shell_assignment: Final = _SHELL_ASSIGNMENT.match(stripped)
if shell_assignment is not None:
return True, f"{shell_assignment['key']}_{number}={shell_assignment['value']}"
assignment: Final = _ASSIGNMENT_LINE.match(stripped)
if assignment is not None:
return True, f"{assignment.group()[:-1].strip()}_{number}{stripped[assignment.end() - 1 :]}"
@ -505,14 +508,10 @@ def _parseable_lines(text: str) -> Iterator[str]:
def _lone_value(line: str) -> str | None:
tokens: Final = line.split(maxsplit=2)
if not tokens or '"' in tokens[0]:
tokens: Final = line.split()
if not tokens or '"' in tokens[0] or (len(tokens) > 1 and not tokens[1].startswith("#")):
return None
value: Final = tokens[0].rstrip(_SHELL_OPERATORS)
trailer: Final = tokens[1] if len(tokens) > 1 else ""
if value != tokens[0] or not trailer or _SHELL_TRAILER.fullmatch(trailer) is not None:
return value
return None
return tokens[0].rstrip(_SHELL_OPERATORS)
def _quoted_assignments(text: str) -> tuple[str, ...]:

View file

@ -120,6 +120,9 @@ def test_scan_message_preserves_quoted_benign_identifiers():
("export DB_PASSWORD=Zx4Kp9Lm2Qr7Ns3Vt DB_HOST=db.internal", "Zx4Kp9Lm2Qr7Ns3Vt"),
("DB_PASSWORD=Zx4Kp9Lm2Qr7Ns3Vt; systemctl restart app", "Zx4Kp9Lm2Qr7Ns3Vt"),
("DB_PASSWORD=Zx4Kp9Lm2Qr7Ns3Vt | tee creds.txt", "Zx4Kp9Lm2Qr7Ns3Vt"),
("DB_PASSWORD=Zx4Kp9Lm2Qr7Ns3Vt > setup.log", "Zx4Kp9Lm2Qr7Ns3Vt"),
("docker run -e DB_PASSWORD=Zx4Kp9Lm2Qr7Ns3Vt --name app postgres", "Zx4Kp9Lm2Qr7Ns3Vt"),
("password=correcthorsebattery please", "correcthorsebattery"),
],
ids=[
"env-password",
@ -153,6 +156,9 @@ def test_scan_message_preserves_quoted_benign_identifiers():
"second-assignment-after-the-value",
"semicolon-after-the-value",
"pipe-after-the-value",
"redirect-after-the-value",
"docker-flag-after-the-value",
"prose-after-a-shell-assignment",
],
)
def test_scan_message_redacts_credentials_assigned_to_credential_keys(content, secret):
@ -171,6 +177,16 @@ def test_scan_message_redacts_only_the_first_token_of_a_shell_assignment():
)
@pytest.mark.parametrize("operator", [";", "&&", "|"])
def test_scan_message_keeps_a_shell_operator_glued_to_the_value(operator):
guardrail = _guardrail()
assert (
guardrail.redact_text(f"DB_PASSWORD=Zx4Kp9Lm2Qr7Ns3Vt{operator} systemctl restart app")
== f"DB_PASSWORD=[REDACTED]{operator} systemctl restart app"
)
def test_scan_message_closes_a_yaml_block_at_the_next_unindented_line():
guardrail = _guardrail()
content = "api_key: >\n aB3dE6gH9jK2mN5p\nSteps\n Rotate-Before-Friday please"
@ -244,6 +260,7 @@ def test_scan_message_redacts_every_credential_on_one_line():
"password_hint: your usual one followed by Ticket-LIT7049-Suffix",
"Translate this recipe note into French:\nsecret_sauce: Worcestershire sauce",
"api_key = Massachusetts (the state, not a key)",
"secret_sauce:Worcestershire sauce",
],
ids=[
"prose-password",
@ -300,6 +317,7 @@ def test_scan_message_redacts_every_credential_on_one_line():
"sentence-holding-a-later-mixed-case-token",
"capitalized-word-starting-a-phrase",
"capitalized-word-before-a-parenthetical",
"yaml-scalar-without-a-space-after-the-colon",
],
)
def test_scan_message_keeps_benign_values(content):