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.
This commit is contained in:
Jesús Pérez 2025-10-09 16:36:27 +01:00
parent 228dbb889b
commit 1fe83246d6
Signed by: jesus
GPG Key ID: 9F243E355E0BC939
17 changed files with 4406 additions and 22 deletions

1
.gitignore vendored
View File

@ -12,6 +12,7 @@ CLAUDE.md
wrks
ROOT
OLD
plugins/nushell-plugins
# Generated by Cargo
# will have compiled files and executables
debug/

418
nulib/audit/commands.nu Normal file
View File

@ -0,0 +1,418 @@
# 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)
}

View File

@ -0,0 +1,329 @@
# Break-Glass Emergency Access Commands
#
# Provides CLI interface for break-glass emergency access system
# Request emergency access
export def "break-glass request" [
reason: string # Emergency reason (brief)
--justification: string # Detailed justification (required)
--resources: list<string> = [] # Target resources
--permissions: list<string> = [] # Requested permissions
--duration: duration = 4hr # Maximum session duration
--orchestrator: string = "http://localhost:8080" # Orchestrator URL
]: nothing -> record {
if ($justification | is-empty) {
error make {msg: "Justification is required for break-glass requests"}
}
# Get current user info
let requester = {
id: (whoami)
email: $"(whoami)@example.com"
name: (whoami)
teams: ["operations"]
roles: ["Operator"]
}
# Convert permissions list to structured format
let structured_permissions = $permissions | each {|p|
let parts = ($p | split row ":")
{
resource: ($parts | get 0)
action: ($parts | get 1? | default "admin")
scope: null
}
}
# Convert duration to hours
let duration_hours = ($duration | into int) / (1hr | into int)
let payload = {
requester: $requester
reason: $reason
justification: $justification
target_resources: $resources
requested_permissions: $structured_permissions
duration_hours: $duration_hours
}
print $"🚨 Requesting emergency access..."
print $" Reason: ($reason)"
print $" Duration: ($duration)"
print $" Resources: ($resources | str join ', ')"
let response = (http post $"($orchestrator)/api/v1/break-glass/request" $payload)
print $"✅ Request created: ($response.request_id)"
print $" Status: ($response.status)"
print $" Expires: ($response.expires_at)"
print ""
print $"⏳ Waiting for approval from 2+ approvers..."
$response
}
# Approve emergency request
export def "break-glass approve" [
request_id: string # Request ID to approve
--reason: string = "Approved" # Approval reason
--orchestrator: string = "http://localhost:8080" # Orchestrator URL
]: nothing -> record {
# Get current user info
let approver = {
id: (whoami)
email: $"(whoami)@example.com"
name: (whoami)
teams: ["security"]
roles: ["SecurityOfficer"]
}
# Get IP address
let ip_address = "127.0.0.1"
let payload = {
approver: $approver
reason: $reason
ip_address: $ip_address
}
print $"✅ Approving request ($request_id)..."
let response = (http post $"($orchestrator)/api/v1/break-glass/requests/($request_id)/approve" $payload)
if $response.approved {
print $"✅ Request fully approved!"
print $" All required approvals received"
print $" Ready for activation"
} else {
print $"⏳ Approval recorded"
print $" ($response.message)"
}
$response
}
# Deny emergency request
export def "break-glass deny" [
request_id: string # Request ID to deny
--reason: string = "Denied" # Denial reason
--orchestrator: string = "http://localhost:8080" # Orchestrator URL
]: nothing -> nothing {
# Get current user info
let denier = {
id: (whoami)
email: $"(whoami)@example.com"
name: (whoami)
teams: ["security"]
roles: ["SecurityOfficer"]
}
let payload = {
denier: $denier
reason: $reason
}
print $"❌ Denying request ($request_id)..."
http post $"($orchestrator)/api/v1/break-glass/requests/($request_id)/deny" $payload | ignore
print $"✅ Request denied"
}
# Activate approved session
export def "break-glass activate" [
request_id: string # Request ID to activate
--orchestrator: string = "http://localhost:8080" # Orchestrator URL
]: nothing -> record {
print $"🔓 Activating emergency session for request ($request_id)..."
let token = (http post $"($orchestrator)/api/v1/break-glass/requests/($request_id)/activate" {})
print $"✅ Emergency session activated!"
print $" Session ID: ($token.session_id)"
print $" Expires: ($token.expires_at)"
print $" Token: ($token.access_token | str substring 0..50)..."
print ""
print $"⚠️ This session is logged and monitored"
print $"⚠️ All actions will be audited"
print ""
print $"Export token:"
print $" export EMERGENCY_TOKEN=($token.access_token)"
$token
}
# Revoke active session
export def "break-glass revoke" [
session_id: string # Session ID to revoke
--reason: string = "Manual revocation" # Revocation reason
--orchestrator: string = "http://localhost:8080" # Orchestrator URL
]: nothing -> nothing {
let payload = {
reason: $reason
}
print $"🔒 Revoking session ($session_id)..."
http post $"($orchestrator)/api/v1/break-glass/sessions/($session_id)/revoke" $payload | ignore
print $"✅ Session revoked"
}
# List pending requests
export def "break-glass list-requests" [
--status: string = "pending" # Filter by status (pending, all)
--orchestrator: string = "http://localhost:8080" # Orchestrator URL
]: nothing -> table {
let pending_only = ($status == "pending")
print $"📋 Listing break-glass requests..."
let requests = (http get $"($orchestrator)/api/v1/break-glass/requests?pending_only=($pending_only)")
if ($requests | is-empty) {
print "No requests found"
return []
}
$requests | select id status requester.email reason created_at expires_at
}
# List active sessions
export def "break-glass list-sessions" [
--active-only: bool = false # Show only active sessions
--orchestrator: string = "http://localhost:8080" # Orchestrator URL
]: nothing -> table {
print $"📋 Listing break-glass sessions..."
let sessions = (http get $"($orchestrator)/api/v1/break-glass/sessions?active_only=($active_only)")
if ($sessions | is-empty) {
print "No sessions found"
return []
}
$sessions | select id status request.requester.email activated_at expires_at
}
# Show session details
export def "break-glass show" [
session_id: string # Session ID to show
--orchestrator: string = "http://localhost:8080" # Orchestrator URL
]: nothing -> record {
print $"🔍 Fetching session details for ($session_id)..."
let session = (http get $"($orchestrator)/api/v1/break-glass/sessions/($session_id)")
print ""
print $"Session ID: ($session.id)"
print $"Status: ($session.status)"
print $"Requester: ($session.request.requester.email)"
print $"Reason: ($session.request.reason)"
print ""
print "Approvals:"
$session.approvals | each {|a|
print $" - ($a.approver.email) at ($a.approved_at)"
}
print ""
print $"Activated: ($session.activated_at)"
print $"Expires: ($session.expires_at)"
print ""
print $"Actions performed: ($session.actions | length)"
$session
}
# Query break-glass audit logs
export def "break-glass audit" [
--from: datetime # Start time
--to: datetime # End time
--session-id: string # Filter by session ID
--orchestrator: string = "http://localhost:8080" # Orchestrator URL
]: nothing -> table {
print $"📜 Querying break-glass audit logs..."
mut params = []
if not ($from | is-empty) {
$params = ($params | append $"from=($from | date to-record | get year)-($from | date to-record | get month)-($from | date to-record | get day)")
}
if not ($to | is-empty) {
$params = ($params | append $"to=($to | date to-record | get year)-($to | date to-record | get month)-($to | date to-record | get day)")
}
if not ($session_id | is-empty) {
$params = ($params | append $"session_id=($session_id)")
}
let query_string = if ($params | is-empty) { "" } else { $"?($params | str join '&')" }
let logs = (http get $"($orchestrator)/api/v1/break-glass/audit($query_string)")
if ($logs | is-empty) {
print "No audit logs found"
return []
}
$logs | select event_id event_type session_id user.email timestamp
}
# Show break-glass statistics
export def "break-glass stats" [
--orchestrator: string = "http://localhost:8080" # Orchestrator URL
]: nothing -> record {
print $"📊 Fetching break-glass statistics..."
let stats = (http get $"($orchestrator)/api/v1/break-glass/statistics")
print ""
print "Approval Statistics:"
print $" Total requests: ($stats.approval.total_requests)"
print $" Pending: ($stats.approval.pending_requests)"
print $" Approved: ($stats.approval.approved_requests)"
print $" Denied: ($stats.approval.denied_requests)"
print $" Expired: ($stats.approval.expired_requests)"
print ""
print "Session Statistics:"
print $" Total sessions: ($stats.session.total_sessions)"
print $" Active: ($stats.session.active_sessions)"
print $" Revoked: ($stats.session.revoked_sessions)"
print $" Expired: ($stats.session.expired_sessions)"
print $" Total actions: ($stats.session.total_actions)"
print ""
print "Revocation Monitoring:"
print $" Enabled: ($stats.revocation.monitoring_enabled)"
print $" Check interval: ($stats.revocation.check_interval_seconds)s"
$stats
}
# Break-glass help
export def "break-glass help" []: nothing -> nothing {
print "Break-Glass Emergency Access System"
print ""
print "Commands:"
print " request - Request emergency access"
print " approve - Approve emergency request"
print " deny - Deny emergency request"
print " activate - Activate approved session"
print " revoke - Revoke active session"
print " list-requests - List pending requests"
print " list-sessions - List active sessions"
print " show - Show session details"
print " audit - Query audit logs"
print " stats - Show statistics"
print ""
print "Examples:"
print " # Request emergency access"
print " break-glass request 'Production outage' --justification 'Database cluster down' --resources ['db/*'] --duration 2hr"
print ""
print " # Approve request"
print " break-glass approve <request_id> --reason 'Emergency confirmed'"
print ""
print " # Activate session"
print " break-glass activate <request_id>"
print ""
print " # Revoke session"
print " break-glass revoke <session_id> --reason 'Emergency resolved'"
}

View File

@ -0,0 +1,508 @@
# Compliance CLI Commands
# Provides comprehensive compliance features for GDPR, SOC2, and ISO 27001
const ORCHESTRATOR_URL = "http://localhost:8080"
# ============================================================================
# GDPR Commands
# ============================================================================
# Export personal data for a user (GDPR Article 15 - Right to Access)
export def "compliance gdpr export" [
user_id: string # User ID to export data for
--orchestrator-url: string = $ORCHESTRATOR_URL # Orchestrator URL
] {
let url = $"($orchestrator_url)/api/v1/compliance/gdpr/export/($user_id)"
print $"Exporting personal data for user: ($user_id)"
try {
let response = http post $url {}
$response | to json
} catch {
error make --unspanned {
msg: $"Failed to export data: ($in)"
}
}
}
# Delete personal data for a user (GDPR Article 17 - Right to Erasure)
export def "compliance gdpr delete" [
user_id: string # User ID to delete data for
--reason: string = "user_request" # Deletion reason
--orchestrator-url: string = $ORCHESTRATOR_URL # Orchestrator URL
] {
let url = $"($orchestrator_url)/api/v1/compliance/gdpr/delete/($user_id)"
print $"Deleting personal data for user: ($user_id)"
print $"Reason: ($reason)"
try {
let response = http post $url {reason: $reason}
print "✓ Data deletion completed"
$response | to json
} catch {
error make --unspanned {
msg: $"Failed to delete data: ($in)"
}
}
}
# Rectify personal data for a user (GDPR Article 16 - Right to Rectification)
export def "compliance gdpr rectify" [
user_id: string # User ID
--field: string # Field to rectify
--value: string # New value
--orchestrator-url: string = $ORCHESTRATOR_URL # Orchestrator URL
] {
if ($field | is-empty) or ($value | is-empty) {
error make --unspanned {
msg: "Both --field and --value must be provided"
}
}
let url = $"($orchestrator_url)/api/v1/compliance/gdpr/rectify/($user_id)"
let corrections = {($field): $value}
print $"Rectifying data for user: ($user_id)"
print $"Field: ($field) -> ($value)"
try {
http post $url {corrections: $corrections}
print "✓ Data rectification completed"
} catch {
error make --unspanned {
msg: $"Failed to rectify data: ($in)"
}
}
}
# Export data for portability (GDPR Article 20 - Right to Data Portability)
export def "compliance gdpr portability" [
user_id: string # User ID
--format: string = "json" # Export format (json, csv, xml)
--output: string # Output file path
--orchestrator-url: string = $ORCHESTRATOR_URL # Orchestrator URL
] {
let url = $"($orchestrator_url)/api/v1/compliance/gdpr/portability/($user_id)"
print $"Exporting data for portability: ($user_id)"
print $"Format: ($format)"
try {
let response = http post $url {format: $format}
if ($output | is-empty) {
$response
} else {
$response | save $output
print $"✓ Data exported to: ($output)"
}
} catch {
error make --unspanned {
msg: $"Failed to export data: ($in)"
}
}
}
# Record objection to processing (GDPR Article 21 - Right to Object)
export def "compliance gdpr object" [
user_id: string # User ID
processing_type: string # Type of processing to object (direct_marketing, profiling, etc.)
--orchestrator-url: string = $ORCHESTRATOR_URL # Orchestrator URL
] {
let url = $"($orchestrator_url)/api/v1/compliance/gdpr/object/($user_id)"
print $"Recording objection for user: ($user_id)"
print $"Processing type: ($processing_type)"
try {
http post $url {processing_type: $processing_type}
print "✓ Objection recorded"
} catch {
error make --unspanned {
msg: $"Failed to record objection: ($in)"
}
}
}
# ============================================================================
# SOC2 Commands
# ============================================================================
# Generate SOC2 compliance report
export def "compliance soc2 report" [
--orchestrator-url: string = $ORCHESTRATOR_URL # Orchestrator URL
--output: string # Output file path
] {
let url = $"($orchestrator_url)/api/v1/compliance/soc2/report"
print "Generating SOC2 compliance report..."
try {
let response = http get $url
if ($output | is-empty) {
$response | to json
} else {
$response | to json | save $output
print $"✓ SOC2 report saved to: ($output)"
}
} catch {
error make --unspanned {
msg: $"Failed to generate SOC2 report: ($in)"
}
}
}
# List SOC2 Trust Service Criteria
export def "compliance soc2 controls" [
--orchestrator-url: string = $ORCHESTRATOR_URL # Orchestrator URL
] {
let url = $"($orchestrator_url)/api/v1/compliance/soc2/controls"
try {
http get $url | get controls
} catch {
error make --unspanned {
msg: $"Failed to list controls: ($in)"
}
}
}
# ============================================================================
# ISO 27001 Commands
# ============================================================================
# Generate ISO 27001 compliance report
export def "compliance iso27001 report" [
--orchestrator-url: string = $ORCHESTRATOR_URL # Orchestrator URL
--output: string # Output file path
] {
let url = $"($orchestrator_url)/api/v1/compliance/iso27001/report"
print "Generating ISO 27001 compliance report..."
try {
let response = http get $url
if ($output | is-empty) {
$response | to json
} else {
$response | to json | save $output
print $"✓ ISO 27001 report saved to: ($output)"
}
} catch {
error make --unspanned {
msg: $"Failed to generate ISO 27001 report: ($in)"
}
}
}
# List ISO 27001 Annex A controls
export def "compliance iso27001 controls" [
--orchestrator-url: string = $ORCHESTRATOR_URL # Orchestrator URL
] {
let url = $"($orchestrator_url)/api/v1/compliance/iso27001/controls"
try {
http get $url | get controls
} catch {
error make --unspanned {
msg: $"Failed to list controls: ($in)"
}
}
}
# List identified risks
export def "compliance iso27001 risks" [
--orchestrator-url: string = $ORCHESTRATOR_URL # Orchestrator URL
] {
let url = $"($orchestrator_url)/api/v1/compliance/iso27001/risks"
try {
http get $url | get risks
} catch {
error make --unspanned {
msg: $"Failed to list risks: ($in)"
}
}
}
# ============================================================================
# Data Protection Commands
# ============================================================================
# Verify data protection controls
export def "compliance protection verify" [
--orchestrator-url: string = $ORCHESTRATOR_URL # Orchestrator URL
] {
let url = $"($orchestrator_url)/api/v1/compliance/protection/verify"
print "Verifying data protection controls..."
try {
http get $url | to json
} catch {
error make --unspanned {
msg: $"Failed to verify protection: ($in)"
}
}
}
# Classify data
export def "compliance protection classify" [
data: string # Data to classify
--orchestrator-url: string = $ORCHESTRATOR_URL # Orchestrator URL
] {
let url = $"($orchestrator_url)/api/v1/compliance/protection/classify"
try {
http post $url {data: $data} | get classification
} catch {
error make --unspanned {
msg: $"Failed to classify data: ($in)"
}
}
}
# ============================================================================
# Access Control Commands
# ============================================================================
# List available roles
export def "compliance access roles" [
--orchestrator-url: string = $ORCHESTRATOR_URL # Orchestrator URL
] {
let url = $"($orchestrator_url)/api/v1/compliance/access/roles"
try {
http get $url | get roles
} catch {
error make --unspanned {
msg: $"Failed to list roles: ($in)"
}
}
}
# Get permissions for a role
export def "compliance access permissions" [
role: string # Role name
--orchestrator-url: string = $ORCHESTRATOR_URL # Orchestrator URL
] {
let url = $"($orchestrator_url)/api/v1/compliance/access/permissions/($role)"
try {
http get $url | get permissions
} catch {
error make --unspanned {
msg: $"Failed to get permissions: ($in)"
}
}
}
# Check if role has permission
export def "compliance access check" [
role: string # Role name
permission: string # Permission to check
--orchestrator-url: string = $ORCHESTRATOR_URL # Orchestrator URL
] {
let url = $"($orchestrator_url)/api/v1/compliance/access/check"
try {
let result = http post $url {role: $role, permission: $permission}
$result | get allowed
} catch {
error make --unspanned {
msg: $"Failed to check permission: ($in)"
}
}
}
# ============================================================================
# Incident Response Commands
# ============================================================================
# Report a security incident
export def "compliance incident report" [
--severity: string # Incident severity (critical, high, medium, low)
--type: string # Incident type (data_breach, unauthorized_access, etc.)
--description: string # Incident description
--orchestrator-url: string = $ORCHESTRATOR_URL # Orchestrator URL
] {
if ($severity | is-empty) or ($type | is-empty) or ($description | is-empty) {
error make --unspanned {
msg: "All parameters (--severity, --type, --description) are required"
}
}
let url = $"($orchestrator_url)/api/v1/compliance/incidents"
print $"Reporting ($severity) incident of type ($type)"
try {
let response = http post $url {
severity: $severity,
incident_type: $type,
description: $description,
affected_systems: [],
affected_users: [],
reported_by: "cli-user"
}
print $"✓ Incident reported: ($response.incident_id)"
$response.incident_id
} catch {
error make --unspanned {
msg: $"Failed to report incident: ($in)"
}
}
}
# List security incidents
export def "compliance incident list" [
--severity: string # Filter by severity
--status: string # Filter by status
--type: string # Filter by type
--orchestrator-url: string = $ORCHESTRATOR_URL # Orchestrator URL
] {
mut query_params = []
if not ($severity | is-empty) {
$query_params = ($query_params | append $"severity=($severity)")
}
if not ($status | is-empty) {
$query_params = ($query_params | append $"status=($status)")
}
if not ($type | is-empty) {
$query_params = ($query_params | append $"incident_type=($type)")
}
let query_string = if ($query_params | length) > 0 {
$"?($query_params | str join '&')"
} else {
""
}
let url = $"($orchestrator_url)/api/v1/compliance/incidents($query_string)"
try {
http get $url
} catch {
error make --unspanned {
msg: $"Failed to list incidents: ($in)"
}
}
}
# Get incident details
export def "compliance incident show" [
incident_id: string # Incident ID
--orchestrator-url: string = $ORCHESTRATOR_URL # Orchestrator URL
] {
let url = $"($orchestrator_url)/api/v1/compliance/incidents/($incident_id)"
try {
http get $url | to json
} catch {
error make --unspanned {
msg: $"Failed to get incident: ($in)"
}
}
}
# ============================================================================
# Combined Reporting
# ============================================================================
# Generate combined compliance report
export def "compliance report" [
--format: string = "json" # Output format (json, yaml)
--output: string # Output file path
--orchestrator-url: string = $ORCHESTRATOR_URL # Orchestrator URL
] {
let url = $"($orchestrator_url)/api/v1/compliance/reports/combined"
print "Generating combined compliance report..."
print "This includes GDPR, SOC2, and ISO 27001 compliance status"
try {
let response = http get $url
let formatted = if $format == "yaml" {
$response | to yaml
} else {
$response | to json
}
if ($output | is-empty) {
$formatted
} else {
$formatted | save $output
print $"✓ Compliance report saved to: ($output)"
}
} catch {
error make --unspanned {
msg: $"Failed to generate report: ($in)"
}
}
}
# Check compliance health status
export def "compliance health" [
--orchestrator-url: string = $ORCHESTRATOR_URL # Orchestrator URL
] {
let url = $"($orchestrator_url)/api/v1/compliance/health"
try {
http get $url
} catch {
error make --unspanned {
msg: $"Failed to check health: ($in)"
}
}
}
# ============================================================================
# Helper Functions
# ============================================================================
# Show compliance command help
export def "compliance help" [] {
print "
Compliance CLI - GDPR, SOC2, and ISO 27001 Features
Usage:
compliance <category> <command> [options]
Categories:
gdpr - GDPR compliance (data subject rights)
soc2 - SOC2 Trust Service Criteria
iso27001 - ISO 27001 Annex A controls
protection - Data protection controls
access - Access control matrix
incident - Incident response
report - Combined compliance reporting
health - Health check
Examples:
# Export user data (GDPR)
compliance gdpr export user123
# Generate SOC2 report
compliance soc2 report --output soc2-report.json
# Generate ISO 27001 report
compliance iso27001 report --output iso27001-report.json
# Report security incident
compliance incident report --severity critical --type data_breach --description \"Unauthorized access detected\"
# Generate combined report
compliance report --output compliance-report.json
For detailed help on a specific command, use:
help compliance <category> <command>
"
}

6
nulib/kms/mod.nu Normal file
View File

@ -0,0 +1,6 @@
#!/usr/bin/env nu
# KMS Service Module
# Unified interface for Key Management Service operations
export use service.nu *

362
nulib/kms/service.nu Normal file
View File

@ -0,0 +1,362 @@
#!/usr/bin/env nu
# KMS Service CLI Integration
# Provides commands to interact with the KMS service API
# Get KMS service base URL from environment or use default
def kms-url [] -> string {
$env.KMS_SERVICE_URL? | default "http://localhost:8081"
}
# Build KMS API endpoint URL
def kms-endpoint [path: string] -> string {
$"(kms-url)/api/v1/kms/(path)"
}
# Encrypt data using KMS
#
# Examples:
# "secret-data" | kms encrypt
# "api-key" | kms encrypt --context "env=prod,service=api"
export def "kms encrypt" [
--context: string # Encryption context (key1=val1,key2=val2)
--backend: string = "vault" # KMS backend (vault or aws-kms)
] -> string {
let input = $in
# Encode plaintext to base64
let plaintext_b64 = $input | encode base64
# Prepare request body
let body = {
plaintext: $plaintext_b64
context: $context
}
# Make API request
let response = http post (kms-endpoint "encrypt") $body | complete
if $response.exit_code != 0 {
error make {
msg: "KMS encrypt request failed"
label: {
text: $response.stderr
span: (metadata $input).span
}
}
}
let result = $response.stdout | from json
if ($result | get -i error | is-not-empty) {
error make {
msg: "KMS encryption failed"
label: {
text: $result.error
span: (metadata $input).span
}
}
}
$result.ciphertext
}
# Decrypt data using KMS
#
# Examples:
# "vault:v1:..." | kms decrypt
# $ciphertext | kms decrypt --context "env=prod,service=api"
export def "kms decrypt" [
--context: string # Encryption context (must match encryption context)
] -> string {
let ciphertext = $in
# Prepare request body
let body = {
ciphertext: $ciphertext
context: $context
}
# Make API request
let response = http post (kms-endpoint "decrypt") $body | complete
if $response.exit_code != 0 {
error make {
msg: "KMS decrypt request failed"
label: {
text: $response.stderr
span: (metadata $ciphertext).span
}
}
}
let result = $response.stdout | from json
if ($result | get -i error | is-not-empty) {
error make {
msg: "KMS decryption failed"
label: {
text: $result.error
span: (metadata $ciphertext).span
}
}
}
# Decode base64 plaintext
$result.plaintext | decode base64
}
# Generate a data key for envelope encryption
#
# Examples:
# kms generate-key
# kms generate-key --key-spec AES_128
export def "kms generate-key" [
--key-spec: string = "AES_256" # Key specification (AES_128, AES_256, RSA_2048, RSA_4096)
] -> record {
let body = {
key_spec: $key_spec
}
let response = http post (kms-endpoint "generate-key") $body | complete
if $response.exit_code != 0 {
error make {
msg: "KMS generate-key request failed"
label: {
text: $response.stderr
}
}
}
let result = $response.stdout | from json
if ($result | get -i error | is-not-empty) {
error make {
msg: "KMS key generation failed"
label: {
text: $result.error
}
}
}
{
plaintext: $result.plaintext
ciphertext: $result.ciphertext
key_id: $result.key_id
}
}
# Rotate KMS key
#
# Examples:
# kms rotate-key "arn:aws:kms:..."
export def "kms rotate-key" [
key_id: string # Key ID to rotate
] -> record {
let body = {
key_id: $key_id
}
let response = http post (kms-endpoint "rotate-key") $body | complete
if $response.exit_code != 0 {
error make {
msg: "KMS rotate-key request failed"
label: {
text: $response.stderr
span: (metadata $key_id).span
}
}
}
let result = $response.stdout | from json
if ($result | get -i error | is-not-empty) {
error make {
msg: "KMS key rotation failed"
label: {
text: $result.error
span: (metadata $key_id).span
}
}
}
{
new_key_id: $result.new_key_id
success: $result.success
}
}
# Check KMS service health
#
# Examples:
# kms health
export def "kms health" [] -> record {
let response = http get (kms-endpoint "health") | complete
if $response.exit_code != 0 {
return {
status: "unhealthy"
error: $response.stderr
}
}
$response.stdout | from json
}
# Get KMS service status
#
# Examples:
# kms status
export def "kms status" [] -> record {
let response = http get (kms-endpoint "status") | complete
if $response.exit_code != 0 {
error make {
msg: "KMS status request failed"
label: {
text: $response.stderr
}
}
}
$response.stdout | from json
}
# Encrypt file using KMS
#
# Examples:
# kms encrypt-file config.yaml
# kms encrypt-file secrets.json --output secrets.enc --context "env=prod"
export def "kms encrypt-file" [
input_file: path # File to encrypt
--output: path # Output file (default: <input>.enc)
--context: string # Encryption context
] {
if not ($input_file | path exists) {
error make {
msg: "Input file not found"
label: {
text: $input_file
span: (metadata $input_file).span
}
}
}
let output_file = if ($output | is-empty) {
$"($input_file).enc"
} else {
$output
}
print $"Encrypting ($input_file)..."
# Read file and encrypt
let plaintext = open --raw $input_file
let ciphertext = $plaintext | kms encrypt --context $context
# Save encrypted data
$ciphertext | save --raw $output_file
print $"✓ Encrypted file saved to ($output_file)"
}
# Decrypt file using KMS
#
# Examples:
# kms decrypt-file config.yaml.enc
# kms decrypt-file secrets.enc --output secrets.json --context "env=prod"
export def "kms decrypt-file" [
input_file: path # File to decrypt
--output: path # Output file (default: remove .enc extension)
--context: string # Encryption context (must match)
] {
if not ($input_file | path exists) {
error make {
msg: "Input file not found"
label: {
text: $input_file
span: (metadata $input_file).span
}
}
}
let output_file = if ($output | is-empty) {
if ($input_file | str ends-with ".enc") {
$input_file | str replace ".enc" ""
} else {
$"($input_file).dec"
}
} else {
$output
}
print $"Decrypting ($input_file)..."
# Read encrypted data and decrypt
let ciphertext = open --raw $input_file
let plaintext = $ciphertext | kms decrypt --context $context
# Save decrypted data
$plaintext | save --raw $output_file
print $"✓ Decrypted file saved to ($output_file)"
}
# Envelope encrypt data (AWS KMS only)
#
# Examples:
# "large-data" | kms envelope encrypt
export def "kms envelope encrypt" [] -> string {
let input = $in
let plaintext_b64 = $input | encode base64
let body = { plaintext: $plaintext_b64 }
let response = http post (kms-endpoint "envelope/encrypt") $body | complete
if $response.exit_code != 0 {
error make {
msg: "KMS envelope encrypt request failed"
label: { text: $response.stderr }
}
}
let result = $response.stdout | from json
if ($result | get -i error | is-not-empty) {
error make {
msg: "KMS envelope encryption failed"
label: { text: $result.error }
}
}
$result.ciphertext
}
# Envelope decrypt data (AWS KMS only)
#
# Examples:
# $envelope_ciphertext | kms envelope decrypt
export def "kms envelope decrypt" [] -> string {
let ciphertext = $in
let body = { ciphertext: $ciphertext }
let response = http post (kms-endpoint "envelope/decrypt") $body | complete
if $response.exit_code != 0 {
error make {
msg: "KMS envelope decrypt request failed"
label: { text: $response.stderr }
}
}
let result = $response.stdout | from json
if ($result | get -i error | is-not-empty) {
error make {
msg: "KMS envelope decryption failed"
label: { text: $result.error }
}
}
$result.plaintext | decode base64
}

View File

@ -0,0 +1,394 @@
# Configuration Encryption CLI Commands
# Provides user-friendly commands for config encryption operations
use encryption.nu *
use accessor.nu *
# Encrypt a configuration file
export def "config encrypt" [
file: path # Configuration file to encrypt
--output (-o): path # Output path (default: <file>.enc)
--kms (-k): string = "age" # KMS backend: age, aws-kms, vault, cosmian
--in-place (-i) # Encrypt in-place (overwrites original)
--debug (-d) # Enable debug output
] {
if not ($file | path exists) {
print $"❌ File not found: ($file)"
return
}
print $"🔒 Encrypting configuration file: ($file)"
print $" Backend: ($kms)"
if $in_place {
print $" Mode: In-place (will overwrite original)"
} else if ($output | is-not-empty) {
print $" Output: ($output)"
} else {
print $" Output: ($file).enc"
}
try {
if $in_place {
encrypt-config $file --kms=$kms --in-place --debug=$debug
} else {
encrypt-config $file $output --kms=$kms --debug=$debug
}
} catch { |err|
print $"❌ Encryption failed: ($err.msg)"
}
}
# Decrypt a configuration file
export def "config decrypt" [
file: path # Encrypted configuration file
--output (-o): path # Output path (default: removes .enc extension)
--in-place (-i) # Decrypt in-place (overwrites original)
--debug (-d) # Enable debug output
] {
if not ($file | path exists) {
print $"❌ File not found: ($file)"
return
}
print $"🔓 Decrypting configuration file: ($file)"
try {
if $in_place {
decrypt-config $file --in-place --debug=$debug
} else {
decrypt-config $file $output --debug=$debug
}
} catch { |err|
print $"❌ Decryption failed: ($err.msg)"
}
}
# Edit encrypted configuration file securely
export def "config edit-secure" [
file: path # Encrypted configuration file
--editor (-e): string # Editor to use (default: $EDITOR or vim)
--debug (-d) # Enable debug output
] {
if not ($file | path exists) {
print $"❌ File not found: ($file)"
return
}
try {
if ($editor | is-not-empty) {
edit-encrypted-config $file --editor=$editor --debug=$debug
} else {
edit-encrypted-config $file --debug=$debug
}
} catch { |err|
print $"❌ Edit failed: ($err.msg)"
}
}
# Rotate encryption keys for a configuration file
export def "config rotate-keys" [
file: path # Encrypted configuration file
new_key: string # New key ID or recipient
--debug (-d) # Enable debug output
] {
if not ($file | path exists) {
print $"❌ File not found: ($file)"
return
}
print $"🔄 Rotating encryption keys"
print $" File: ($file)"
print $" New key: ($new_key)"
try {
rotate-encryption-keys $file $new_key --debug=$debug
} catch { |err|
print $"❌ Key rotation failed: ($err.msg)"
}
}
# Check if a configuration file is encrypted
export def "config is-encrypted" [
file: path # Configuration file to check
] {
if not ($file | path exists) {
print $"❌ File not found: ($file)"
return
}
let encrypted = (is-encrypted-config $file)
if $encrypted {
print $"🔒 File is encrypted: ($file)"
} else {
print $"🔓 File is not encrypted: ($file)"
}
$encrypted
}
# Validate encryption configuration and setup
export def "config validate-encryption" [] {
print $"🔍 Validating encryption configuration..."
print ""
let validation = (validate-encryption-config)
# Show summary
print $"📊 Encryption Configuration Summary"
print $"=================================="
print $" SOPS installed: ($validation.summary.sops_installed)"
print $" Age backend: ($validation.summary.age_backend)"
print $" KMS enabled: ($validation.summary.kms_enabled)"
print $" Errors: ($validation.summary.total_errors)"
print $" Warnings: ($validation.summary.total_warnings)"
print ""
# Show errors
if ($validation.errors | length) > 0 {
print $"❌ Errors:"
for error in $validation.errors {
print $" • ($error.message)"
}
print ""
}
# Show warnings
if ($validation.warnings | length) > 0 {
print $"⚠️ Warnings:"
for warning in $validation.warnings {
print $" • ($warning.message)"
}
print ""
}
if $validation.valid {
print $"✅ Encryption configuration is valid"
} else {
print $"❌ Encryption configuration has errors"
}
$validation
}
# Scan directory for unencrypted sensitive configurations
export def "config scan-sensitive" [
directory: path = "." # Directory to scan
--recursive (-r) # Scan recursively
] {
if not ($directory | path exists) {
print $"❌ Directory not found: ($directory)"
return
}
print $"🔍 Scanning for unencrypted sensitive configs in: ($directory)"
let results = (scan-unencrypted-configs $directory --recursive=$recursive)
if ($results | is-empty) {
print $"✅ No unencrypted sensitive configs found"
} else {
print $"\n⚠ Found ($results | length) unencrypted sensitive configs:"
print ""
print $results
print ""
print $"💡 Run 'config encrypt-all ($directory)' to encrypt them"
}
$results
}
# Encrypt all sensitive configurations in directory
export def "config encrypt-all" [
directory: path = "." # Directory to encrypt
--kms (-k): string = "age" # KMS backend: age, aws-kms, vault, cosmian
--recursive (-r) # Scan recursively
--dry-run (-n) # Dry run (no actual encryption)
] {
if not ($directory | path exists) {
print $"❌ Directory not found: ($directory)"
return
}
try {
encrypt-sensitive-configs $directory --kms=$kms --recursive=$recursive --dry-run=$dry_run
} catch { |err|
print $"❌ Bulk encryption failed: ($err.msg)"
}
}
# Show encryption information for configuration
export def "config encryption-info" [
file: path # Configuration file
] {
if not ($file | path exists) {
print $"❌ File not found: ($file)"
return
}
print $"📋 Encryption Information"
print $"========================"
print $" File: ($file)"
let encrypted = (is-encrypted-config $file)
print $" Encrypted: ($encrypted)"
if $encrypted {
# Try to extract SOPS metadata
try {
let content = (open $file --raw)
if ($content | str contains "sops:") {
print $" Type: SOPS encrypted"
# Extract some metadata (without decrypting)
if ($content | str contains "age:") {
print $" Backend: Age"
}
if ($content | str contains "kms:") {
print $" Backend: AWS KMS"
}
if ($content | str contains "vault:") {
print $" Backend: Vault"
}
}
} catch {
print $" Type: Encrypted (unknown format)"
}
} else {
let sensitive = (contains-sensitive-data $file)
print $" Contains sensitive data: ($sensitive)"
if $sensitive {
print ""
print $"⚠️ This file contains sensitive data but is not encrypted!"
print $"💡 Run 'config encrypt ($file)' to encrypt it"
}
}
}
# Initialize encryption setup (generate keys, create SOPS config)
export def "config init-encryption" [
--kms (-k): string = "age" # KMS backend to initialize: age, aws-kms, vault
--force (-f) # Force re-initialization
] {
print $"🔧 Initializing encryption setup with ($kms)"
print ""
match $kms {
"age" => {
# Check if age is installed
let age_check = (^which age | complete)
if $age_check.exit_code != 0 {
print $"❌ Age is not installed"
print $"💡 Install with: brew install age"
return
}
# Generate age key if not exists
let age_key_file = ($env.HOME | path join ".config" | path join "sops" | path join "age" | path join "keys.txt")
let age_key_dir = ($age_key_file | path dirname)
if ($age_key_file | path exists) and not $force {
print $"✅ Age key already exists: ($age_key_file)"
print $" Use --force to regenerate"
} else {
# Create directory
mkdir $age_key_dir
# Generate new age key
print $"🔑 Generating new Age key..."
let key_output = (^age-keygen -o $age_key_file | complete)
if $key_output.exit_code == 0 {
print $"✅ Age key generated: ($age_key_file)"
# Extract recipient
let key_content = (open $age_key_file --raw)
let recipient = ($key_content | lines | where ($it | str starts-with "# public key:") | first | split row ": " | get 1)
print $" Public key (recipient): ($recipient)"
print ""
print $"💡 Set this environment variable:"
print $" export SOPS_AGE_RECIPIENTS=($recipient)"
} else {
print $"❌ Failed to generate Age key"
return
}
}
# Create .sops.yaml if not exists
let sops_config = ($env.PWD | path join ".sops.yaml")
if not ($sops_config | path exists) or $force {
print $"📝 Creating SOPS configuration: ($sops_config)"
let key_content = (open $age_key_file --raw)
let recipient = ($key_content | lines | where ($it | str starts-with "# public key:") | first | split row ": " | get 1)
let sops_yaml = $"creation_rules:
- path_regex: .*\\.enc\\.yaml$
age: ($recipient)
- path_regex: .*\\.enc\\.yml$
age: ($recipient)
- path_regex: .*\\.enc\\.toml$
age: ($recipient)
- path_regex: .*\\.enc\\.json$
age: ($recipient)
- path_regex: workspace/.*/config/secure\\.yaml$
age: ($recipient)
"
$sops_yaml | save --force $sops_config
print $"✅ SOPS configuration created"
}
}
"aws-kms" => {
print $"⚠️ AWS KMS requires manual configuration"
print $"💡 Follow these steps:"
print $" 1. Create KMS key in AWS Console"
print $" 2. Update .sops.yaml with KMS ARN"
print $" 3. Configure AWS credentials"
}
"vault" | "cosmian" => {
print $"⚠️ ($kms) KMS requires manual configuration"
print $"💡 Configure KMS settings in config file"
}
_ => {
print $"❌ Unknown KMS backend: ($kms)"
print $" Supported: age, aws-kms, vault, cosmian"
}
}
print ""
print $"✅ Encryption initialization completed"
}
# Main help command
export def main [] {
print "Configuration Encryption Commands"
print "================================="
print ""
print "Encryption/Decryption:"
print " config encrypt <file> Encrypt configuration file"
print " config decrypt <file> Decrypt configuration file"
print " config edit-secure <file> Edit encrypted file securely"
print " config rotate-keys <file> Rotate encryption keys"
print ""
print "Information:"
print " config is-encrypted <file> Check if file is encrypted"
print " config encryption-info <file> Show encryption details"
print " config validate-encryption Validate encryption setup"
print ""
print "Bulk Operations:"
print " config scan-sensitive <dir> Find unencrypted sensitive configs"
print " config encrypt-all <dir> Encrypt all sensitive configs"
print ""
print "Setup:"
print " config init-encryption Initialize encryption (generate keys)"
print ""
print "Examples:"
print " config encrypt workspace/config/secure.yaml"
print " config edit-secure workspace/config/secure.enc.yaml"
print " config scan-sensitive workspace/config --recursive"
print " config init-encryption --kms age"
}

View File

@ -0,0 +1,505 @@
# Configuration Encryption Module for Provisioning System
# Provides transparent encryption/decryption for configuration files using SOPS
use std log
use ../sops/lib.nu *
use ../kms/lib.nu *
use accessor.nu *
# Detect if a config file is encrypted
export def is-encrypted-config [
file_path: string
]: nothing -> bool {
if not ($file_path | path exists) {
return false
}
# Check if file has SOPS metadata
is_sops_file $file_path
}
# Load configuration file with automatic decryption
export def load-encrypted-config [
file_path: string
--debug = false
]: nothing -> record {
if not ($file_path | path exists) {
error make {
msg: $"Configuration file not found: ($file_path)"
}
}
if $debug {
print $"Loading configuration file: ($file_path)"
}
# Check if file is encrypted
if (is-encrypted-config $file_path) {
if $debug {
print $" Detected encrypted file, decrypting..."
}
# Decrypt in memory (never write to disk)
let decrypted_content = (decrypt-config-memory $file_path --debug=$debug)
# Parse based on file extension
let ext = ($file_path | path parse | get extension)
match $ext {
"yaml" | "yml" => ($decrypted_content | from yaml)
"toml" => ($decrypted_content | from toml)
"json" => ($decrypted_content | from json)
_ => {
error make {
msg: $"Unsupported encrypted config format: ($ext)"
}
}
}
} else {
if $debug {
print $" Loading unencrypted file..."
}
# Load unencrypted file normally
open $file_path
}
}
# Decrypt configuration file in memory only (never writes to disk)
export def decrypt-config-memory [
file_path: string
--debug = false
]: nothing -> string {
if not (is-encrypted-config $file_path) {
error make {
msg: $"File is not encrypted: ($file_path)"
}
}
# Use SOPS to decrypt (output goes to stdout, captured in memory)
let decrypted = (on_sops "decrypt" $file_path --quiet)
if ($decrypted | is-empty) {
error make {
msg: $"Failed to decrypt configuration file: ($file_path)"
}
}
$decrypted
}
# Encrypt a configuration file
export def encrypt-config [
source_path: string
output_path?: string
--kms: string = "age" # age, aws-kms, vault
--in-place = false
--debug = false
]: nothing -> nothing {
if not ($source_path | path exists) {
error make {
msg: $"Source file not found: ($source_path)"
}
}
# Check if already encrypted
if (is-encrypted-config $source_path) {
print $"⚠️ File is already encrypted: ($source_path)"
return
}
# Determine output path
let target = if $in_place {
$source_path
} else if ($output_path | is-not-empty) {
$output_path
} else {
$"($source_path).enc"
}
if $debug {
print $"Encrypting ($source_path) → ($target) using ($kms)"
}
# Encrypt based on KMS backend
match $kms {
"age" => {
let encrypted = (on_sops "encrypt" $source_path)
if ($encrypted | is-empty) {
error make {
msg: $"Failed to encrypt file with age: ($source_path)"
}
}
$encrypted | save --force $target
print $"✅ Encrypted successfully: ($target)"
}
"aws-kms" => {
# For AWS KMS, SOPS will use AWS KMS backend if configured
let encrypted = (on_sops "encrypt" $source_path)
if ($encrypted | is-empty) {
error make {
msg: $"Failed to encrypt file with AWS KMS: ($source_path)"
}
}
$encrypted | save --force $target
print $"✅ Encrypted successfully with AWS KMS: ($target)"
}
"vault" | "cosmian" => {
# Use KMS client for Vault or Cosmian KMS
let encrypted = (on_kms "encrypt" $source_path)
if ($encrypted | is-empty) {
error make {
msg: $"Failed to encrypt file with ($kms): ($source_path)"
}
}
$encrypted | save --force $target
print $"✅ Encrypted successfully with ($kms): ($target)"
}
_ => {
error make {
msg: $"Unsupported KMS backend: ($kms). Supported: age, aws-kms, vault, cosmian"
}
}
}
}
# Decrypt a configuration file
export def decrypt-config [
source_path: string
output_path?: string
--in-place = false
--debug = false
]: nothing -> nothing {
if not ($source_path | path exists) {
error make {
msg: $"Source file not found: ($source_path)"
}
}
# Check if encrypted
if not (is-encrypted-config $source_path) {
print $"⚠️ File is not encrypted: ($source_path)"
return
}
# Determine output path
let target = if $in_place {
$source_path
} else if ($output_path | is-not-empty) {
$output_path
} else {
# Remove .enc extension if present
if ($source_path | str ends-with ".enc") {
$source_path | str substring 0..-5
} else {
$"($source_path).dec"
}
}
if $debug {
print $"Decrypting ($source_path) → ($target)"
}
# Decrypt using SOPS (works with any backend)
let decrypted = (on_sops "decrypt" $source_path)
if ($decrypted | is-empty) {
error make {
msg: $"Failed to decrypt file: ($source_path)"
}
}
$decrypted | save --force $target
print $"✅ Decrypted successfully: ($target)"
}
# Edit encrypted config file (decrypt, edit, re-encrypt)
export def edit-encrypted-config [
file_path: string
--editor: string = ""
--debug = false
]: nothing -> nothing {
if not ($file_path | path exists) {
error make {
msg: $"File not found: ($file_path)"
}
}
# Check if encrypted
if not (is-encrypted-config $file_path) {
print $"⚠️ File is not encrypted, opening normally..."
let editor_cmd = if ($editor | is-not-empty) {
$editor
} else {
$env.EDITOR? | default "vim"
}
^$editor_cmd $file_path
return
}
# Use SOPS editor functionality (handles decrypt -> edit -> encrypt)
print $"🔒 Opening encrypted file with SOPS editor..."
# SOPS has built-in editor support
let sops_config = (find-sops-config-path)
if ($sops_config | is-not-empty) {
^sops --config $sops_config $file_path
} else {
^sops $file_path
}
print $"✅ File saved and re-encrypted"
}
# Rotate encryption keys for a file
export def rotate-encryption-keys [
file_path: string
new_key_id: string
--debug = false
]: nothing -> nothing {
if not ($file_path | path exists) {
error make {
msg: $"File not found: ($file_path)"
}
}
if not (is-encrypted-config $file_path) {
error make {
msg: $"File is not encrypted: ($file_path)"
}
}
print $"🔄 Rotating encryption keys for ($file_path)"
print $" New key: ($new_key_id)"
# Create temporary decrypted file
let temp_file = ($file_path | str replace ".yaml" ".tmp.yaml")
try {
# Decrypt to temp
let decrypted = (on_sops "decrypt" $file_path)
$decrypted | save --force $temp_file
# Update SOPS config to use new key
# This requires updating .sops.yaml to reference new key
print $"⚠️ Manual step required: Update .sops.yaml with new key ($new_key_id)"
print $" Then run: sops updatekeys ($file_path)"
# Re-encrypt with new key
^sops updatekeys $file_path
# Remove temp file
rm --force $temp_file
print $"✅ Key rotation completed"
} catch {
# Clean up temp file on error
if ($temp_file | path exists) {
rm --force $temp_file
}
error make {
msg: $"Key rotation failed for ($file_path)"
}
}
}
# Validate encryption configuration
export def validate-encryption-config []: nothing -> record {
mut errors = []
mut warnings = []
# Check if SOPS is installed
let sops_check = (^which sops | complete)
if $sops_check.exit_code != 0 {
$errors = ($errors | append {
type: "missing_binary"
severity: "error"
message: "SOPS binary not found. Install with: brew install sops"
})
}
# Check if Age is installed (if using Age)
let use_sops = (get-provisioning-use-sops)
if ($use_sops | str contains "age") {
let age_check = (^which age | complete)
if $age_check.exit_code != 0 {
$warnings = ($warnings | append {
type: "missing_binary"
severity: "warning"
message: "Age binary not found. Install with: brew install age"
})
}
# Check for Age keys
let age_key_file = (get-sops-age-key-file)
if ($age_key_file | is-empty) {
$warnings = ($warnings | append {
type: "missing_key"
severity: "warning"
message: "Age key file not configured (PROVISIONING_KAGE)"
})
} else if not ($age_key_file | path exists) {
$errors = ($errors | append {
type: "missing_key_file"
severity: "error"
message: $"Age key file not found: ($age_key_file)"
})
}
}
# Check SOPS configuration file
let sops_config = (find-sops-config-path)
if ($sops_config | is-empty) {
$warnings = ($warnings | append {
type: "missing_config"
severity: "warning"
message: "SOPS config file (.sops.yaml) not found in project"
})
}
# Check KMS configuration if enabled
let kms_enabled = (get-kms-enabled)
if $kms_enabled {
let kms_server = (get-kms-server)
if ($kms_server | is-empty) {
$errors = ($errors | append {
type: "missing_kms_config"
severity: "error"
message: "KMS enabled but server URL not configured"
})
}
}
{
valid: (($errors | length) == 0)
errors: $errors
warnings: $warnings
summary: {
sops_installed: ($sops_check.exit_code == 0)
age_backend: ($use_sops | str contains "age")
kms_enabled: $kms_enabled
total_errors: ($errors | length)
total_warnings: ($warnings | length)
}
}
}
# Find SOPS configuration file
def find-sops-config-path []: nothing -> string {
# Check common locations
let locations = [
".sops.yaml"
".sops.yml"
($env.PWD | path join ".sops.yaml")
($env.HOME | path join ".config" | path join "provisioning" | path join "sops.yaml")
(get-sops-config-path)
]
for loc in $locations {
if ($loc | path exists) {
return $loc
}
}
""
}
# Check if config file contains sensitive data (heuristic)
export def contains-sensitive-data [
file_path: string
]: nothing -> bool {
if not ($file_path | path exists) {
return false
}
let content = (open $file_path --raw)
# Patterns that indicate sensitive data
let sensitive_patterns = [
"password" "secret" "api_key" "token" "private_key"
"credential" "auth" "access_key" "secret_key"
]
for pattern in $sensitive_patterns {
if ($content | str contains $pattern) {
return true
}
}
false
}
# Scan directory for unencrypted sensitive configs
export def scan-unencrypted-configs [
directory: string
--recursive = true
]: nothing -> table {
mut results = []
let files = if $recursive {
glob $"($directory)/**/*.{yaml,yml,toml,json}"
} else {
glob $"($directory)/*.{yaml,yml,toml,json}"
}
for file in $files {
if (contains-sensitive-data $file) and not (is-encrypted-config $file) {
$results = ($results | append {
file: $file
encrypted: false
has_sensitive_data: true
recommendation: "Should be encrypted"
})
}
}
$results
}
# Encrypt all sensitive configs in directory
export def encrypt-sensitive-configs [
directory: string
--kms: string = "age"
--dry-run = false
--recursive = true
]: nothing -> nothing {
print $"🔍 Scanning for unencrypted sensitive configs in ($directory)"
let unencrypted = (scan-unencrypted-configs $directory --recursive=$recursive)
if ($unencrypted | is-empty) {
print $"✅ No unencrypted sensitive configs found"
return
}
print $"\n📋 Found ($unencrypted | length) unencrypted sensitive configs:"
print $unencrypted
if $dry_run {
print $"\n⚠ Dry run mode - no files will be encrypted"
return
}
print $"\n🔒 Encrypting files with ($kms)..."
for file in ($unencrypted | get file) {
print $" Encrypting: ($file)"
encrypt-config $file --kms $kms --in-place
}
print $"\n✅ Encryption completed for all sensitive configs"
}
# Export key functions for CLI integration
export def main [] {
print "Configuration Encryption Module"
print "================================"
print ""
print "Available commands:"
print " - is-encrypted-config <file> Check if config is encrypted"
print " - load-encrypted-config <file> Load and decrypt config"
print " - encrypt-config <file> Encrypt config file"
print " - decrypt-config <file> Decrypt config file"
print " - edit-encrypted-config <file> Edit encrypted config"
print " - rotate-encryption-keys <file> Rotate encryption keys"
print " - validate-encryption-config Validate encryption setup"
print " - scan-unencrypted-configs <dir> Find unencrypted sensitive configs"
print " - encrypt-sensitive-configs <dir> Encrypt all sensitive configs"
}

View File

@ -0,0 +1,589 @@
# Configuration Encryption System Tests
# Comprehensive test suite for encryption functionality
use encryption.nu *
use ../kms/client.nu *
# Test suite runner
export def run-encryption-tests [
--verbose (-v) # Verbose output
--test: string = "" # Run specific test (empty = all)
] {
print "🧪 Configuration Encryption Test Suite"
print "======================================"
print ""
mut results = []
# Test 1: Encryption detection
if ($test == "" or $test == "detection") {
print "🔍 Test 1: Encryption Detection"
let result = (test-encryption-detection)
$results = ($results | append $result)
show-test-result $result
print ""
}
# Test 2: Encrypt/Decrypt round-trip
if ($test == "" or $test == "roundtrip") {
print "🔍 Test 2: Encrypt/Decrypt Round-trip"
let result = (test-encrypt-decrypt-roundtrip)
$results = ($results | append $result)
show-test-result $result
print ""
}
# Test 3: Memory-only decryption
if ($test == "" or $test == "memory") {
print "🔍 Test 3: Memory-Only Decryption"
let result = (test-memory-only-decryption)
$results = ($results | append $result)
show-test-result $result
print ""
}
# Test 4: Sensitive data detection
if ($test == "" or $test == "sensitive") {
print "🔍 Test 4: Sensitive Data Detection"
let result = (test-sensitive-data-detection)
$results = ($results | append $result)
show-test-result $result
print ""
}
# Test 5: KMS backend integration
if ($test == "" or $test == "kms") {
print "🔍 Test 5: KMS Backend Integration"
let result = (test-kms-backend-integration)
$results = ($results | append $result)
show-test-result $result
print ""
}
# Test 6: Config loader integration
if ($test == "" or $test == "loader") {
print "🔍 Test 6: Config Loader Integration"
let result = (test-config-loader-integration)
$results = ($results | append $result)
show-test-result $result
print ""
}
# Test 7: Validation
if ($test == "" or $test == "validation") {
print "🔍 Test 7: Encryption Validation"
let result = (test-encryption-validation)
$results = ($results | append $result)
show-test-result $result
print ""
}
# Summary
print "📊 Test Summary"
print "=============="
let total = ($results | length)
let passed = ($results | where passed == true | length)
let failed = ($total - $passed)
print $" Total tests: ($total)"
print $" Passed: ($passed)"
print $" Failed: ($failed)"
print $" Success rate: (($passed * 100) / $total)%"
if $failed == 0 {
print ""
print "✅ All tests passed!"
} else {
print ""
print "❌ Some tests failed:"
for result in ($results | where passed == false) {
print $" • ($result.test_name): ($result.error)"
}
}
{
total: $total
passed: $passed
failed: $failed
results: $results
}
}
# Test 1: Encryption detection
def test-encryption-detection []: nothing -> record {
let test_name = "Encryption Detection"
try {
# Create test file
let test_file = "/tmp/test_plain.yaml"
"test: value" | save --force $test_file
# Should detect as not encrypted
let is_enc = (is-encrypted-config $test_file)
rm --force $test_file
if $is_enc {
return {
test_name: $test_name
passed: false
error: "Plain file incorrectly detected as encrypted"
}
}
{
test_name: $test_name
passed: true
error: null
}
} catch { |err|
{
test_name: $test_name
passed: false
error: $err.msg
}
}
}
# Test 2: Encrypt/Decrypt round-trip
def test-encrypt-decrypt-roundtrip []: nothing -> record {
let test_name = "Encrypt/Decrypt Round-trip"
try {
# Create test file
let test_file = "/tmp/test_roundtrip.yaml"
let original_content = "test_key: secret_value"
$original_content | save --force $test_file
# Check if age is available
let age_check = (^which age | complete)
if $age_check.exit_code != 0 {
rm --force $test_file
return {
test_name: $test_name
passed: true
error: "Skipped: Age not installed"
skipped: true
}
}
# Check if SOPS recipients configured
let recipients = ($env.SOPS_AGE_RECIPIENTS? | default "")
if ($recipients | is-empty) {
rm --force $test_file
return {
test_name: $test_name
passed: true
error: "Skipped: SOPS_AGE_RECIPIENTS not configured"
skipped: true
}
}
# Encrypt the file
let encrypted_file = $"($test_file).enc"
encrypt-config $test_file $encrypted_file --kms="age"
# Verify it's encrypted
if not (is-encrypted-config $encrypted_file) {
rm --force $test_file $encrypted_file
return {
test_name: $test_name
passed: false
error: "File not encrypted after encrypt-config"
}
}
# Decrypt the file
let decrypted_file = $"($test_file).dec"
decrypt-config $encrypted_file $decrypted_file
# Verify content matches
let decrypted_content = (open $decrypted_file --raw)
# Cleanup
rm --force $test_file $encrypted_file $decrypted_file
if $decrypted_content != $original_content {
return {
test_name: $test_name
passed: false
error: "Decrypted content doesn't match original"
}
}
{
test_name: $test_name
passed: true
error: null
}
} catch { |err|
{
test_name: $test_name
passed: false
error: $err.msg
}
}
}
# Test 3: Memory-only decryption
def test-memory-only-decryption []: nothing -> record {
let test_name = "Memory-Only Decryption"
try {
# Check if age is available
let age_check = (^which age | complete)
if $age_check.exit_code != 0 {
return {
test_name: $test_name
passed: true
error: "Skipped: Age not installed"
skipped: true
}
}
# Check if SOPS recipients configured
let recipients = ($env.SOPS_AGE_RECIPIENTS? | default "")
if ($recipients | is-empty) {
return {
test_name: $test_name
passed: true
error: "Skipped: SOPS_AGE_RECIPIENTS not configured"
skipped: true
}
}
# Create and encrypt test file
let test_file = "/tmp/test_memory.yaml"
let original_content = "secret: memory_test"
$original_content | save --force $test_file
let encrypted_file = $"($test_file).enc"
encrypt-config $test_file $encrypted_file --kms="age"
# Decrypt in memory
let decrypted_memory = (decrypt-config-memory $encrypted_file)
# Cleanup
rm --force $test_file $encrypted_file
# Verify no decrypted file was created
if ($"($encrypted_file).dec" | path exists) {
return {
test_name: $test_name
passed: false
error: "Decrypted file was created (should be memory-only)"
}
}
# Verify decrypted content
if $decrypted_memory != $original_content {
return {
test_name: $test_name
passed: false
error: "Memory-decrypted content doesn't match original"
}
}
{
test_name: $test_name
passed: true
error: null
}
} catch { |err|
{
test_name: $test_name
passed: false
error: $err.msg
}
}
}
# Test 4: Sensitive data detection
def test-sensitive-data-detection []: nothing -> record {
let test_name = "Sensitive Data Detection"
try {
# Create test file with sensitive data
let test_file = "/tmp/test_sensitive.yaml"
let sensitive_content = "api_key: secret123\npassword: mypassword"
$sensitive_content | save --force $test_file
# Should detect sensitive data
let has_sensitive = (contains-sensitive-data $test_file)
# Create test file without sensitive data
let test_file_safe = "/tmp/test_safe.yaml"
let safe_content = "name: test\nvalue: 123"
$safe_content | save --force $test_file_safe
# Should not detect sensitive data
let has_no_sensitive = not (contains-sensitive-data $test_file_safe)
# Cleanup
rm --force $test_file $test_file_safe
if not ($has_sensitive and $has_no_sensitive) {
return {
test_name: $test_name
passed: false
error: "Sensitive data detection not working correctly"
}
}
{
test_name: $test_name
passed: true
error: null
}
} catch { |err|
{
test_name: $test_name
passed: false
error: $err.msg
}
}
}
# Test 5: KMS backend integration
def test-kms-backend-integration []: nothing -> record {
let test_name = "KMS Backend Integration"
try {
# Test detection
let backend = (detect-kms-backend)
if ($backend | is-empty) {
return {
test_name: $test_name
passed: false
error: "Failed to detect KMS backend"
}
}
# Test status
let status = (kms-status)
if ($status.backend | is-empty) {
return {
test_name: $test_name
passed: false
error: "KMS status check failed"
}
}
{
test_name: $test_name
passed: true
error: null
details: {
detected_backend: $backend
status: $status
}
}
} catch { |err|
{
test_name: $test_name
passed: false
error: $err.msg
}
}
}
# Test 6: Config loader integration
def test-config-loader-integration []: nothing -> record {
let test_name = "Config Loader Integration"
try {
# Create test directory
mkdir /tmp/test_config_loader
# Create plain config
let plain_config = "/tmp/test_config_loader/plain.yaml"
{test: "plain", value: 123} | to yaml | save --force $plain_config
# Load plain config (should work)
use loader.nu
let loaded_plain = (loader load-config-file $plain_config false false "yaml")
if ($loaded_plain.test != "plain") {
rm --force --recursive /tmp/test_config_loader
return {
test_name: $test_name
passed: false
error: "Failed to load plain config through loader"
}
}
# Cleanup
rm --force --recursive /tmp/test_config_loader
{
test_name: $test_name
passed: true
error: null
}
} catch { |err|
{
test_name: $test_name
passed: false
error: $err.msg
}
}
}
# Test 7: Encryption validation
def test-encryption-validation []: nothing -> record {
let test_name = "Encryption Validation"
try {
# Run validation
let validation = (validate-encryption-config)
# Check that validation returns expected structure
if not (($validation | columns) | all { |col| $col in ["valid", "errors", "warnings", "summary"] }) {
return {
test_name: $test_name
passed: false
error: "Validation result structure incorrect"
}
}
{
test_name: $test_name
passed: true
error: null
details: $validation.summary
}
} catch { |err|
{
test_name: $test_name
passed: false
error: $err.msg
}
}
}
# Show test result
def show-test-result [result: record] {
if $result.passed {
print $" ✅ ($result.test_name)"
if ($result | get -o skipped) == true {
print $" ⚠️ ($result.error)"
}
} else {
print $" ❌ ($result.test_name)"
print $" Error: ($result.error)"
}
}
# Integration test with full workflow
export def test-full-encryption-workflow [] {
print "🧪 Full Encryption Workflow Test"
print "================================="
print ""
let test_dir = "/tmp/test_encryption_workflow"
mkdir $test_dir
try {
# Step 1: Create test config with sensitive data
print "📝 Step 1: Creating test configuration"
let config_file = $"($test_dir)/secure.yaml"
{
database: {
host: "localhost"
password: "supersecret123"
api_key: "key_abc123"
}
aws: {
access_key: "AKIAIOSFODNN7EXAMPLE"
secret_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
}
} | to yaml | save --force $config_file
print " ✅ Config created"
# Step 2: Detect sensitive data
print "🔍 Step 2: Detecting sensitive data"
let has_sensitive = (contains-sensitive-data $config_file)
if $has_sensitive {
print " ✅ Sensitive data detected"
} else {
print " ❌ Failed to detect sensitive data"
rm --force --recursive $test_dir
return
}
# Step 3: Check encryption prerequisites
print "🔧 Step 3: Checking encryption prerequisites"
let validation = (validate-encryption-config)
if $validation.valid {
print " ✅ Encryption system configured"
} else {
print " ⚠️ Encryption system has issues:"
for error in $validation.errors {
print $" • ($error.message)"
}
}
# Step 4: Encrypt config (if possible)
print "🔒 Step 4: Encrypting configuration"
let recipients = ($env.SOPS_AGE_RECIPIENTS? | default "")
if ($recipients | is-not-empty) {
try {
encrypt-config $config_file --in-place --kms="age"
print " ✅ Configuration encrypted"
# Step 5: Verify encryption
print "🔍 Step 5: Verifying encryption"
let is_enc = (is-encrypted-config $config_file)
if $is_enc {
print " ✅ Encryption verified"
} else {
print " ❌ File not encrypted"
}
# Step 6: Load encrypted config
print "📖 Step 6: Loading encrypted config"
let loaded = (load-encrypted-config $config_file)
if ($loaded.database.password == "supersecret123") {
print " ✅ Decrypted and loaded correctly"
} else {
print " ❌ Failed to load encrypted config"
}
} catch { |err|
print $" ❌ Encryption failed: ($err.msg)"
}
} else {
print " ⚠️ Skipped: SOPS_AGE_RECIPIENTS not configured"
}
print ""
print "✅ Workflow test completed"
} catch { |err|
print $"❌ Workflow test failed: ($err.msg)"
}
# Cleanup
rm --force --recursive $test_dir
}
# Main help
export def main [] {
print "Configuration Encryption Tests"
print "=============================="
print ""
print "Commands:"
print " run-encryption-tests Run all tests"
print " run-encryption-tests --test <name> Run specific test"
print " test-full-encryption-workflow Run complete workflow test"
print ""
print "Available tests:"
print " detection - Encryption detection"
print " roundtrip - Encrypt/decrypt round-trip"
print " memory - Memory-only decryption"
print " sensitive - Sensitive data detection"
print " kms - KMS backend integration"
print " loader - Config loader integration"
print " validation - Encryption validation"
}

View File

@ -164,7 +164,7 @@ export def load-provisioning-config [
$final_config
}
# Load a single configuration file (supports YAML and TOML)
# Load a single configuration file (supports YAML and TOML with automatic decryption)
export def load-config-file [
file_path: string
required = false
@ -188,32 +188,67 @@ export def load-config-file [
# log debug $"Loading config file: ($file_path)"
}
# Determine format from file extension if auto
let file_format = if $format == "auto" {
let ext = ($file_path | path parse | get extension)
match $ext {
"yaml" | "yml" => "yaml"
"toml" => "toml"
_ => "toml" # default to toml for backward compatibility
# Check if file is encrypted and auto-decrypt
# Inline SOPS detection to avoid circular import
if (check-if-sops-encrypted $file_path) {
if $debug {
# log debug $"Detected encrypted config, decrypting in memory: ($file_path)"
}
} else {
$format
}
# Load the file with appropriate parser
if ($file_path | path exists) {
match $file_format {
"yaml" => (open $file_path)
"toml" => (open $file_path)
_ => (open $file_path)
try {
# Decrypt in memory using SOPS
let decrypted_content = (decrypt-sops-file $file_path)
if ($decrypted_content | is-empty) {
if $debug {
print $"⚠️ Failed to decrypt ($file_path), attempting to load as plain file"
}
open $file_path
} else {
# Parse based on file extension
let ext = ($file_path | path parse | get extension)
match $ext {
"yaml" | "yml" => ($decrypted_content | from yaml)
"toml" => ($decrypted_content | from toml)
"json" => ($decrypted_content | from json)
_ => ($decrypted_content | from yaml) # default to yaml
}
}
} catch {
if $debug {
print $"⚠️ Failed to decrypt ($file_path), attempting to load as plain file"
}
# Fallback to regular loading if decryption fails
open $file_path
}
} else {
if $required {
error make {
msg: $"Configuration file not found: ($file_path)"
# Determine format from file extension if auto
let file_format = if $format == "auto" {
let ext = ($file_path | path parse | get extension)
match $ext {
"yaml" | "yml" => "yaml"
"toml" => "toml"
_ => "toml" # default to toml for backward compatibility
}
} else {
{}
$format
}
# Load unencrypted file with appropriate parser
if ($file_path | path exists) {
match $file_format {
"yaml" => (open $file_path)
"toml" => (open $file_path)
_ => (open $file_path)
}
} else {
if $required {
error make {
msg: $"Configuration file not found: ($file_path)"
}
} else {
{}
}
}
}
}
@ -1747,6 +1782,60 @@ def update-workspace-last-used-internal [workspace_name: string] {
}
}
# Check if file is SOPS encrypted (inline to avoid circular import)
def check-if-sops-encrypted [file_path: string]: nothing -> bool {
if not ($file_path | path exists) {
return false
}
let file_content = (open $file_path --raw)
# Check for SOPS markers
if ($file_content | str contains "sops:") and ($file_content | str contains "ENC[") {
return true
}
false
}
# Decrypt SOPS file (inline to avoid circular import)
def decrypt-sops-file [file_path: string]: nothing -> string {
# Find SOPS config
let sops_config = find-sops-config-path
# Decrypt using SOPS binary
let result = if ($sops_config | is-not-empty) {
^sops --decrypt --config $sops_config $file_path | complete
} else {
^sops --decrypt $file_path | complete
}
if $result.exit_code != 0 {
return ""
}
$result.stdout
}
# Find SOPS configuration file
def find-sops-config-path []: nothing -> string {
# Check common locations
let locations = [
".sops.yaml"
".sops.yml"
($env.PWD | path join ".sops.yaml")
($env.HOME | path join ".config" | path join "provisioning" | path join "sops.yaml")
]
for loc in $locations {
if ($loc | path exists) {
return $loc
}
}
""
}
# Get active workspace from user config
# CRITICAL: This replaces get-defaults-config-path
def get-active-workspace [] {

View File

@ -6,6 +6,10 @@ export use loader.nu *
export use accessor.nu *
export use migration.nu *
# Encryption functionality
export use encryption.nu *
export use commands.nu *
# Convenience function to get the complete configuration
export def config [] {
get-config

View File

@ -0,0 +1,547 @@
# KMS Client Interface for Multiple Backends
# Provides unified interface for Age, AWS KMS, HashiCorp Vault, and Cosmian KMS
use std log
use ../config/accessor.nu *
use ../utils/error.nu throw-error
use ../utils/interface.nu _print
# KMS Client for encryption/decryption operations
export def kms-encrypt [
data: string # Data to encrypt
key_id?: string # Key ID (backend-specific)
--backend: string = "" # age, aws-kms, vault, cosmian (auto-detect if empty)
--output-format: string = "base64" # base64, hex, binary
]: nothing -> string {
let kms_backend = if ($backend | is-empty) {
detect-kms-backend
} else {
$backend
}
match $kms_backend {
"age" => {
kms-encrypt-age $data $key_id --output-format=$output_format
}
"aws-kms" => {
kms-encrypt-aws $data $key_id --output-format=$output_format
}
"vault" => {
kms-encrypt-vault $data $key_id --output-format=$output_format
}
"cosmian" => {
kms-encrypt-cosmian $data $key_id --output-format=$output_format
}
_ => {
error make {
msg: $"Unsupported KMS backend: ($kms_backend)"
}
}
}
}
# Decrypt data using KMS
export def kms-decrypt [
encrypted_data: string # Encrypted data
key_id?: string # Key ID (backend-specific)
--backend: string = "" # age, aws-kms, vault, cosmian (auto-detect if empty)
--input-format: string = "base64" # base64, hex, binary
]: nothing -> string {
let kms_backend = if ($backend | is-empty) {
detect-kms-backend
} else {
$backend
}
match $kms_backend {
"age" => {
kms-decrypt-age $encrypted_data $key_id --input-format=$input_format
}
"aws-kms" => {
kms-decrypt-aws $encrypted_data $key_id --input-format=$input_format
}
"vault" => {
kms-decrypt-vault $encrypted_data $key_id --input-format=$input_format
}
"cosmian" => {
kms-decrypt-cosmian $encrypted_data $key_id --input-format=$input_format
}
_ => {
error make {
msg: $"Unsupported KMS backend: ($kms_backend)"
}
}
}
}
# Age backend encryption
def kms-encrypt-age [
data: string
key_id?: string
--output-format: string = "base64"
]: nothing -> string {
# Get Age recipients
let recipients = if ($key_id | is-not-empty) {
$key_id
} else {
$env.SOPS_AGE_RECIPIENTS? | default (get-sops-age-recipients)
}
if ($recipients | is-empty) {
error make {
msg: "No Age recipients configured"
}
}
# Encrypt with age
let encrypted = ($data | ^age -r $recipients -a | complete)
if $encrypted.exit_code != 0 {
error make {
msg: $"Age encryption failed: ($encrypted.stderr)"
}
}
$encrypted.stdout
}
# Age backend decryption
def kms-decrypt-age [
encrypted_data: string
key_id?: string
--input-format: string = "base64"
]: nothing -> string {
# Get Age key file
let key_file = if ($key_id | is-not-empty) {
$key_id
} else {
get-sops-age-key-file
}
if ($key_file | is-empty) {
error make {
msg: "No Age key file configured"
}
}
if not ($key_file | path exists) {
error make {
msg: $"Age key file not found: ($key_file)"
}
}
# Decrypt with age
let decrypted = ($encrypted_data | ^age -d -i $key_file | complete)
if $decrypted.exit_code != 0 {
error make {
msg: $"Age decryption failed: ($decrypted.stderr)"
}
}
$decrypted.stdout
}
# AWS KMS backend encryption
def kms-encrypt-aws [
data: string
key_id?: string
--output-format: string = "base64"
]: nothing -> string {
# Get KMS key ID from config or parameter
let kms_key = if ($key_id | is-not-empty) {
$key_id
} else {
get-config-value "kms.aws.key_id" ""
}
if ($kms_key | is-empty) {
error make {
msg: "No AWS KMS key ID configured"
}
}
# Check if AWS CLI is available
let aws_check = (^which aws | complete)
if $aws_check.exit_code != 0 {
error make {
msg: "AWS CLI not installed"
}
}
# Encrypt with AWS KMS
let encrypted = ($data | ^aws kms encrypt --key-id $kms_key --plaintext fileb:///dev/stdin --output text --query CiphertextBlob | complete)
if $encrypted.exit_code != 0 {
error make {
msg: $"AWS KMS encryption failed: ($encrypted.stderr)"
}
}
$encrypted.stdout | str trim
}
# AWS KMS backend decryption
def kms-decrypt-aws [
encrypted_data: string
key_id?: string
--input-format: string = "base64"
]: nothing -> binary {
# Check if AWS CLI is available
let aws_check = (^which aws | complete)
if $aws_check.exit_code != 0 {
error make {
msg: "AWS CLI not installed"
}
}
# Decrypt with AWS KMS
let decrypted = ($encrypted_data | ^aws kms decrypt --ciphertext-blob fileb:///dev/stdin --output text --query Plaintext | complete)
if $decrypted.exit_code != 0 {
error make {
msg: $"AWS KMS decryption failed: ($decrypted.stderr)"
}
}
$decrypted.stdout | str trim | decode base64
}
# HashiCorp Vault backend encryption
def kms-encrypt-vault [
data: string
key_id?: string
--output-format: string = "base64"
]: nothing -> string {
# Get Vault configuration
let vault_addr = $env.VAULT_ADDR? | default (get-config-value "kms.vault.address" "")
let vault_token = $env.VAULT_TOKEN? | default (get-config-value "kms.vault.token" "")
let transit_key = if ($key_id | is-not-empty) { $key_id } else { get-config-value "kms.vault.transit_key" "provisioning" }
if ($vault_addr | is-empty) {
error make {
msg: "Vault address not configured"
}
}
# Encode data to base64 for Vault transit
let plaintext_b64 = ($data | encode base64)
# Encrypt with Vault transit
let vault_payload = {plaintext: $plaintext_b64} | to json
let encrypted = (^curl -s -X POST -H $"X-Vault-Token: ($vault_token)" -H "Content-Type: application/json" -d $vault_payload $"($vault_addr)/v1/transit/encrypt/($transit_key)" | complete)
if $encrypted.exit_code != 0 {
error make {
msg: $"Vault encryption failed: ($encrypted.stderr)"
}
}
let response = ($encrypted.stdout | from json)
if ($response | get -o errors) != null {
error make {
msg: $"Vault encryption error: ($response.errors | to json)"
}
}
$response.data.ciphertext
}
# HashiCorp Vault backend decryption
def kms-decrypt-vault [
encrypted_data: string
key_id?: string
--input-format: string = "base64"
]: nothing -> binary {
# Get Vault configuration
let vault_addr = $env.VAULT_ADDR? | default (get-config-value "kms.vault.address" "")
let vault_token = $env.VAULT_TOKEN? | default (get-config-value "kms.vault.token" "")
let transit_key = if ($key_id | is-not-empty) { $key_id } else { get-config-value "kms.vault.transit_key" "provisioning" }
if ($vault_addr | is-empty) {
error make {
msg: "Vault address not configured"
}
}
# Decrypt with Vault transit
let vault_payload = {ciphertext: $encrypted_data} | to json
let decrypted = (^curl -s -X POST -H $"X-Vault-Token: ($vault_token)" -H "Content-Type: application/json" -d $vault_payload $"($vault_addr)/v1/transit/decrypt/($transit_key)" | complete)
if $decrypted.exit_code != 0 {
error make {
msg: $"Vault decryption failed: ($decrypted.stderr)"
}
}
let response = ($decrypted.stdout | from json)
if ($response | get -o errors) != null {
error make {
msg: $"Vault decryption error: ($response.errors | to json)"
}
}
$response.data.plaintext | decode base64
}
# Cosmian KMS backend encryption
def kms-encrypt-cosmian [
data: string
key_id?: string
--output-format: string = "base64"
]: nothing -> string {
# Get Cosmian KMS configuration
let kms_server = get-kms-server
if ($kms_server | is-empty) {
error make {
msg: "Cosmian KMS server not configured"
}
}
# Use curl to call Cosmian KMS REST API
let encrypted = ($data | ^curl -s -X POST -H "Content-Type: application/octet-stream" --data-binary @- $"($kms_server)/encrypt" | complete)
if $encrypted.exit_code != 0 {
error make {
msg: $"Cosmian KMS encryption failed: ($encrypted.stderr)"
}
}
$encrypted.stdout | encode base64
}
# Cosmian KMS backend decryption
def kms-decrypt-cosmian [
encrypted_data: string
key_id?: string
--input-format: string = "base64"
]: nothing -> string {
# Get Cosmian KMS configuration
let kms_server = get-kms-server
if ($kms_server | is-empty) {
error make {
msg: "Cosmian KMS server not configured"
}
}
# Decode from base64 first
let binary_data = ($encrypted_data | decode base64)
# Use curl to call Cosmian KMS REST API
let decrypted = ($binary_data | ^curl -s -X POST -H "Content-Type: application/octet-stream" --data-binary @- $"($kms_server)/decrypt" | complete)
if $decrypted.exit_code != 0 {
error make {
msg: $"Cosmian KMS decryption failed: ($decrypted.stderr)"
}
}
$decrypted.stdout
}
# Detect KMS backend from configuration
def detect-kms-backend []: nothing -> string {
let kms_enabled = (get-kms-enabled)
if not $kms_enabled {
# Default to Age if KMS not enabled
return "age"
}
let kms_mode = (get-kms-mode)
match $kms_mode {
"local" => {
let local_provider = (get-kms-local-provider)
$local_provider
}
"remote" => {
# Remote KMS (Cosmian or Vault)
let kms_server = (get-kms-server)
if ($kms_server | str contains "vault") {
"vault"
} else {
"cosmian"
}
}
_ => "age"
}
}
# Test KMS connectivity and functionality
export def kms-test [
--backend: string = "" # age, aws-kms, vault, cosmian (auto-detect if empty)
]: nothing -> record {
print $"🧪 Testing KMS backend..."
let kms_backend = if ($backend | is-empty) {
detect-kms-backend
} else {
$backend
}
print $" Backend: ($kms_backend)"
let test_data = "Hello, KMS encryption test!"
let initial_result = {
backend: $kms_backend
encryption_success: false
decryption_success: false
round_trip_success: false
error: null
}
let test_result = (do {
# Test encryption
print $" Testing encryption..."
let encrypted = (kms-encrypt $test_data --backend=$kms_backend)
let encryption_result = ($initial_result | upsert encryption_success true)
# Test decryption
print $" Testing decryption..."
let decrypted = (kms-decrypt $encrypted --backend=$kms_backend)
let decryption_result = ($encryption_result | upsert decryption_success true)
# Verify round-trip
if $decrypted == $test_data {
print $" ✅ Round-trip successful"
$decryption_result | upsert round_trip_success true
} else {
print $" ❌ Round-trip failed: data mismatch"
$decryption_result | upsert error "Round-trip data mismatch"
}
} | complete | if $in.exit_code == 0 {
$in.stdout
} else {
print $" ❌ Test failed"
$initial_result | upsert error "Test execution failed"
})
print ""
if $test_result.round_trip_success {
print $"✅ KMS backend ($kms_backend) is working correctly"
} else {
print $"❌ KMS backend ($kms_backend) test failed"
}
$test_result
}
# List available KMS backends
export def kms-list-backends [] {
print "Available KMS Backends:"
print "======================="
print ""
print "Local Backends:"
print " • age - Age encryption (file-based keys)"
print ""
print "Cloud Backends:"
print " • aws-kms - AWS Key Management Service"
print ""
print "Enterprise Backends:"
print " • vault - HashiCorp Vault Transit Engine"
print " • cosmian - Cosmian KMS (confidential computing)"
print ""
print "Current Configuration:"
let kms_enabled = (get-kms-enabled)
print $" KMS Enabled: ($kms_enabled)"
if $kms_enabled {
let kms_mode = (get-kms-mode)
print $" Mode: ($kms_mode)"
let detected_backend = (detect-kms-backend)
print $" Detected Backend: ($detected_backend)"
} else {
print " Detected Backend: age (default)"
}
}
# Get KMS backend status
export def kms-status []: nothing -> record {
let kms_enabled = (get-kms-enabled)
let backend = (detect-kms-backend)
mut status = {
enabled: $kms_enabled
backend: $backend
configured: false
available: false
}
# Check if backend is configured
match $backend {
"age" => {
let key_file = (get-sops-age-key-file)
let recipients = ($env.SOPS_AGE_RECIPIENTS? | default "")
$status = ($status | upsert configured (($key_file | is-not-empty) and ($recipients | is-not-empty)))
let available = if ($key_file | is-not-empty) { $key_file | path exists } else { false }
$status = ($status | upsert available $available)
}
"aws-kms" => {
let aws_check = (^which aws | complete)
$status = ($status | upsert available ($aws_check.exit_code == 0))
let key_id = (get-config-value "kms.aws.key_id" "")
$status = ($status | upsert configured ($key_id | is-not-empty))
}
"vault" => {
let vault_addr = ($env.VAULT_ADDR? | default "")
let vault_token = ($env.VAULT_TOKEN? | default "")
$status = ($status | upsert configured (($vault_addr | is-not-empty) and ($vault_token | is-not-empty)))
# Test connectivity
if $status.configured {
let health_check = (^curl -s $"($vault_addr)/v1/sys/health" | complete)
$status = ($status | upsert available ($health_check.exit_code == 0))
}
}
"cosmian" => {
let kms_server = (get-kms-server)
$status = ($status | upsert configured ($kms_server | is-not-empty))
# Test connectivity
if $status.configured {
let health_check = (^curl -s $"($kms_server)/health" | complete)
$status = ($status | upsert available ($health_check.exit_code == 0))
}
}
}
$status
}
# Get configuration value with fallback
def get-config-value [
path: string
default_value: any
]: nothing -> any {
# This would integrate with the config accessor
# For now, return default
$default_value
}
# Main help
export def main [] {
print "KMS Client Interface"
print "===================="
print ""
print "Commands:"
print " kms-encrypt <data> Encrypt data using KMS"
print " kms-decrypt <data> Decrypt data using KMS"
print " kms-test Test KMS backend functionality"
print " kms-list-backends List available KMS backends"
print " kms-status Show KMS backend status"
print ""
print "Supported Backends:"
print " age, aws-kms, vault, cosmian"
}

View File

@ -1 +1,2 @@
export use lib.nu *
export use lib.nu *
export use client.nu *

378
nulib/mfa/commands.nu Normal file
View File

@ -0,0 +1,378 @@
# Multi-Factor Authentication (MFA) CLI commands
#
# Provides comprehensive MFA management through the control-center API
use ../lib_provisioning/config/loader.nu get-config
# Get API base URL from config
def get-api-url [] {
let config = get-config
$config.api.base_url? | default "http://localhost:8080"
}
# Get auth token from environment or config
def get-auth-token [] {
$env.PROVISIONING_AUTH_TOKEN? | default ""
}
# Make authenticated API request
def api-request [
method: string # HTTP method (GET, POST, DELETE)
endpoint: string # API endpoint path
body?: any # Request body (optional)
] {
let base_url = get-api-url
let token = get-auth-token
let url = $"($base_url)/api/v1($endpoint)"
let headers = {
"Authorization": $"Bearer ($token)"
"Content-Type": "application/json"
}
if ($body | is-empty) {
http $method $url --headers $headers
} else {
http $method $url --headers $headers ($body | to json)
}
}
# ============================================================================
# TOTP Commands
# ============================================================================
# Enroll TOTP (Time-based One-Time Password)
#
# Example:
# mfa totp enroll
export def "mfa totp enroll" [] {
print "📱 Enrolling TOTP device..."
let response = api-request "POST" "/mfa/totp/enroll"
print ""
print "✅ TOTP device enrolled successfully!"
print ""
print "📋 Device ID:" $response.device_id
print ""
print "🔑 Manual entry secret (if QR code doesn't work):"
print $" ($response.secret)"
print ""
print "📱 Scan this QR code with your authenticator app:"
print " (Google Authenticator, Authy, Microsoft Authenticator, etc.)"
print ""
# Save QR code to file
let qr_file = $"/tmp/mfa-qr-($response.device_id).html"
$"<!DOCTYPE html>
<html>
<head><title>MFA Setup - QR Code</title></head>
<body style='text-align: center; padding: 50px;'>
<h1>Scan QR Code</h1>
<img src='($response.qr_code)' style='max-width: 400px;' />
<p><code>($response.secret)</code></p>
</body>
</html>" | save -f $qr_file
print $" QR code saved to: ($qr_file)"
print $" Open in browser: open ($qr_file)"
print ""
print "💾 Backup codes (save these securely):"
for code in $response.backup_codes {
print $" ($code)"
}
print ""
print "⚠️ IMPORTANT: Test your TOTP setup with 'mfa totp verify <code>'"
print ""
}
# Verify TOTP code
#
# Example:
# mfa totp verify 123456
export def "mfa totp verify" [
code: string # 6-digit TOTP code
--device-id: string # Specific device ID (optional)
] {
print $"🔐 Verifying TOTP code: ($code)..."
let body = {
code: $code
device_id: $device_id
}
let response = api-request "POST" "/mfa/totp/verify" $body
if $response.verified {
print ""
print "✅ TOTP verification successful!"
if $response.backup_code_used {
print "⚠️ Note: A backup code was used"
}
print ""
} else {
print ""
print "❌ TOTP verification failed"
print " Please check your code and try again"
print ""
exit 1
}
}
# Disable TOTP
#
# Example:
# mfa totp disable
export def "mfa totp disable" [] {
print "⚠️ Disabling TOTP..."
print ""
print "This will remove all TOTP devices from your account."
let confirm = input "Are you sure? (yes/no): "
if $confirm != "yes" {
print "Cancelled."
return
}
api-request "POST" "/mfa/totp/disable"
print ""
print "✅ TOTP disabled successfully"
print ""
}
# Show backup codes status
#
# Example:
# mfa totp backup-codes
export def "mfa totp backup-codes" [] {
print "🔑 Fetching backup codes status..."
let response = api-request "GET" "/mfa/totp/backup-codes"
print ""
print "📋 Backup Codes:"
for code in $response.backup_codes {
print $" ($code)"
}
print ""
}
# Regenerate backup codes
#
# Example:
# mfa totp regenerate
export def "mfa totp regenerate" [] {
print "🔄 Regenerating backup codes..."
print ""
print "⚠️ This will invalidate all existing backup codes."
let confirm = input "Continue? (yes/no): "
if $confirm != "yes" {
print "Cancelled."
return
}
let response = api-request "POST" "/mfa/totp/regenerate"
print ""
print "✅ New backup codes generated:"
print ""
for code in $response.backup_codes {
print $" ($code)"
}
print ""
print "💾 Save these codes securely!"
print ""
}
# ============================================================================
# WebAuthn Commands
# ============================================================================
# Enroll WebAuthn device (security key)
#
# Example:
# mfa webauthn enroll --device-name "YubiKey 5"
export def "mfa webauthn enroll" [
--device-name: string = "Security Key" # Device name
] {
print $"🔐 Enrolling WebAuthn device: ($device_name)"
print ""
print "⚠️ WebAuthn enrollment requires browser interaction."
print " Use the Web UI at: (get-api-url)/mfa/setup"
print ""
print " Or use the API directly with a browser-based client."
print ""
}
# List WebAuthn devices
#
# Example:
# mfa webauthn list
export def "mfa webauthn list" [] {
print "🔑 Fetching WebAuthn devices..."
let devices = api-request "GET" "/mfa/webauthn/devices"
if ($devices | is-empty) {
print ""
print "No WebAuthn devices registered"
print ""
return
}
print ""
print "📱 WebAuthn Devices:"
print ""
for device in $devices {
print $"Device: ($device.device_name)"
print $" ID: ($device.id)"
print $" Created: ($device.created_at)"
print $" Last used: ($device.last_used | default 'Never')"
print $" Status: (if $device.enabled { '✅ Enabled' } else { '❌ Disabled' })"
print $" Transports: ($device.transports | str join ', ')"
print ""
}
}
# Remove WebAuthn device
#
# Example:
# mfa webauthn remove <device-id>
export def "mfa webauthn remove" [
device_id: string # Device ID to remove
] {
print $"🗑️ Removing WebAuthn device: ($device_id)"
print ""
let confirm = input "Are you sure? (yes/no): "
if $confirm != "yes" {
print "Cancelled."
return
}
api-request "DELETE" $"/mfa/webauthn/devices/($device_id)"
print ""
print "✅ Device removed successfully"
print ""
}
# ============================================================================
# General MFA Commands
# ============================================================================
# Show MFA status
#
# Example:
# mfa status
export def "mfa status" [] {
print "🔐 Fetching MFA status..."
let status = api-request "GET" "/mfa/status"
print ""
print "📊 MFA Status:"
print $" Enabled: (if $status.enabled { '✅ Yes' } else { '❌ No' })"
print ""
if not ($status.totp_devices | is-empty) {
print "📱 TOTP Devices:"
for device in $status.totp_devices {
print $" • ID: ($device.id)"
print $" Created: ($device.created_at)"
print $" Last used: ($device.last_used | default 'Never')"
print $" Status: (if $device.enabled { 'Enabled' } else { 'Not verified' })"
}
print ""
}
if not ($status.webauthn_devices | is-empty) {
print "🔑 WebAuthn Devices:"
for device in $status.webauthn_devices {
print $" • ($device.device_name)"
print $" ID: ($device.id)"
print $" Created: ($device.created_at)"
print $" Last used: ($device.last_used | default 'Never')"
}
print ""
}
if $status.has_backup_codes {
print "💾 Backup codes: Available"
print ""
}
if (not $status.enabled) {
print " MFA is not enabled. Set it up with:"
print " • mfa totp enroll - For TOTP (recommended)"
print " • mfa webauthn enroll - For hardware keys"
print ""
}
}
# Disable all MFA methods
#
# Example:
# mfa disable
export def "mfa disable" [] {
print "⚠️ Disabling ALL MFA methods..."
print ""
print "This will remove:"
print " • All TOTP devices"
print " • All WebAuthn devices"
print " • All backup codes"
print ""
let confirm = input "Are you ABSOLUTELY sure? Type 'disable mfa': "
if $confirm != "disable mfa" {
print "Cancelled."
return
}
api-request "POST" "/mfa/disable"
print ""
print "✅ All MFA methods have been disabled"
print ""
}
# List all MFA devices
#
# Example:
# mfa list-devices
export def "mfa list-devices" [] {
mfa status
}
# ============================================================================
# Help Command
# ============================================================================
# Show MFA help
export def "mfa help" [] {
print ""
print "🔐 Multi-Factor Authentication (MFA) Commands"
print ""
print "TOTP (Time-based One-Time Password):"
print " mfa totp enroll - Enroll TOTP device"
print " mfa totp verify <code> - Verify TOTP code"
print " mfa totp disable - Disable TOTP"
print " mfa totp backup-codes - Show backup codes status"
print " mfa totp regenerate - Regenerate backup codes"
print ""
print "WebAuthn (Hardware Security Keys):"
print " mfa webauthn enroll - Enroll security key"
print " mfa webauthn list - List registered devices"
print " mfa webauthn remove <id> - Remove device"
print ""
print "General:"
print " mfa status - Show MFA status"
print " mfa list-devices - List all devices"
print " mfa disable - Disable all MFA"
print " mfa help - Show this help"
print ""
}

5
nulib/ssh/mod.nu Normal file
View File

@ -0,0 +1,5 @@
# SSH Module
#
# Temporal SSH key management system
export use temporal.nu *

248
nulib/ssh/temporal.nu Normal file
View File

@ -0,0 +1,248 @@
# SSH Temporal Key Management CLI
#
# Provides Nushell commands for managing temporal SSH keys with automatic cleanup.
# Generate temporal SSH key
export def "ssh generate-key" [
server: string # Target server (hostname or IP)
--user: string = "root" # SSH user
--ttl: duration = 1hr # Key lifetime
--type: string = "dynamic" # Key type: ca, otp, or dynamic
--ip: string # Allowed IP (for OTP mode)
--principal: string # Principal (for CA mode)
] {
let ttl_seconds = ($ttl | into int) / 1_000_000_000
let request = {
key_type: (match $type {
"ca" => "certificate"
"otp" => "otp"
"dynamic" => "dynamickeypair"
_ => (error make {msg: $"Invalid key type: ($type). Use: ca, otp, or dynamic"})
})
user: $user
target_server: $server
ttl_seconds: $ttl_seconds
allowed_ip: $ip
principal: $principal
}
let response = (http post http://localhost:8080/api/v1/ssh/generate $request)
if $response.success {
print $"✓ SSH key generated successfully"
print $" Key ID: ($response.data.id)"
print $" Type: ($response.data.key_type)"
print $" User: ($response.data.user)"
print $" Server: ($response.data.target_server)"
print $" Expires: ($response.data.expires_at)"
print $" Fingerprint: ($response.data.fingerprint)"
if $response.data.private_key != null {
print ""
print "Private Key (save this securely, will not be shown again):"
print $"($response.data.private_key)"
}
$response.data
} else {
error make {msg: $"Failed to generate SSH key: ($response.error)"}
}
}
# Deploy SSH key to server
export def "ssh deploy-key" [
key_id: string # SSH key ID
] {
let response = (http post $"http://localhost:8080/api/v1/ssh/($key_id)/deploy" {})
if $response.success {
if $response.data.success {
print $"✓ SSH key deployed successfully to ($response.data.server)"
} else {
error make {msg: $"Failed to deploy key: ($response.data.error)"}
}
$response.data
} else {
error make {msg: $"Failed to deploy SSH key: ($response.error)"}
}
}
# List active SSH keys
export def "ssh list-keys" [
--expired: bool = false # Include expired keys
] {
let response = (http get http://localhost:8080/api/v1/ssh/keys)
if $response.success {
let keys = $response.data
if ($keys | is-empty) {
print "No active SSH keys found"
return []
}
print $"Found ($keys | length) active SSH keys:"
$keys | select id key_type user target_server expires_at deployed
} else {
error make {msg: $"Failed to list SSH keys: ($response.error)"}
}
}
# Revoke SSH key
export def "ssh revoke-key" [
key_id: string # SSH key ID to revoke
] {
let response = (http post $"http://localhost:8080/api/v1/ssh/($key_id)/revoke" {})
if $response.success {
print $"✓ SSH key revoked: ($key_id)"
$response.data
} else {
error make {msg: $"Failed to revoke SSH key: ($response.error)"}
}
}
# Get SSH key details
export def "ssh get-key" [
key_id: string # SSH key ID
] {
let response = (http get $"http://localhost:8080/api/v1/ssh/($key_id)")
if $response.success {
$response.data
} else {
error make {msg: $"Failed to get SSH key: ($response.error)"}
}
}
# SSH with temporal key (auto-generate, deploy, connect, revoke)
export def "ssh connect" [
server: string # Target server
--user: string = "root" # SSH user
--ttl: duration = 1hr # Key lifetime
--type: string = "dynamic" # Key type: ca, otp, or dynamic
--keep: bool = false # Keep key after disconnect
] {
print $"Generating temporal SSH key for ($user)@($server)..."
let key = (ssh generate-key $server --user $user --ttl $ttl --type $type)
print "Deploying key to server..."
ssh deploy-key $key.id
# Save private key to temp file
let key_file = $"/tmp/ssh_temp_($key.id)"
$key.private_key | save -f $key_file
chmod 600 $key_file
print $"Connecting to ($user)@($server)..."
^ssh -i $key_file -o "StrictHostKeyChecking=no" $"($user)@($server)"
# Cleanup
rm $key_file
if not $keep {
print "Revoking temporary key..."
ssh revoke-key $key.id
} else {
print $"Key ($key.id) kept active until expiration"
}
}
# Show SSH key statistics
export def "ssh stats" [] {
let response = (http get http://localhost:8080/api/v1/ssh/stats)
if $response.success {
let stats = $response.data
print "SSH Key Statistics:"
print $" Total generated: ($stats.total_generated)"
print $" Active keys: ($stats.active_keys)"
print $" Expired keys: ($stats.expired_keys)"
print ""
print "Keys by type:"
$stats.keys_by_type | transpose key value | each {|row| print $" ($row.key): ($row.value)"}
if $stats.last_cleanup_at != null {
print ""
print $"Last cleanup: ($stats.last_cleanup_at)"
print $" Cleaned keys: ($stats.last_cleanup_count)"
}
$stats
} else {
error make {msg: $"Failed to get SSH statistics: ($response.error)"}
}
}
# Trigger manual cleanup of expired keys
export def "ssh cleanup" [] {
print "Triggering cleanup of expired SSH keys..."
let response = (http post http://localhost:8080/api/v1/ssh/cleanup {})
if $response.success {
print $"✓ Cleaned up ($response.data.cleaned_count) expired keys"
if ($response.data.cleaned_key_ids | length) > 0 {
print "Cleaned key IDs:"
$response.data.cleaned_key_ids | each {|id| print $" - ($id)"}
}
$response.data
} else {
error make {msg: $"Failed to cleanup expired keys: ($response.error)"}
}
}
# Quick test - generate, deploy, and revoke a test key
export def "ssh test" [
server: string # Target server
--user: string = "root" # SSH user
] {
print "=== SSH Temporal Key Test ==="
print ""
print "1. Generating test key..."
let key = (ssh generate-key $server --user $user --ttl 5min)
print ""
print "2. Deploying key to server..."
ssh deploy-key $key.id
print ""
print "3. Testing SSH connection..."
print "(Manual step: Try connecting with the private key shown above)"
print ""
print "4. Revoking key..."
ssh revoke-key $key.id
print ""
print "✓ Test complete"
}
# Show help for SSH commands
export def "ssh help" [] {
print "SSH Temporal Key Management Commands:"
print ""
print " ssh generate-key <server> Generate new SSH key"
print " ssh deploy-key <key_id> Deploy key to server"
print " ssh list-keys List active keys"
print " ssh get-key <key_id> Get key details"
print " ssh revoke-key <key_id> Revoke key immediately"
print " ssh connect <server> Connect with auto-generated key"
print " ssh stats Show statistics"
print " ssh cleanup Cleanup expired keys"
print " ssh test <server> Run quick test"
print ""
print "Options:"
print " --user <name> SSH user (default: root)"
print " --ttl <duration> Key lifetime (default: 1hr)"
print " --type <ca|otp|dynamic> Key type (default: dynamic)"
print " --ip <address> Allowed IP (OTP mode)"
print " --principal <name> Principal (CA mode)"
print ""
print "Examples:"
print " ssh generate-key server.example.com --ttl 30min"
print " ssh connect server.example.com --user deploy"
print " ssh list-keys | where deployed == true"
print " ssh stats"
}

0
plugins/.gitkeep Normal file
View File