d815339c52
Implement centralized redaction utility that masks API keys, tokens, and credentials across CLI output, structlog logs, and error messages. - Add shared/redaction.py with pattern-based secret detection (sk-*, sk-ant-*, tok_*, Bearer tokens), sensitive key name detection, database URL masking, custom pattern registration, and thread-safe global show_secrets flag - Add config/logging.py with structlog configuration integrating the secrets_masking_processor into the processor chain - Add --show-secrets global CLI option to reveal secrets when needed - Redact error details in main.py, project.py, and auto_debug.py error handlers before printing - Wrap format_output() in formatting.py with automatic dict redaction - Add show_secrets field and safe __repr__ to Settings model - Add 43-scenario Behave feature (features/security_secrets.feature) - Add 10 Robot Framework smoke tests (robot/security_secrets.robot) - Add ASV benchmarks (benchmarks/security_secrets_bench.py) - Add reference docs (docs/reference/secrets_handling.md)
189 lines
7.0 KiB
Gherkin
189 lines
7.0 KiB
Gherkin
Feature: SEC5 - Secrets masking and validation
|
|
As a security-conscious developer
|
|
I want secrets automatically masked in logs and CLI output
|
|
So that API keys and tokens are never leaked
|
|
|
|
# --- Core redaction utility ---
|
|
|
|
Scenario: Redact OpenAI API key pattern
|
|
Given a string containing "sk-proj-abc123def456ghi789"
|
|
When I redact the string
|
|
Then the redacted value should be "***REDACTED***"
|
|
|
|
Scenario: Redact Anthropic API key pattern
|
|
Given a string containing "sk-ant-api03-xyzzy1234567890abcdef"
|
|
When I redact the string
|
|
Then the redacted value should be "***REDACTED***"
|
|
|
|
Scenario: Redact token pattern
|
|
Given a string containing "tok_01HXYZ1234567890ABCDEF"
|
|
When I redact the string
|
|
Then the redacted value should be "***REDACTED***"
|
|
|
|
Scenario: Redact generic bearer token
|
|
Given a string containing "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJz"
|
|
When I redact the string
|
|
Then the redacted value should contain "***REDACTED***"
|
|
|
|
Scenario: Non-secret strings are not redacted
|
|
Given a string containing "hello world this is normal text"
|
|
When I redact the string
|
|
Then the redacted value should be "hello world this is normal text"
|
|
|
|
Scenario: Mixed text with embedded secret is partially redacted
|
|
Given a string containing "My key is sk-proj-abc123def456ghi789 and name is Bob"
|
|
When I redact the string
|
|
Then the redacted value should contain "***REDACTED***"
|
|
And the redacted value should contain "My key is"
|
|
And the redacted value should contain "and name is Bob"
|
|
|
|
Scenario: Empty string is returned unchanged
|
|
Given a string containing ""
|
|
When I redact the string
|
|
Then the redacted value should be empty
|
|
|
|
# --- Dict redaction ---
|
|
|
|
Scenario: Dict with sensitive key names are masked
|
|
Given a dict with key "api_key" and value "sk-proj-abc123"
|
|
When I redact the dict
|
|
Then the key "api_key" should have value "***REDACTED***"
|
|
|
|
Scenario: Dict with password key is masked
|
|
Given a dict with key "password" and value "mysecretpass"
|
|
When I redact the dict
|
|
Then the key "password" should have value "***REDACTED***"
|
|
|
|
Scenario: Dict with secret key is masked
|
|
Given a dict with key "secret" and value "topsecretvalue"
|
|
When I redact the dict
|
|
Then the key "secret" should have value "***REDACTED***"
|
|
|
|
Scenario: Dict with token key is masked
|
|
Given a dict with key "token" and value "sometoken123"
|
|
When I redact the dict
|
|
Then the key "token" should have value "***REDACTED***"
|
|
|
|
Scenario: Dict with credential key is masked
|
|
Given a dict with key "credential" and value "cred-value"
|
|
When I redact the dict
|
|
Then the key "credential" should have value "***REDACTED***"
|
|
|
|
Scenario: Dict with non-sensitive key is not masked
|
|
Given a dict with key "username" and value "bob"
|
|
When I redact the dict
|
|
Then the key "username" should have value "bob"
|
|
|
|
Scenario: Nested dict values are redacted recursively
|
|
Given a nested dict with inner key "api_key" and value "sk-proj-nested"
|
|
When I redact the dict
|
|
Then the nested key "api_key" should have value "***REDACTED***"
|
|
|
|
Scenario: Dict with pattern-matched value is masked regardless of key name
|
|
Given a dict with key "config_value" and value "sk-ant-api03-somekey123"
|
|
When I redact the dict
|
|
Then the key "config_value" should have value "***REDACTED***"
|
|
|
|
Scenario: Dict redaction respects show_secrets flag
|
|
Given a dict with key "api_key" and value "sk-proj-abc123"
|
|
When I redact the dict with show_secrets enabled
|
|
Then the key "api_key" should have value "sk-proj-abc123"
|
|
|
|
# --- Sensitive key detection ---
|
|
|
|
Scenario Outline: Sensitive key names are detected
|
|
When I check if "<key>" is a sensitive key
|
|
Then the sensitivity check should be true
|
|
|
|
Examples:
|
|
| key |
|
|
| api_key |
|
|
| API_KEY |
|
|
| password |
|
|
| secret |
|
|
| token |
|
|
| credential |
|
|
| openai_api_key |
|
|
| ANTHROPIC_API_KEY |
|
|
| access_token |
|
|
| auth_token |
|
|
| private_key |
|
|
|
|
Scenario Outline: Non-sensitive key names are not flagged
|
|
When I check if "<key>" is a sensitive key
|
|
Then the sensitivity check should be false
|
|
|
|
Examples:
|
|
| key |
|
|
| username |
|
|
| email |
|
|
| name |
|
|
| description |
|
|
| project_id |
|
|
| token_count |
|
|
|
|
# --- Database URL masking ---
|
|
|
|
Scenario: SQLite URL is not masked
|
|
Given a database URL "sqlite:///path/to/db.sqlite"
|
|
When I mask the database URL
|
|
Then the masked URL should be "sqlite:///path/to/db.sqlite"
|
|
|
|
Scenario: PostgreSQL URL with credentials is masked
|
|
Given a database URL "postgresql://admin:s3cret@localhost:5432/mydb"
|
|
When I mask the database URL
|
|
Then the masked URL should contain "***"
|
|
And the masked URL should contain "localhost"
|
|
And the masked URL should not contain "s3cret"
|
|
|
|
# --- Global show_secrets flag ---
|
|
|
|
Scenario: Global show_secrets defaults to false
|
|
When I check the global show_secrets flag
|
|
Then the flag should be false
|
|
|
|
Scenario: Global show_secrets can be toggled
|
|
When I set the global show_secrets flag to true
|
|
Then the flag should be true
|
|
When I set the global show_secrets flag to false
|
|
Then the flag should be false
|
|
|
|
# --- structlog processor ---
|
|
|
|
Scenario: Structlog masking processor redacts event dict values
|
|
Given a structlog event dict with key "api_key" and value "sk-proj-secret123"
|
|
When the masking processor is applied
|
|
Then the event dict key "api_key" should be "***REDACTED***"
|
|
|
|
Scenario: Structlog masking processor redacts event message
|
|
Given a structlog event dict with event "Failed with key sk-ant-api03-xyz"
|
|
When the masking processor is applied
|
|
Then the event message should contain "***REDACTED***"
|
|
And the event message should not contain "sk-ant-api03-xyz"
|
|
|
|
Scenario: Structlog masking processor passes non-secret events
|
|
Given a structlog event dict with event "User logged in successfully"
|
|
When the masking processor is applied
|
|
Then the event message should be "User logged in successfully"
|
|
|
|
# --- CLI error detail redaction ---
|
|
|
|
Scenario: Error details with secret values are redacted
|
|
Given error details with key "api_key" and value "sk-proj-leaked"
|
|
When I redact the error details for CLI display
|
|
Then the redacted details key "api_key" should be "***REDACTED***"
|
|
|
|
Scenario: Error details with non-secret values are preserved
|
|
Given error details with key "plan_id" and value "01HXR123"
|
|
When I redact the error details for CLI display
|
|
Then the redacted details key "plan_id" should be "01HXR123"
|
|
|
|
# --- Secret pattern registration ---
|
|
|
|
Scenario: Custom secret pattern can be registered
|
|
Given the default secret patterns
|
|
When I register a custom pattern "custom_[a-z0-9]{16}"
|
|
And I redact the string "my custom_abcdef1234567890 key"
|
|
Then the redacted value should contain "***REDACTED***"
|
|
And the redacted value should not contain "custom_abcdef1234567890"
|