prvng_core/nulib/audit/commands.nu
Jesús Pérez 1fe83246d6
feat: integrate enterprise security system into core libraries
Adds KMS, secrets management, config encryption, and auth plugins to enable
zero-trust security architecture across the provisioning platform.
2025-10-09 16:36:27 +01:00

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)
}