Adds KMS, secrets management, config encryption, and auth plugins to enable zero-trust security architecture across the provisioning platform.
419 lines
13 KiB
Plaintext
419 lines
13 KiB
Plaintext
# 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)
|
|
}
|