# Audit logging CLI commands for Provisioning platform # # Provides query, export, and management of audit logs use std assert # Query audit logs with filters # # @param --user: Filter by user ID # @param --action: Filter by action type # @param --resource: Filter by resource name # @param --workspace: Filter by workspace name # @param --status: Filter by status (success, failure, error) # @param --from: Filter by start date (RFC3339 or relative like "1d", "1w", "1M") # @param --to: Filter by end date (RFC3339 or relative) # @param --ip: Filter by IP address # @param --limit: Maximum number of results (default: 100) # @param --offset: Offset for pagination (default: 0) # @param --sort-by: Sort field (timestamp, user, action) # @param --sort-desc: Sort in descending order # @param --out: Output format (json, table, csv) # # @example Query all server creates by a user # audit query --user "user123" --action "server_create" # # @example Query failed operations in last 24 hours # audit query --status "failure" --from "1d" --out table export def "audit query" [ --user: string # Filter by user ID --action: string # Filter by action type --resource: string # Filter by resource --workspace: string # Filter by workspace --status: string # Filter by status --from: string # Start date (RFC3339 or relative) --to: string # End date (RFC3339 or relative) --ip: string # Filter by IP address --limit: int = 100 # Maximum results --offset: int = 0 # Pagination offset --sort-by: string = "timestamp" # Sort field --sort-desc # Sort descending --out: string = "table" # Output format ] { # Convert relative dates to absolute let from_date = if ($from | is-not-empty) { parse_relative_date $from } else { null } let to_date = if ($to | is-not-empty) { parse_relative_date $to } else { null } # Build query parameters let query = { filter: { user_id: $user action_type: $action resource: $resource workspace: $workspace status: $status from: $from_date to: $to_date ip: $ip } limit: $limit offset: $offset sort_by: $sort_by sort_desc: $sort_desc } # Call orchestrator API let orchestrator_url = get_orchestrator_url let response = http post $"($orchestrator_url)/api/v1/audit/query" $query if ($response | get success) { let events = ($response | get data) # Format output match $out { "json" => { $events | to json } "csv" => { $events | to csv } "table" => { $events | select event_id timestamp user.id action.action_type action.resource result.status result.duration_ms | rename event_id timestamp user action resource status duration_ms | table } _ => { $events } } } else { error make { msg: "Failed to query audit logs" label: { text: ($response | get error) span: (metadata $response).span } } } } # Export audit logs to file # # @param format: Export format (json, jsonlines, csv, splunk, elastic) # @param --output: Output file path # @param --user: Filter by user ID # @param --action: Filter by action type # @param --from: Start date filter # @param --to: End date filter # # @example Export last 7 days to JSON # audit export json --output audit-export.json --from "7d" # # @example Export to Splunk format # audit export splunk --output splunk-events.json export def "audit export" [ format: string # Export format (json, jsonlines, csv, splunk, elastic) --output: string # Output file path --user: string # Filter by user ID --action: string # Filter by action type --resource: string # Filter by resource --workspace: string # Filter by workspace --status: string # Filter by status --from: string # Start date filter --to: string # End date filter ] { # Validate format let valid_formats = ["json" "jsonlines" "csv" "splunk" "elastic"] if ($format not-in $valid_formats) { error make { msg: $"Invalid format: ($format)" label: { text: $"Valid formats: ($valid_formats | str join ', ')" span: (metadata $format).span } } } # Convert relative dates let from_date = if ($from | is-not-empty) { parse_relative_date $from } else { null } let to_date = if ($to | is-not-empty) { parse_relative_date $to } else { null } # Build export request let request = { format: $format filter: { user_id: $user action_type: $action resource: $resource workspace: $workspace status: $status from: $from_date to: $to_date } } # Call orchestrator API let orchestrator_url = get_orchestrator_url let response = http post $"($orchestrator_url)/api/v1/audit/export" $request if ($response | get success) { let data = ($response | get data) # Write to file if output specified if ($output | is-not-empty) { $data | save --force $output print $"✓ Audit logs exported to: ($output)" } else { # Print to stdout $data } } else { error make { msg: "Failed to export audit logs" label: { text: ($response | get error) span: (metadata $response).span } } } } # Show audit log statistics # # @example Display audit statistics # audit stats export def "audit stats" [] { let orchestrator_url = get_orchestrator_url let response = http get $"($orchestrator_url)/api/v1/audit/stats" if ($response | get success) { let stats = ($response | get data) print "\nAudit Log Statistics:\n" [ ["Metric" "Value"]; ["Total Events" ($stats.total_events | into string)] ["Success Events" ($stats.success_events | into string)] ["Failure Events" ($stats.failure_events | into string)] ["Error Events" ($stats.error_events | into string)] ["Buffered Events" ($stats.buffered_events | into string)] ["Flushed Events" ($stats.flushed_events | into string)] ] | table } else { error make { msg: "Failed to get audit statistics" label: { text: ($response | get error) span: (metadata $response).span } } } } # Show storage statistics # # @example Display storage statistics # audit storage-stats export def "audit storage-stats" [] { let orchestrator_url = get_orchestrator_url let response = http get $"($orchestrator_url)/api/v1/audit/storage-stats" if ($response | get success) { let stats = ($response | get data) print "\nAudit Storage Statistics:\n" [ ["Metric" "Value"]; ["Total Events" ($stats.total_events | into string)] ["Total Size" (format_bytes $stats.total_size_bytes)] ["Oldest Event" ($stats.oldest_event | default "N/A")] ["Newest Event" ($stats.newest_event | default "N/A")] ] | table } else { error make { msg: "Failed to get storage statistics" label: { text: ($response | get error) span: (metadata $response).span } } } } # Apply retention policy to audit logs # # @param --max-age-days: Maximum age in days (default: 365) # @param --archive: Archive before deletion # @param --archive-path: Archive destination path # @param --yes: Skip confirmation # # @example Apply 90-day retention policy # audit apply-retention --max-age-days 90 --archive --yes export def "audit apply-retention" [ --max-age-days: int = 365 # Maximum age in days --archive # Archive before deletion --archive-path: string # Archive destination --yes # Skip confirmation ] { if not $yes { print $"⚠️ This will delete audit logs older than ($max-age_days) days" let confirm = (input "Continue? (yes/no): ") if ($confirm != "yes") { print "Cancelled" return } } let policy = { max_age_days: $max_age_days auto_apply: false archive_before_delete: $archive archive_path: $archive_path } let orchestrator_url = get_orchestrator_url let response = http post $"($orchestrator_url)/api/v1/audit/apply-retention" $policy if ($response | get success) { let deleted = ($response | get data) print $"✓ Retention policy applied: ($deleted) events processed" } else { error make { msg: "Failed to apply retention policy" label: { text: ($response | get error) span: (metadata $response).span } } } } # View recent audit events (tail) # # @param --count: Number of recent events to show (default: 20) # @param --follow: Follow new events in real-time # # @example View last 50 events # audit tail --count 50 # # @example Follow audit log in real-time # audit tail --follow export def "audit tail" [ --count: int = 20 # Number of events to show --follow # Follow new events ] { if $follow { # Real-time following via WebSocket let orchestrator_url = get_orchestrator_url let ws_url = ($orchestrator_url | str replace "http" "ws") print "Following audit log (Ctrl+C to stop)..." # TODO: Implement WebSocket streaming print "WebSocket streaming not yet implemented" } else { # Show recent events audit query --limit $count --sort-by timestamp --sort-desc --out table } } # Search audit logs with text query # # @param query: Search text # @param --field: Field to search (user, resource, action) # # @example Search for specific server # audit search "server-123" # # @example Search in specific field # audit search "john" --field user export def "audit search" [ query: string # Search query --field: string = "all" # Field to search ] { # Use orchestrator search endpoint let orchestrator_url = get_orchestrator_url let response = http post $"($orchestrator_url)/api/v1/audit/search" { query: $query field: $field } if ($response | get success) { let results = ($response | get data) $results | select event_id timestamp user.id action.action_type action.resource | rename event_id timestamp user action resource | table } else { error make { msg: "Failed to search audit logs" label: { text: ($response | get error) span: (metadata $response).span } } } } # Helper: Get orchestrator URL from config def get_orchestrator_url [] { # Try environment variable first let env_url = ($env.PROVISIONING_ORCHESTRATOR_URL? | default "") if ($env_url | is-not-empty) { $env_url } else { # Default to localhost "http://localhost:8080" } } # Helper: Parse relative date (1d, 7d, 1w, 1M, 1y) def parse_relative_date [relative: string] { let now = (date now) if ($relative | str ends-with "d") { let days = ($relative | str replace "d" "" | into int) $now | date subtract ($days) day | format date "%+" } else if ($relative | str ends-with "w") { let weeks = ($relative | str replace "w" "" | into int) $now | date subtract ($weeks * 7) day | format date "%+" } else if ($relative | str ends-with "M") { let months = ($relative | str replace "M" "" | into int) $now | date subtract ($months) month | format date "%+" } else if ($relative | str ends-with "y") { let years = ($relative | str replace "y" "" | into int) $now | date subtract ($years) year | format date "%+" } else { # Assume it's already RFC3339 $relative } } # Helper: Format bytes to human-readable def format_bytes [bytes: int] { if $bytes < 1024 { $"($bytes) B" } else if $bytes < (1024 * 1024) { $"(($bytes / 1024) | math round -p 2) KB" } else if $bytes < (1024 * 1024 * 1024) { $"(($bytes / 1024 / 1024) | math round -p 2) MB" } else { $"(($bytes / 1024 / 1024 / 1024) | math round -p 2) GB" } } # Helper: Test if value is empty def "is-empty" [] { $in == null or $in == "" } # Helper: Test if value is not empty def "is-not-empty" [] { not ($in | is-empty) }