Feature: SEC7 - Audit logging for apply As a security-conscious developer I want all security-relevant operations recorded in an audit log So that I have a compliance trail of plan applies, cancellations, and changes # ── Recording events ────────────────────────────────────────── Scenario: Record a plan_applied event Given a fresh audit service When I record a "plan_applied" event with plan_id "plan-001" and project "myproject" Then the audit log should contain 1 entry And the entry event_type should be "plan_applied" And the entry plan_id should be "plan-001" And the entry project_name should be "myproject" Scenario: Record a plan_cancelled event Given a fresh audit service When I record a "plan_cancelled" event with plan_id "plan-002" and details reason "user request" Then the audit log should contain 1 entry And the entry event_type should be "plan_cancelled" And the entry details should contain key "reason" Scenario: Record a resource_modified event Given a fresh audit service When I record a "resource_modified" event with details resource_id "res-001" and tool "file_write" Then the audit log should contain 1 entry And the entry event_type should be "resource_modified" And the entry details key "tool" should be "file_write" Scenario: Record a correction_applied event Given a fresh audit service When I record a "correction_applied" event with actor "openai/gpt-4o" Then the entry actor_name should be "openai/gpt-4o" Scenario: Record a config_changed event Given a fresh audit service When I record a "config_changed" event with user "hamza" Then the entry user_identity should be "hamza" Scenario: Record an entity_deleted event Given a fresh audit service When I record a "entity_deleted" event with details entity_type "resource" and entity_name "old-res" Then the entry details key "entity_type" should be "resource" And the entry details key "entity_name" should be "old-res" Scenario: Record a session_created event Given a fresh audit service When I record a "session_created" event with session_id "sess-001" Then the audit log should contain 1 entry And the entry event_type should be "session_created" And the entry details key "session_id" should be "sess-001" Scenario: Record event with empty details Given a fresh audit service When I record a "plan_applied" event with no details Then the audit log should contain 1 entry And the entry details should be empty Scenario: Record multiple events Given a fresh audit service When I record 5 "plan_applied" events Then the audit log should contain 5 entries # ── Querying / filtering ────────────────────────────────────── Scenario: List entries filtered by plan_id Given a fresh audit service with mixed events When I list entries with plan filter "plan-A" Then all returned entries should have plan_id "plan-A" Scenario: List entries filtered by project Given a fresh audit service with mixed events When I list entries with project filter "project-X" Then all returned entries should have project_name "project-X" Scenario: List entries filtered by event_type Given a fresh audit service with mixed events When I list entries with type filter "plan_cancelled" Then all returned entries should have event_type "plan_cancelled" Scenario: List entries filtered by since timestamp Given a fresh audit service with old and new events When I list entries since "2099-01-01T00:00:00" Then no entries should be returned Scenario: List entries with limit Given a fresh audit service with 20 events When I list entries with limit 5 Then exactly 5 entries should be returned Scenario: List entries returns newest first Given a fresh audit service When I record a "plan_applied" event with plan_id "first" and project "p" And I record a "plan_applied" event with plan_id "second" and project "p" And I list all entries Then the first entry should have plan_id "second" # ── Show single entry ───────────────────────────────────────── Scenario: Show entry by ID Given a fresh audit service When I record a "plan_applied" event with plan_id "plan-show" and project "proj" And I fetch the entry by its ID Then the fetched entry should have event_type "plan_applied" And the fetched entry should have plan_id "plan-show" Scenario: Show non-existent entry returns None Given a fresh audit service When I fetch entry with ID 99999 Then no entry should be returned # ── Count ────────────────────────────────────────────────────── Scenario: Count returns total entries Given a fresh audit service When I record 7 "config_changed" events Then the count should be 7 Scenario: Count on empty log returns zero Given a fresh audit service Then the count should be 0 # ── Retention / pruning ──────────────────────────────────────── Scenario: Prune deletes entries older than retention days Given a fresh audit service with entries from 60 days ago When I prune with retention 30 days Then the audit log should be empty Scenario: Prune keeps recent entries Given a fresh audit service When I record a "plan_applied" event with plan_id "recent" and project "p" And I prune with retention 30 days Then the audit log should contain 1 entry Scenario: Prune with zero retention keeps everything Given a fresh audit service with entries from 60 days ago When I prune with retention 0 days Then the audit log should not be empty Scenario: Prune returns count of deleted entries Given a fresh audit service with 10 entries from 90 days ago When I prune with retention 30 days Then the prune result should be 10 # ── Settings ─────────────────────────────────────────────────── Scenario: Default audit retention is zero (keep indefinitely) Given default settings Then audit_retention_days should be 0 Scenario: Audit retention can be configured via env var Given settings with audit_retention_days 90 Then audit_retention_days should be 90 # ── Data class serialization ─────────────────────────────────── Scenario: AuditLogEntry as_dict returns all fields Given a fresh audit service When I record a "plan_applied" event with plan_id "p-1" and project "proj-1" And I serialize the entry to dict Then the dict should contain keys "id" "event_type" "plan_id" "project_name" "created_at" "details" Scenario: AuditLogEntry details contain structured data Given a fresh audit service When I record a "plan_applied" event with details files_changed 3 and validation_passed true And I serialize the entry to dict Then the dict details key "files_changed" should be 3 # ── Edge cases ───────────────────────────────────────────────── Scenario: Record event with all nullable fields as None Given a fresh audit service When I record a "plan_applied" event with all nullable fields as None Then the entry plan_id should be None And the entry project_name should be None And the entry actor_name should be None And the entry user_identity should be None Scenario: Details with non-serializable values use str fallback Given a fresh audit service When I record a "plan_applied" event with datetime in details Then the audit log should contain 1 entry And the entry details should contain key "timestamp" Scenario: Recording with invalid event_type raises ValueError Given a fresh audit service When I record an event with invalid event_type "plan_appleid" Then a ValueError should be raised for audit service And the audit error message should contain "plan_appleid" Scenario: Service requires Settings instance When I create an AuditService with invalid settings Then a TypeError should be raised for audit service # ── Context manager and owned session ────────────────────────── Scenario: Service context manager opens and closes Given a fresh audit service with owned session When I use the service as a context manager to record "plan_applied" Then the audit context manager result should have event_type "plan_applied" Scenario: Service close is safe to call on non-owned session Given a fresh audit service When I close the service Then no audit error should be raised Scenario: Row with invalid JSON details falls back to empty dict Given a fresh audit service with a corrupt details row When I list all entries Then exactly 1 entries should be returned And the first listed entry details should be empty # ── CLI commands ──────────────────────────────────────────────── Scenario: CLI list shows entries Given a fresh audit service When I record a "plan_applied" event with plan_id "cli-1" and project "proj" And I invoke the audit CLI list command Then the CLI exit code should be 0 And the CLI output should contain "Audit Log" Scenario: CLI list with no entries shows empty message Given a fresh audit service When I invoke the audit CLI list command Then the CLI exit code should be 0 And the CLI output should contain "No audit entries found" Scenario: CLI list with filters Given a fresh audit service When I record a "plan_applied" event with plan_id "f-1" and project "proj" And I invoke the audit CLI list command with plan "f-1" and type "plan_applied" and project "proj" and limit 10 Then the CLI exit code should be 0 And the CLI output should contain "Audit Log" Scenario: CLI show displays entry details Given a fresh audit service When I record a "config_changed" event with user "hamza" And I invoke the audit CLI show command with the entry ID Then the CLI exit code should be 0 And the CLI output should contain "config_changed" And the CLI output should contain "hamza" Scenario: CLI show with non-existent ID exits with error Given a fresh audit service When I invoke the audit CLI show command with ID 99999 Then the CLI exit code should be 1 And the CLI output should contain "not found" Scenario: CLI count shows total Given a fresh audit service When I record 3 "plan_applied" events And I invoke the audit CLI count command Then the CLI exit code should be 0 And the CLI output should contain "3" Scenario: CLI prune with yes flag Given a fresh audit service with entries from 60 days ago When I invoke the audit CLI prune command with days 30 and yes flag Then the CLI exit code should be 0 And the CLI output should contain "Pruned" Scenario: CLI prune without yes prompts and aborts Given a fresh audit service with entries from 60 days ago When I invoke the audit CLI prune command with days 30 and no confirmation Then the CLI exit code should be 1 Scenario: CLI prune with zero retention does nothing Given a fresh audit service When I invoke the audit CLI prune command with days 0 and no confirmation Then the CLI exit code should be 0 And the CLI output should contain "keep everything" # ── Logging configuration ────────────────────────────────────── Scenario: configure_structlog sets up development logging When I configure structlog for development with level "DEBUG" Then structlog should be configured Scenario: configure_structlog sets up production logging When I configure structlog for production with level "INFO" Then structlog should be configured Scenario: configure_structlog rejects invalid log level When I configure structlog with invalid level "BOGUS" Then a ValueError should be raised for audit service Scenario: get_logger returns a bound logger When I call get_logger with name "test" Then a structlog logger should be returned # ── Redaction edge cases ──────────────────────────────────────── Scenario: set_show_secrets rejects non-bool When I call set_show_secrets with a non-bool value Then a TypeError should be raised for audit service Scenario: is_sensitive_key with empty string returns false When I check if empty string is sensitive Then the sensitivity result should be false Scenario: mask_database_url with empty string returns empty When I mask an empty database URL Then the audit masked URL should be empty Scenario: register_pattern rejects empty pattern When I register an empty secret pattern Then a ValueError should be raised for audit service Scenario: secrets_masking_processor skips when show_secrets is true When I run the structlog processor with show_secrets enabled Then the event dict should be returned unchanged Scenario: secrets_masking_processor redacts nested dict values When I run the structlog processor with a nested dict value Then the nested dict should be redacted