Files
cleveragents-core/features/security_audit.feature
khyari hamza 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
test(security): add coverage for audit CLI, logging, and redaction edge cases
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)
2026-02-19 20:10:56 +00:00

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