eeb0d49377
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 15s
CI / build (pull_request) Successful in 17s
CI / quality (pull_request) Successful in 17s
CI / typecheck (pull_request) Successful in 29s
CI / security (pull_request) Successful in 38s
CI / integration_tests (pull_request) Successful in 2m22s
CI / unit_tests (pull_request) Successful in 4m0s
CI / docker (pull_request) Successful in 37s
CI / benchmark-regression (pull_request) Successful in 10m6s
CI / coverage (pull_request) Successful in 11m21s
Add 22 new Behave scenarios covering: - audit CLI commands (list, show, prune, count) - audit service context manager and owned-session paths - corrupt JSON details fallback in _row_to_entry - configure_structlog dev/prod/invalid and get_logger - redaction edge cases (empty key, empty URL, empty pattern, show_secrets bypass, nested dict processor)
317 lines
14 KiB
Gherkin
317 lines
14 KiB
Gherkin
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
|