test(redact): add sensitive text redaction
- Introduce a new test suite for the `redact_sensitive_text` function, covering various sensitive data formats including API keys, tokens, and environment variables. - Ensure that sensitive information is properly masked in logs and outputs while non-sensitive data remains unchanged. - Add tests for different scenarios including JSON fields, authorization headers, and environment variable assignments. - Implement a redacting formatter for logging to enhance security during log output.
This commit is contained in:
173
tests/agent/test_redact.py
Normal file
173
tests/agent/test_redact.py
Normal file
@@ -0,0 +1,173 @@
|
||||
"""Tests for agent.redact -- secret masking in logs and output."""
|
||||
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
|
||||
from agent.redact import redact_sensitive_text, RedactingFormatter
|
||||
|
||||
|
||||
class TestKnownPrefixes:
|
||||
def test_openai_sk_key(self):
|
||||
text = "Using key sk-proj-abc123def456ghi789jkl012"
|
||||
result = redact_sensitive_text(text)
|
||||
assert "sk-pro" in result
|
||||
assert "abc123def456" not in result
|
||||
assert "..." in result
|
||||
|
||||
def test_openrouter_sk_key(self):
|
||||
text = "OPENROUTER_API_KEY=sk-or-v1-abcdefghijklmnopqrstuvwxyz1234567890"
|
||||
result = redact_sensitive_text(text)
|
||||
assert "abcdefghijklmnop" not in result
|
||||
|
||||
def test_github_pat_classic(self):
|
||||
result = redact_sensitive_text("token: ghp_abc123def456ghi789jkl")
|
||||
assert "abc123def456" not in result
|
||||
|
||||
def test_github_pat_fine_grained(self):
|
||||
result = redact_sensitive_text("github_pat_abc123def456ghi789jklmno")
|
||||
assert "abc123def456" not in result
|
||||
|
||||
def test_slack_token(self):
|
||||
token = "xoxb-" + "0" * 12 + "-" + "a" * 14
|
||||
result = redact_sensitive_text(token)
|
||||
assert "a" * 14 not in result
|
||||
|
||||
def test_google_api_key(self):
|
||||
result = redact_sensitive_text("AIzaSyB-abc123def456ghi789jklmno012345")
|
||||
assert "abc123def456" not in result
|
||||
|
||||
def test_perplexity_key(self):
|
||||
result = redact_sensitive_text("pplx-abcdef123456789012345")
|
||||
assert "abcdef12345" not in result
|
||||
|
||||
def test_fal_key(self):
|
||||
result = redact_sensitive_text("fal_abc123def456ghi789jkl")
|
||||
assert "abc123def456" not in result
|
||||
|
||||
def test_short_token_fully_masked(self):
|
||||
result = redact_sensitive_text("key=sk-short1234567")
|
||||
assert "***" in result
|
||||
|
||||
|
||||
class TestEnvAssignments:
|
||||
def test_export_api_key(self):
|
||||
text = "export OPENAI_API_KEY=sk-proj-abc123def456ghi789jkl012"
|
||||
result = redact_sensitive_text(text)
|
||||
assert "OPENAI_API_KEY=" in result
|
||||
assert "abc123def456" not in result
|
||||
|
||||
def test_quoted_value(self):
|
||||
text = 'MY_SECRET_TOKEN="supersecretvalue123456789"'
|
||||
result = redact_sensitive_text(text)
|
||||
assert "MY_SECRET_TOKEN=" in result
|
||||
assert "supersecretvalue" not in result
|
||||
|
||||
def test_non_secret_env_unchanged(self):
|
||||
text = "HOME=/home/user"
|
||||
result = redact_sensitive_text(text)
|
||||
assert result == text
|
||||
|
||||
def test_path_unchanged(self):
|
||||
text = "PATH=/usr/local/bin:/usr/bin"
|
||||
result = redact_sensitive_text(text)
|
||||
assert result == text
|
||||
|
||||
|
||||
class TestJsonFields:
|
||||
def test_json_api_key(self):
|
||||
text = '{"apiKey": "sk-proj-abc123def456ghi789jkl012"}'
|
||||
result = redact_sensitive_text(text)
|
||||
assert "abc123def456" not in result
|
||||
|
||||
def test_json_token(self):
|
||||
text = '{"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.longtoken.here"}'
|
||||
result = redact_sensitive_text(text)
|
||||
assert "eyJhbGciOiJSUzI1NiIs" not in result
|
||||
|
||||
def test_json_non_secret_unchanged(self):
|
||||
text = '{"name": "John", "model": "gpt-4"}'
|
||||
result = redact_sensitive_text(text)
|
||||
assert result == text
|
||||
|
||||
|
||||
class TestAuthHeaders:
|
||||
def test_bearer_token(self):
|
||||
text = "Authorization: Bearer sk-proj-abc123def456ghi789jkl012"
|
||||
result = redact_sensitive_text(text)
|
||||
assert "Authorization: Bearer" in result
|
||||
assert "abc123def456" not in result
|
||||
|
||||
def test_case_insensitive(self):
|
||||
text = "authorization: bearer mytoken123456789012345678"
|
||||
result = redact_sensitive_text(text)
|
||||
assert "mytoken12345" not in result
|
||||
|
||||
|
||||
class TestTelegramTokens:
|
||||
def test_bot_token(self):
|
||||
text = "bot123456789:ABCDEfghij-KLMNopqrst_UVWXyz12345"
|
||||
result = redact_sensitive_text(text)
|
||||
assert "ABCDEfghij" not in result
|
||||
assert "123456789:***" in result
|
||||
|
||||
def test_raw_token(self):
|
||||
text = "12345678901:ABCDEfghijKLMNopqrstUVWXyz1234567890"
|
||||
result = redact_sensitive_text(text)
|
||||
assert "ABCDEfghij" not in result
|
||||
|
||||
|
||||
class TestPassthrough:
|
||||
def test_empty_string(self):
|
||||
assert redact_sensitive_text("") == ""
|
||||
|
||||
def test_none_returns_none(self):
|
||||
assert redact_sensitive_text(None) is None
|
||||
|
||||
def test_normal_text_unchanged(self):
|
||||
text = "Hello world, this is a normal log message with no secrets."
|
||||
assert redact_sensitive_text(text) == text
|
||||
|
||||
def test_code_unchanged(self):
|
||||
text = "def main():\n print('hello')\n return 42"
|
||||
assert redact_sensitive_text(text) == text
|
||||
|
||||
def test_url_without_key_unchanged(self):
|
||||
text = "Connecting to https://api.openai.com/v1/chat/completions"
|
||||
assert redact_sensitive_text(text) == text
|
||||
|
||||
|
||||
class TestRedactingFormatter:
|
||||
def test_formats_and_redacts(self):
|
||||
formatter = RedactingFormatter("%(message)s")
|
||||
record = logging.LogRecord(
|
||||
name="test", level=logging.INFO, pathname="", lineno=0,
|
||||
msg="Key is sk-proj-abc123def456ghi789jkl012",
|
||||
args=(), exc_info=None,
|
||||
)
|
||||
result = formatter.format(record)
|
||||
assert "abc123def456" not in result
|
||||
assert "sk-pro" in result
|
||||
|
||||
|
||||
class TestPrintenvSimulation:
|
||||
"""Simulate what happens when the agent runs `env` or `printenv`."""
|
||||
|
||||
def test_full_env_dump(self):
|
||||
env_dump = """HOME=/home/user
|
||||
PATH=/usr/local/bin:/usr/bin
|
||||
OPENAI_API_KEY=sk-proj-abc123def456ghi789jkl012mno345
|
||||
OPENROUTER_API_KEY=sk-or-v1-reallyLongSecretKeyValue12345678
|
||||
FIRECRAWL_API_KEY=fc-shortkey123456789012
|
||||
TELEGRAM_BOT_TOKEN=bot987654321:ABCDEfghij-KLMNopqrst_UVWXyz12345
|
||||
SHELL=/bin/bash
|
||||
USER=teknium"""
|
||||
result = redact_sensitive_text(env_dump)
|
||||
# Secrets should be masked
|
||||
assert "abc123def456" not in result
|
||||
assert "reallyLongSecretKey" not in result
|
||||
assert "ABCDEfghij" not in result
|
||||
# Non-secrets should survive
|
||||
assert "HOME=/home/user" in result
|
||||
assert "SHELL=/bin/bash" in result
|
||||
assert "USER=teknium" in result
|
||||
@@ -1037,8 +1037,12 @@ def terminal_tool(
|
||||
)
|
||||
output = output[:head_chars] + truncated_notice + output[-tail_chars:]
|
||||
|
||||
# Redact secrets from command output (catches env/printenv leaking keys)
|
||||
from agent.redact import redact_sensitive_text
|
||||
output = redact_sensitive_text(output.strip()) if output else ""
|
||||
|
||||
return json.dumps({
|
||||
"output": output.strip() if output else "",
|
||||
"output": output,
|
||||
"exit_code": returncode,
|
||||
"error": None
|
||||
}, ensure_ascii=False)
|
||||
|
||||
Reference in New Issue
Block a user