Feature: Consolidated Security Combined scenarios from: security_eval, security_exceptions, security_readonly, security_secrets, security_templates # ============================================================ # Originally from: security_eval.feature # Feature: SEC1 - eval/exec removal security # ============================================================ Scenario: Config with code injection attempt is rejected Given a SimpleToolAgent configured with a code injection payload When I attempt to process content through the injected agent Then the agent should reject the code block with a routing error And the error message should mention code blocks are not supported Scenario: Malicious transform expression does not execute Given the CleverAgents reactive system is available When I configure a transform with a malicious eval expression Then the transform should be rejected with a routing error And the error message should mention unknown transform Scenario: Valid config still works after eval removal Given a SimpleToolAgent configured with a named operation "uppercase" When I process "hello" through the named operation agent Then the named operation result should be "HELLO" Scenario: Named transform from registry works after eval removal Given the CleverAgents reactive system is available When I configure a transform with a registered named function "uppercase" Then the named transform should produce the uppercased result Scenario: Code block with import attempt is rejected Given a SimpleToolAgent configured with an import injection payload When I attempt to process content through the injected agent Then the agent should reject the code block with a routing error Scenario: Eval expression mimicking registry name is still rejected Given the CleverAgents reactive system is available When I configure a transform with expression "lambda x: __import__('os').system('rm -rf /')" Then the transform should be rejected with a routing error Scenario: Custom registered operation works Given a SimpleToolAgent with a custom registered operation "reverse" When I process "abcde" through the custom operation agent Then the custom operation result should be "edcba" Scenario: Custom registered transform works Given the CleverAgents reactive system is available And a custom transform "double" is registered When I configure a transform with a registered named function "double" Then the custom transform should produce the doubled result Scenario: Config scanner detects eval in content Given config content containing "value: eval('hack')" When I scan the config content for security violations Then the scanner should report at least 1 violation And the scanner should report a violation with token "eval(" And the violation severity should be "CRITICAL" Scenario: Config scanner detects exec in content Given config content containing "action: exec('import os')" When I scan the config content for security violations Then the scanner should report at least 1 violation And the scanner should report a violation with token "exec(" Scenario: Config scanner passes clean content Given config content containing "name: safe-project" When I scan the config content for security violations Then the scanner should report 0 violations Scenario: Config scanner reports correct line numbers Given multiline config content with a violation on line 3 When I scan the config content for security violations Then the scanner should report a violation on line 3 Scenario: Config safety validation raises on dangerous content Given config content containing "run: exec('bad')" When I validate the config content for safety Then a ConfigurationError should be raised Scenario: Config safety validation passes clean content Given config content containing "name: safe-project" When I validate the config content for safety Then no configuration error should be raised Scenario: Config scanner skips comment lines Given config content containing "# eval('this is just a comment')" When I scan the config content for security violations Then the scanner should report 0 violations Scenario: Config scanner skips INI-style semicolon comment lines Given config content containing "; eval('this is an INI comment')" When I scan the config content for security violations Then the scanner should report 0 violations Scenario: Config scanner skips inline comments after hash Given config content containing "name: safe-project # eval('just a note')" When I scan the config content for security violations Then the scanner should report 0 violations Scenario: Config scanner preserves hash inside quoted strings Given config content containing "value: \"eval('inside quotes') # not a comment\"" When I scan the config content for security violations Then the scanner should report at least 1 violation And the scanner should report a violation with token "eval(" Scenario: Config scanner detects multiple violation types Given config content with eval and subprocess patterns When I scan the config content for security violations Then the scanner should report at least 2 violations Scenario: Config scanner scans multiple files Given two temporary config files with violations When I scan the config files for security violations Then the scanner should report at least 2 violations Scenario: Config scanner CLI shows usage when no args given When I run the config scanner CLI with no arguments Then the scanner CLI should return exit code 2 Scenario: Config scanner CLI reports violations found When I run the config scanner CLI with a dirty temp file Then the scanner CLI should return exit code 1 And the scanner CLI output should contain "violation" Scenario: Config scanner CLI reports clean file When I run the config scanner CLI with a clean temp file Then the scanner CLI should return exit code 0 Scenario: Config scanner scan_file raises for missing file When I scan a non-existent file path Then a scanner FileNotFoundError should be raised Scenario: Config scanner scan_file handles non-UTF-8 files gracefully Given a temporary config file with non-UTF-8 encoding When I scan the non-UTF-8 file for violations Then the scan should complete without crashing # ============================================================ # Originally from: security_exceptions.feature # Feature: Explicit exception handling and error classification # ============================================================ Scenario: Classify a ValidationError as 422 Given a security exceptions test environment Given a ValidationError with message "field is invalid" When I classify the security exception Then the error code should be 422 And the error code name should be "VALIDATION_FAILED" And the error handling category should be "CLIENT" Scenario: Classify a ResourceNotFoundError as 404 Given a security exceptions test environment Given a ResourceNotFoundError for resource "plan" with id "01PLAN001" When I classify the security exception Then the error code should be 404 And the error code name should be "NOT_FOUND" Scenario: Classify an AuthenticationError as 401 Given a security exceptions test environment Given an AuthenticationError with message "invalid token" When I classify the security exception Then the error code should be 401 And the error code name should be "UNAUTHORIZED" Scenario: Classify an AuthorizationError as 403 Given a security exceptions test environment Given an AuthorizationError with message "access denied" When I classify the security exception Then the error code should be 403 And the error code name should be "FORBIDDEN" Scenario: Classify a RateLimitError as 429 Given a security exceptions test environment Given a RateLimitError with message "slow down" When I classify the security exception Then the error code should be 429 And the error code name should be "RATE_LIMITED" Scenario: Classify a DatabaseError as 522 Given a security exceptions test environment Given a DatabaseError with message "connection lost" When I classify the security exception Then the error code should be 522 And the error handling category should be "SERVER" Scenario: Classify a ConfigurationError as 525 Given a security exceptions test environment Given a ConfigurationError with message "missing key" When I classify the security exception Then the error code should be 525 Scenario: Classify a NetworkError as 524 Given a security exceptions test environment Given a NetworkError with message "timeout" When I classify the security exception Then the error code should be 524 Scenario: Classify a FileSystemError as 523 Given a security exceptions test environment Given a FileSystemError with message "permission denied" When I classify the security exception Then the error code should be 523 Scenario: Classify a ProviderError as 520 Given a security exceptions test environment Given a ProviderError with message "model failed" When I classify the security exception Then the error code should be 520 Scenario: Classify a TokenLimitExceededError as 526 Given a security exceptions test environment Given a TokenLimitExceededError with message "context too long" When I classify the security exception Then the error code should be 526 Scenario: Classify a BusinessRuleViolation as 400 Given a security exceptions test environment Given a BusinessRuleViolation with message "invalid transition" When I classify the security exception Then the error code should be 400 Scenario: Classify a PlanError as 400 Given a security exceptions test environment Given a PlanError with message "plan stuck" When I classify the security exception Then the error code should be 400 Scenario: Classify an unknown Exception as 500 Given a security exceptions test environment Given a bare Exception with message "something broke" When I classify the security exception Then the error code should be 500 And the error handling category should be "SERVER" Scenario: Classify an ExecutionError as 521 Given a security exceptions test environment Given an ExecutionError with message "agent failed" When I classify the security exception Then the error code should be 521 Scenario: Classify a StreamRoutingError as 528 Given a security exceptions test environment Given a StreamRoutingError with message "routing failed" When I classify the security exception Then the error code should be 528 Scenario: Classify a ModelNotAvailableError as 527 Given a security exceptions test environment Given a ModelNotAvailableError with message "deprecated" When I classify the security exception Then the error code should be 527 Scenario: Classify a ResourceConflictError as 409 Given a security exceptions test environment Given a ResourceConflictError with message "already exists" When I classify the security exception Then the error code should be 409 Scenario: Classify a MissingConfigurationError as 525 Given a security exceptions test environment Given a MissingConfigurationError with message "key not set" When I classify the security exception Then the error code should be 525 Scenario: Classify an ExternalServiceError as 503 Given a security exceptions test environment Given an ExternalServiceError with message "service down" When I classify the security exception Then the error code should be 503 # -- Secret redaction ----------------------------------------------------- Scenario: Redact API key from error details Given a security exceptions test environment Given redaction test details with key "api_key" and value "sk-abc123secret" When I redact the error details Then the redacted detail "api_key" should be "***REDACTED***" Scenario: Redact password from error details Given a security exceptions test environment Given redaction test details with key "password" and value "my-secret-pass" When I redact the error details Then the redacted detail "password" should be "***REDACTED***" Scenario: Redact OpenAI key pattern from string value Given a security exceptions test environment Given redaction test details with key "message" and value "Failed with key sk-abcdefghijklmnopqrstuvwxyz" When I redact the error details Then the redacted detail "message" should contain "***REDACTED***" And the redacted detail "message" should not contain "sk-abcdefghijklmnopqrstuvwxyz" Scenario: Redact JWT token pattern from string value Given a security exceptions test environment Given redaction test details with key "log" and value "auth eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 failed" When I redact the error details Then the redacted detail "log" should contain "***REDACTED***" Scenario: Redact nested dict values Given a security exceptions test environment Given redaction test details with nested key "config" containing secret_key "abc123" When I redact the error details Then the redacted nested detail "config" key "secret_key" should be "***REDACTED***" Scenario: Non-sensitive values pass through unchanged Given a security exceptions test environment Given redaction test details with key "plan_id" and value "01PLAN001" When I redact the error details Then the redacted detail "plan_id" should be "01PLAN001" Scenario: Redact GitHub PAT pattern from string value Given a security exceptions test environment Given redaction test details with key "msg" and value "token ghp_abcdefghijklmnopqrstuvwxyz0123456 expired" When I redact the error details Then the redacted detail "msg" should contain "***REDACTED***" Scenario: Redact value with redact_value function Given a security exceptions test environment Given a redaction test string value "key=sk-abcdefghijklmnopqrstuvwxyz" When I redact the string value Then the redacted string should contain "***REDACTED***" # -- Wrapping unexpected exceptions ---------------------------------------- Scenario: Wrap a bare Exception into CleverAgentsError Given a security exceptions test environment Given a bare Exception with message "internal crash" When I wrap the unexpected exception Then the wrapped error should be a CleverAgentsError And the wrapped error message should be "An unexpected error occurred" And the wrapped error details should have key "exception_type" Scenario: Wrap preserves CleverAgentsError as-is Given a security exceptions test environment Given a CleverAgentsError with message "known issue" When I wrap the unexpected exception Then the wrapped error message should be "known issue" Scenario: Wrap merges context into CleverAgentsError details Given a security exceptions test environment Given a CleverAgentsError with message "known issue" And a wrapping context with key "plan_id" and value "01PLAN001" When I wrap the unexpected exception Then the wrapped error details should have key "plan_id" Scenario: Wrap with custom safe message Given a security exceptions test environment Given a bare Exception with message "segfault" When I wrap the unexpected exception with message "Operation failed" Then the wrapped error message should be "Operation failed" Scenario: Wrap redacts secrets in context Given a security exceptions test environment Given a bare Exception with message "oops" And a wrapping context with key "api_key" and value "sk-secret123456789012345" When I wrap the unexpected exception Then the wrapped error details key "api_key" should be "***REDACTED***" # -- CLI formatting ------------------------------------------------------- Scenario: Format error for CLI output Given a security exceptions test environment Given a ValidationError with message "name is required" When I classify the security exception And I format the error for CLI Then the error CLI output should contain "422" And the error CLI output should contain "VALIDATION_FAILED" And the error CLI output should contain "name is required" Scenario: Format error with details for CLI output Given a security exceptions test environment Given a detailed CleverAgentsError "db fail" with detail "table" as "plans" When I classify the security exception And I format the error for CLI Then the error CLI output should contain "table: plans" # -- Secret in message (S1) ------------------------------------------------ Scenario: classify_error redacts secrets embedded in exception message Given a security exceptions test environment Given a CleverAgentsError with message "key=sk-abcdefghijklmnopqrstuvwxyz leaked" When I classify the security exception Then the error info message should contain "***REDACTED***" And the error info message should not contain "sk-abcdefghijklmnopqrstuvwxyz" Scenario: Format CLI output redacts secrets from message Given a security exceptions test environment Given a CleverAgentsError with message "tok sk-abcdefghijklmnopqrstuvwxyz end" When I classify the security exception And I format the error for CLI Then the error CLI output should contain "***REDACTED***" And the error CLI output should not contain "sk-abcdefghijklmnopqrstuvwxyz" # -- List value redaction (B2/T1) ------------------------------------------ Scenario: Redact secrets inside list values Given a security exceptions test environment Given redaction test details with key "keys" and list values "sk-abcdefghijklmnopqrstuvwxyz" and "normal" When I redact the error details Then the redacted detail "keys" should be a list And the redacted list "keys" should contain "***REDACTED***" And the redacted list "keys" should not contain "sk-abcdefghijklmnopqrstuvwxyz" # -- Non-string non-dict passthrough (T4) ---------------------------------- Scenario: Integer values pass through redaction unchanged Given a security exceptions test environment Given redaction test details with key "count" and integer value 42 When I redact the error details Then the redacted detail "count" should be integer 42 Scenario: Boolean values pass through redaction unchanged Given a security exceptions test environment Given redaction test details with key "enabled" and boolean value true When I redact the error details Then the redacted detail "enabled" should be boolean true # -- Non-mutation of original details (B1) --------------------------------- Scenario: Wrap does not mutate the original exception details dict Given a security exceptions test environment Given a tracked CleverAgentsError "original" with details key "a" value "1" And a wrapping context with key "b" and value "2" When I wrap the unexpected exception Then the original exception details reference should not have key "b" And the wrapped error details should have key "b" # ============================================================ # Originally from: security_readonly.feature # Feature: Read-only action enforcement # ============================================================ Scenario: Read-only plan blocks a write-capable tool with tool name in error Given the read-only enforcement modules are available Given a tool "builtin/file-write" with writes=True and read_only=False And a tool execution context with plan_id "plan-ro-1" and read_only True When I enforce capabilities on the tool Then a ToolAccessDeniedError should be raised for the tool And the access-denied message should contain "builtin/file-write" And the access-denied message should contain "read-only" Scenario: Read-only plan allows a read-only tool Given the read-only enforcement modules are available Given a tool "builtin/search" with writes=False and read_only=True And a tool execution context with plan_id "plan-ro-2" and read_only True When I enforce capabilities on the tool Then no enforcement error should be raised Scenario: Non-read-only plan allows a write-capable tool Given the read-only enforcement modules are available Given a tool "builtin/file-write" with writes=True and read_only=False And a tool execution context with plan_id "plan-rw-1" and read_only False When I enforce capabilities on the tool Then no enforcement error should be raised Scenario: Tool with writes=True is blocked even when cap.read_only is unset Given the read-only enforcement modules are available Given a tool "builtin/hybrid-tool" with writes=True and read_only=False And a tool execution context with plan_id "plan-ro-3" and read_only True When I enforce capabilities on the tool Then a ToolAccessDeniedError should be raised for the tool And the access-denied message should contain "builtin/hybrid-tool" Scenario: Tool with neither read_only nor writes passes on read-only plan Given the read-only enforcement modules are available Given a tool "builtin/noop" with writes=False and read_only=False And a tool execution context with plan_id "plan-ro-4" and read_only True When I enforce capabilities on the tool Then no enforcement error should be raised # ── SkillContext.enforce_write_guard ────────────────────────────────── Scenario: SkillContext enforce_write_guard blocks write tool with tool name Given the read-only enforcement modules are available Given a read-only SkillContext for plan "plan-sk-1" When I call enforce_write_guard with tool_name "skill/writer" Then a SkillExecutionError should be raised for the guard And the write-guard error message should contain "skill/writer" And the write-guard error type should be PERMISSION_DENIED Scenario: SkillContext enforce_write_guard allows on writable context Given the read-only enforcement modules are available Given a writable SkillContext for plan "plan-sk-2" When I call enforce_write_guard with tool_name "skill/writer" Then no write-guard error should be raised # ── ChangeSetCapture read-only enforcement ─────────────────────────── Scenario: ChangeSet builder rejects write entry when plan is read-only Given the read-only enforcement modules are available Given a ChangeSetCapture with plan_id "plan-cs-1" and read_only True When I try to wrap a write-capable tool spec Then a ReadOnlyViolationError should be raised for the capture And the capture error message should contain "read-only" Scenario: ChangeSet builder allows wrapping when not read-only Given the read-only enforcement modules are available Given a ChangeSetCapture with plan_id "plan-cs-2" and read_only False When I try to wrap a write-capable tool spec Then no capture error should be raised Scenario: ChangeSet builder passes read-only tool through unchanged Given the read-only enforcement modules are available Given a ChangeSetCapture with plan_id "plan-cs-3" and read_only True When I wrap a read-only tool spec Then the wrapped tool spec should be returned unchanged # ── ExecuteStubActor read-only propagation ───────────────────────────── Scenario: ExecuteStubActor propagates read_only to ChangeSetCapture Given the read-only enforcement modules are available When I execute via ExecuteStubActor with read_only True Then the stub actor should create a read-only ChangeSetCapture Scenario: ExecuteStubActor allows writes when not read-only Given the read-only enforcement modules are available When I execute via ExecuteStubActor with read_only False Then the stub actor should create a writable ChangeSetCapture # ── File write tool blocked ────────────────────────────────────────── Scenario: File write tool blocked under read-only plan via ToolRuntime Given the read-only enforcement modules are available Given a tool "builtin/file-write" with writes=True and read_only=False And a tool execution context with plan_id "plan-fw-1" and read_only True When I enforce capabilities on the tool Then a ToolAccessDeniedError should be raised for the tool And the access-denied message should contain "builtin/file-write" # ── Git push tool blocked ──────────────────────────────────────────── Scenario: Git push tool blocked under read-only plan via ToolRuntime Given the read-only enforcement modules are available Given a tool "git/push" with writes=True and read_only=False And a tool execution context with plan_id "plan-gp-1" and read_only True When I enforce capabilities on the tool Then a ToolAccessDeniedError should be raised for the tool And the access-denied message should contain "git/push" # ── Read-only SkillContext blocks write tool ─────────────────────────── Scenario: Read-only SkillContext blocks write tool via enforce_write_guard Given the read-only enforcement modules are available Given a read-only SkillContext for plan "plan-compat-1" When I call enforce_write_guard with tool_name "test/write-tool" Then a SkillExecutionError should be raised for the guard And the write-guard error message should contain "test/write-tool" Scenario: Writable SkillContext allows write tool Given the read-only enforcement modules are available Given a writable SkillContext for plan "plan-compat-2" When I call enforce_write_guard with tool_name "test/write-tool" Then no write-guard error should be raised # ── Plan -> ToolExecutionContext propagation ────────────────────────── Scenario: ToolExecutionContext carries plan_read_only from plan Given the read-only enforcement modules are available Given a Plan domain model with read_only=True and plan_id "plan-ex-1" When I create a ToolExecutionContext from the plan Then the propagated context plan_read_only should be True Scenario: ToolExecutionContext writable for writable plan Given the read-only enforcement modules are available Given a Plan domain model with read_only=False and plan_id "plan-ex-2" When I create a ToolExecutionContext from the plan Then the propagated context plan_read_only should be False # ============================================================ # Originally from: security_secrets.feature # Feature: SEC5 - Secrets masking and validation # ============================================================ 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 Google/Gemini API key pattern Given a string containing "AIzaSyA1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q" When I redact the string Then the redacted value should be "***REDACTED***" Scenario: Mixed text with Gemini API key is partially redacted Given a string containing "Using key AIzaSyA1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q for Gemini" When I redact the string Then the redacted value should contain "***REDACTED***" And the redacted value should contain "Using key" And the redacted value should contain "for Gemini" And the redacted value should not contain "AIzaSy" 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 "" 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 "" 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" # ============================================================ # Originally from: security_templates.feature # Feature: Secure template rendering # ============================================================ Scenario: Render a simple placeholder Given a secure template test environment Given the secure template text "{greeting} {name}" And a secure template context key "greeting" with value "Hello" And a secure template context key "name" with value "World" When I render the secure template Then the secure template output should be "Hello World" Scenario: Render with missing placeholder leaves it intact Given a secure template test environment Given the secure template text "Hello {name}, your role is {role}" And a secure template context key "name" with value "Alice" When I render the secure template Then the secure template output should be "Hello Alice, your role is {role}" Scenario: Render with empty context returns template as-is Given a secure template test environment Given the secure template text "No placeholders here" When I render the secure template Then the secure template output should be "No placeholders here" Scenario: Render with numeric values converts to string Given a secure template test environment Given the secure template text "Count: {count}" And a secure template context key "count" with value "42" When I render the secure template Then the secure template output should be "Count: 42" # -- Security rejection --------------------------------------------------- Scenario: Reject attribute access in template Given a secure template test environment Given the secure template text "{obj.__class__.__name__}" When I render the secure template expecting a security error Then the secure template error should mention "Unsafe" Scenario: Reject index access in template Given a secure template test environment Given the secure template text "{items[0]}" When I render the secure template expecting a security error Then the secure template error should mention "Unsafe" Scenario: Reject format spec in template Given a secure template test environment Given the secure template text "{value:>10}" When I render the secure template expecting a security error Then the secure template error should mention "Unsafe" Scenario: Reject conversion flag in template Given a secure template test environment Given the secure template text "{value!r}" When I render the secure template expecting a security error Then the secure template error should mention "Unsafe" Scenario: Reject function call in template Given a secure template test environment Given the secure template text "{fn()}" When I render the secure template expecting a security error Then the secure template error should mention "Unsafe" Scenario: Reject Jinja2 expression delimiters Given a secure template test environment Given the secure template text "{{ config }}" When I render the secure template expecting a security error Then the secure template error should mention "Unsafe" Scenario: Reject Jinja2 block delimiters Given a secure template test environment Given the secure template text "{% for x in items %}x{% endfor %}" When I render the secure template expecting a security error Then the secure template error should mention "Unsafe" # -- Size limits ----------------------------------------------------------- Scenario: Reject template exceeding max length Given a secure template test environment Given a secure template that is 11000 characters long When I render the secure template expecting a size error Then the secure template error should mention "exceeds maximum" Scenario: Reject output exceeding max length Given a secure template test environment Given the secure template text "{big}" And a secure template config with max output length of 20 And a secure template context key "big" with value "This value is definitely longer than twenty characters" When I render the secure template expecting a size error Then the secure template error should mention "exceeds maximum" # -- Allowlist enforcement ------------------------------------------------ Scenario: Reject template key not in allowlist Given a secure template test environment Given the secure template text "{name} {secret}" And a secure template with allowed keys "name" And a secure template context key "name" with value "Alice" And a secure template context key "secret" with value "hidden" When I render the secure template expecting a validation error Then the secure template error should mention "disallowed" Scenario: Accept template key that is in allowlist Given a secure template test environment Given the secure template text "{name}" And a secure template with allowed keys "name" And a secure template context key "name" with value "Alice" When I render the secure template Then the secure template output should be "Alice" Scenario: Open allowlist allows any simple key Given a secure template test environment Given the secure template text "{anything}" And the secure template has no key restrictions And a secure template context key "anything" with value "works" When I render the secure template Then the secure template output should be "works" # -- Pre-validation ------------------------------------------------------- Scenario: Validate clean template returns no errors Given a secure template test environment Given the secure template text "{name} {role}" When I validate the secure template Then the secure template validation should be empty Scenario: Validate unsafe template returns errors Given a secure template test environment Given the secure template text "{obj.__class__}" When I validate the secure template Then the secure template validation should not be empty And the secure template validation should mention "Unsafe" Scenario: Validate template with unknown keys returns errors Given a secure template test environment Given the secure template text "{name} {secret}" And a secure template with allowed keys "name" When I validate the secure template Then the secure template validation should not be empty And the secure template validation should mention "Unknown" Scenario: Validate oversized template returns errors Given a secure template test environment Given a secure template that is 11000 characters long When I validate the secure template Then the secure template validation should not be empty And the secure template validation should mention "exceeds" # -- Convenience function ------------------------------------------------- Scenario: render_template_secure convenience function works Given a secure template test environment Given the secure template text "{greeting}" And a secure template context key "greeting" with value "Hi" When I render via the secure convenience function Then the secure template output should be "Hi" Scenario: render_template_secure rejects unsafe input Given a secure template test environment Given the secure template text "{obj.attr}" When I render via the secure convenience function expecting an error Then the secure template error should mention "Unsafe" # -- Config property access ------------------------------------------------ Scenario: Renderer exposes active configuration Given a secure template test environment Given a secure template with allowed keys "name" When I create a renderer with those settings Then the renderer config allowed keys should contain "name" # -- Validate with matching allowlist (no unknown keys) ------------------- Scenario: Validate template where all keys match allowlist Given a secure template test environment Given the secure template text "{name}" And a secure template with allowed keys "name" When I validate the secure template Then the secure template validation should be empty # -- Rejected keys without logging ---------------------------------------- Scenario: Reject unknown key without logging when log_rejected is false Given a secure template test environment Given the secure template text "{name} {secret}" And a secure template config with logging disabled And a secure template context key "name" with value "Alice" And a secure template context key "secret" with value "hidden" When I render the secure template expecting a validation error Then the secure template error should mention "disallowed" # -- reject_unknown_keys=False path ---------------------------------------- Scenario: Allow unknown keys when reject_unknown_keys is disabled Given a secure template test environment Given the secure template text "{name} {extra}" And a secure template config that permits unknown keys And a secure template context key "name" with value "Alice" And a secure template context key "extra" with value "data" When I render the secure template Then the secure template output should be "Alice data" # -- Legacy renderer with config passthrough -------------------------------- Scenario: Legacy TemplateRenderer accepts config passthrough Given a secure template test environment Given the secure template text "{name}" And a secure template with allowed keys "name" And a secure template context key "name" with value "Carol" When I render via the legacy TemplateRenderer with config Then the secure template output should be "Carol" # -- Legacy renderer backward compat -------------------------------------- Scenario: Legacy TemplateRenderer returns raw on unsafe template Given a secure template test environment Given the secure template text "{obj.__class__}" And a secure template context key "obj" with value "test" When I render via the legacy TemplateRenderer Then the secure template output should be "{obj.__class__}" Scenario: Legacy TemplateRenderer renders safe templates Given a secure template test environment Given the secure template text "{name}" And a secure template context key "name" with value "Bob" When I render via the legacy TemplateRenderer Then the secure template output should be "Bob"