|
1 | 1 | from collections import OrderedDict |
| 2 | +from unittest.mock import patch |
2 | 3 |
|
3 | 4 | import pytest |
4 | 5 |
|
5 | | -from openhands.sdk.utils.command import sanitized_env |
| 6 | +from openhands.sdk.utils.command import execute_command, sanitized_env |
6 | 7 |
|
7 | 8 |
|
8 | 9 | def test_sanitized_env_returns_copy(): |
@@ -47,3 +48,81 @@ def test_sanitized_env_removes_ld_library_path_when_orig_empty(): |
47 | 48 | """When LD_LIBRARY_PATH_ORIG is empty, removes LD_LIBRARY_PATH.""" |
48 | 49 | env = {"LD_LIBRARY_PATH": "/pyinstaller", "LD_LIBRARY_PATH_ORIG": ""} |
49 | 50 | assert "LD_LIBRARY_PATH" not in sanitized_env(env) |
| 51 | + |
| 52 | + |
| 53 | +# --------------------------------------------------------------------------- |
| 54 | +# execute_command logging redaction |
| 55 | +# --------------------------------------------------------------------------- |
| 56 | + |
| 57 | + |
| 58 | +class TestExecuteCommandLoggingRedaction: |
| 59 | + """Tests for sensitive value redaction in execute_command logging.""" |
| 60 | + |
| 61 | + def test_logs_command_without_errors(self, caplog): |
| 62 | + """Command logging with redaction doesn't raise errors.""" |
| 63 | + with patch("subprocess.Popen") as mock_popen: |
| 64 | + mock_process = mock_popen.return_value |
| 65 | + mock_process.stdout = None |
| 66 | + mock_process.stderr = None |
| 67 | + |
| 68 | + cmd = ["docker", "run", "-e", "LMNR_PROJECT_API_KEY=secret123", "image"] |
| 69 | + |
| 70 | + try: |
| 71 | + execute_command(cmd) |
| 72 | + except RuntimeError: |
| 73 | + # Logging should happen even if subprocess fails |
| 74 | + pass |
| 75 | + |
| 76 | + # Command should be logged |
| 77 | + assert "docker" in caplog.text |
| 78 | + assert "run" in caplog.text |
| 79 | + assert "image" in caplog.text |
| 80 | + |
| 81 | + def test_redacts_api_key_from_string_command(self): |
| 82 | + """API keys in string commands are properly redacted.""" |
| 83 | + from openhands.sdk.utils.redact import redact_text_secrets |
| 84 | + |
| 85 | + # Test the redaction function directly |
| 86 | + # Valid Anthropic key format: sk-ant-api[2 digits]-[20+ chars] |
| 87 | + cmd_str = "curl -H 'Authorization: sk-ant-api00-abcd1234567890abcdefghijklmnop' https://api.anthropic.com" |
| 88 | + redacted = redact_text_secrets(cmd_str) |
| 89 | + |
| 90 | + # The secret should be redacted in the output of the function |
| 91 | + assert "sk-ant-api00-abcd1234567890abcdefghijklmnop" not in redacted |
| 92 | + assert "<redacted>" in redacted |
| 93 | + # Command structure should be preserved |
| 94 | + assert "curl" in redacted |
| 95 | + assert "https://api.anthropic.com" in redacted |
| 96 | + |
| 97 | + def test_redacts_key_value_env_format(self): |
| 98 | + """KEY=VALUE environment variable format is redacted.""" |
| 99 | + from openhands.sdk.utils.redact import redact_text_secrets |
| 100 | + |
| 101 | + cmd_str = "docker run -e api_key='secretvalue123456789' -e DEBUG=true image" |
| 102 | + redacted = redact_text_secrets(cmd_str) |
| 103 | + |
| 104 | + # api_key value should be redacted |
| 105 | + assert "secretvalue123456789" not in redacted |
| 106 | + # But non-sensitive DEBUG value should be present |
| 107 | + assert "DEBUG" in redacted |
| 108 | + # Command structure preserved |
| 109 | + assert "docker" in redacted |
| 110 | + |
| 111 | + def test_preserves_non_sensitive_args(self, caplog): |
| 112 | + """Non-sensitive arguments are preserved in logs.""" |
| 113 | + with patch("subprocess.Popen") as mock_popen: |
| 114 | + mock_process = mock_popen.return_value |
| 115 | + mock_process.stdout = None |
| 116 | + mock_process.stderr = None |
| 117 | + |
| 118 | + cmd = ["docker", "run", "-e", "DEBUG=true", "image:latest"] |
| 119 | + |
| 120 | + try: |
| 121 | + execute_command(cmd) |
| 122 | + except RuntimeError: |
| 123 | + pass |
| 124 | + |
| 125 | + # Non-sensitive values should be visible |
| 126 | + assert "DEBUG=true" in caplog.text |
| 127 | + assert "image:latest" in caplog.text |
| 128 | + assert "docker" in caplog.text |
0 commit comments