Session Recording and Audit

Session Recording and Audit

Comprehensive session recording provides forensic capabilities and helps meet compliance requirements. Modern bastion hosts must capture not just commands but full session interactions for security analysis.

Implement advanced session recording:

#!/bin/bash
# bastion-session-recorder.sh
# Advanced session recording system

# Session recording configuration
RECORDING_BASE="/var/log/bastion-recordings"
RETENTION_DAYS=90
COMPRESSION_DAYS=7

# Initialize recording system
init_recording_system() {
    # Create directory structure
    mkdir -p "$RECORDING_BASE"/{active,completed,compressed,indexed}
    
    # Install required tools
    apt-get install -y asciinema ttyrec screen jq
    
    # Create recording wrapper script
    cat > /usr/local/bin/record-session << 'EOF'
#!/bin/bash
# Session recording wrapper

SESSION_ID="${USER}-$(date +%s)-$$"
RECORDING_DIR="$RECORDING_BASE/active/$SESSION_ID"
mkdir -p "$RECORDING_DIR"

# Record session metadata
cat > "$RECORDING_DIR/metadata.json" << METADATA
{
    "session_id": "$SESSION_ID",
    "user": "$USER",
    "real_user": "${SUDO_USER:-$USER}",
    "client_ip": "${SSH_CLIENT%% *}",
    "start_time": "$(date -Iseconds)",
    "command": "${SSH_ORIGINAL_COMMAND:-interactive}",
    "tty": "$(tty)",
    "environment": $(env | jq -R . | jq -s .)
}
METADATA

# Start recording based on session type
if [ -n "$SSH_ORIGINAL_COMMAND" ]; then
    # Non-interactive command
    script -q -f -c "$SSH_ORIGINAL_COMMAND" \
        "$RECORDING_DIR/typescript" \
        -t "$RECORDING_DIR/timing" 2>/dev/null
    EXIT_CODE=$?
else
    # Interactive session - use asciinema for better playback
    asciinema rec \
        --quiet \
        --append \
        --title "Bastion Session $SESSION_ID" \
        --command "${SHELL:-/bin/bash}" \
        "$RECORDING_DIR/session.cast"
    EXIT_CODE=$?
fi

# Finalize recording
echo "$EXIT_CODE" > "$RECORDING_DIR/exit_code"
echo "$(date -Iseconds)" > "$RECORDING_DIR/end_time"

# Move to completed
mv "$RECORDING_DIR" "$RECORDING_BASE/completed/"

# Trigger post-processing
/usr/local/bin/process-recording "$SESSION_ID" &

exit $EXIT_CODE
EOF
    
    chmod +x /usr/local/bin/record-session
}

# Process completed recordings
create_processing_script() {
    cat > /usr/local/bin/process-recording << 'EOF'
#!/bin/bash
# Post-process session recordings

SESSION_ID=$1
RECORDING_DIR="$RECORDING_BASE/completed/$SESSION_ID"

# Extract text content for indexing
if [ -f "$RECORDING_DIR/typescript" ]; then
    # Clean typescript file
    cat "$RECORDING_DIR/typescript" | \
        sed 's/\x1b\[[0-9;]*m//g' | \
        sed 's/\r$//' > "$RECORDING_DIR/session_text.txt"
elif [ -f "$RECORDING_DIR/session.cast" ]; then
    # Extract from asciinema
    asciinema cat "$RECORDING_DIR/session.cast" | \
        sed 's/\x1b\[[0-9;]*m//g' > "$RECORDING_DIR/session_text.txt"
fi

# Extract commands for analysis
grep -E '^\$ |^# |^> ' "$RECORDING_DIR/session_text.txt" > \
    "$RECORDING_DIR/commands.txt" || true

# Scan for sensitive data
/usr/local/bin/scan-sensitive-data "$RECORDING_DIR/session_text.txt" > \
    "$RECORDING_DIR/sensitive_data_scan.json"

# Generate searchable index
jq -n \
    --arg id "$SESSION_ID" \
    --arg user "$(jq -r .user "$RECORDING_DIR/metadata.json")" \
    --arg start "$(jq -r .start_time "$RECORDING_DIR/metadata.json")" \
    --arg commands "$(wc -l < "$RECORDING_DIR/commands.txt")" \
    --arg duration "$(($(date -d "$(cat "$RECORDING_DIR/end_time")" +%s) - \
                      $(date -d "$(jq -r .start_time "$RECORDING_DIR/metadata.json")" +%s)))" \
    '{
        session_id: $id,
        user: $user,
        start_time: $start,
        duration: $duration,
        command_count: $commands,
        indexed_at: now | todate
    }' > "$RECORDING_BASE/indexed/${SESSION_ID}.json"

# Compress if older than threshold
if [ $COMPRESSION_DAYS -gt 0 ]; then
    find "$RECORDING_BASE/completed" -name "$SESSION_ID" -type d -mtime +$COMPRESSION_DAYS -exec \
        tar -czf "$RECORDING_BASE/compressed/${SESSION_ID}.tar.gz" {} \; -exec \
        rm -rf {} \;
fi
EOF
    
    chmod +x /usr/local/bin/process-recording
}

# Sensitive data scanner
create_sensitive_scanner() {
    cat > /usr/local/bin/scan-sensitive-data << 'EOF'
#!/usr/bin/env python3
import re
import sys
import json

patterns = {
    'aws_access_key': r'AKIA[0-9A-Z]{16}',
    'aws_secret_key': r'[0-9a-zA-Z/+=]{40}',
    'private_key': r'-----BEGIN (RSA |EC |)PRIVATE KEY-----',
    'api_token': r'[a-zA-Z0-9]{32,}',
    'password': r'(password|passwd|pwd)\s*[:=]\s*\S+',
    'credit_card': r'\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b',
    'ssn': r'\b\d{3}-\d{2}-\d{4}\b',
    'email': r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b'
}

def scan_file(filepath):
    findings = []
    
    with open(filepath, 'r', errors='ignore') as f:
        content = f.read()
        
        for pattern_name, pattern in patterns.items():
            matches = re.finditer(pattern, content, re.IGNORECASE)
            for match in matches:
                # Get line number
                line_num = content[:match.start()].count('\n') + 1
                
                findings.append({
                    'type': pattern_name,
                    'line': line_num,
                    'position': match.start(),
                    'matched': match.group()[:20] + '...' if len(match.group()) > 20 else match.group()
                })
    
    return findings

if __name__ == '__main__':
    if len(sys.argv) != 2:
        print("Usage: scan-sensitive-data <file>")
        sys.exit(1)
        
    findings = scan_file(sys.argv[1])
    print(json.dumps({
        'scan_complete': True,
        'findings_count': len(findings),
        'findings': findings
    }, indent=2))
EOF
    
    chmod +x /usr/local/bin/scan-sensitive-data
}

# Session playback tool
create_playback_tool() {
    cat > /usr/local/bin/bastion-playback << 'EOF'
#!/bin/bash
# Play back recorded sessions

usage() {
    echo "Usage: bastion-playback [options] SESSION_ID"
    echo "Options:"
    echo "  -s, --speed SPEED    Playback speed multiplier (default: 1.0)"
    echo "  -f, --from TIME      Start playback from TIME seconds"
    echo "  -t, --to TIME        Stop playback at TIME seconds"
    echo "  -i, --info           Show session information only"
    echo "  -h, --help           Show this help"
}

# Default values
SPEED=1.0
FROM=0
TO=""
INFO_ONLY=false

# Parse arguments
while [[ $# -gt 0 ]]; do
    case $1 in
        -s|--speed)
            SPEED="$2"
            shift 2
            ;;
        -f|--from)
            FROM="$2"
            shift 2
            ;;
        -t|--to)
            TO="$2"
            shift 2
            ;;
        -i|--info)
            INFO_ONLY=true
            shift
            ;;
        -h|--help)
            usage
            exit 0
            ;;
        *)
            SESSION_ID="$1"
            shift
            ;;
    esac
done

if [ -z "$SESSION_ID" ]; then
    usage
    exit 1
fi

# Find recording
RECORDING_DIR=""
for dir in "$RECORDING_BASE"/{completed,compressed}; do
    if [ -d "$dir/$SESSION_ID" ]; then
        RECORDING_DIR="$dir/$SESSION_ID"
        break
    elif [ -f "$dir/${SESSION_ID}.tar.gz" ]; then
        # Extract compressed recording
        TEMP_DIR=$(mktemp -d)
        tar -xzf "$dir/${SESSION_ID}.tar.gz" -C "$TEMP_DIR"
        RECORDING_DIR="$TEMP_DIR/$SESSION_ID"
        CLEANUP_TEMP=true
        break
    fi
done

if [ -z "$RECORDING_DIR" ] || [ ! -d "$RECORDING_DIR" ]; then
    echo "Recording not found: $SESSION_ID"
    exit 1
fi

# Show session info
if [ "$INFO_ONLY" = true ] || [ ! -t 1 ]; then
    jq . "$RECORDING_DIR/metadata.json"
    exit 0
fi

# Playback session
echo "Playing back session: $SESSION_ID"
echo "Press Ctrl+C to stop"
echo ""

if [ -f "$RECORDING_DIR/session.cast" ]; then
    # Asciinema recording
    asciinema play "$RECORDING_DIR/session.cast" --speed "$SPEED"
elif [ -f "$RECORDING_DIR/typescript" ] && [ -f "$RECORDING_DIR/timing" ]; then
    # Script recording
    scriptreplay --timing "$RECORDING_DIR/timing" \
                 --typescript "$RECORDING_DIR/typescript" \
                 --divisor "$SPEED"
else
    echo "No playable recording found"
    exit 1
fi

# Cleanup
if [ "$CLEANUP_TEMP" = true ]; then
    rm -rf "$TEMP_DIR"
fi
EOF
    
    chmod +x /usr/local/bin/bastion-playback
}

# Initialize all components
init_recording_system
create_processing_script
create_sensitive_scanner
create_playback_tool

# Create cleanup cron job
cat > /etc/cron.daily/bastion-recording-cleanup << EOF
#!/bin/bash
# Clean up old recordings

# Remove recordings older than retention period
find $RECORDING_BASE/compressed -name "*.tar.gz" -mtime +$RETENTION_DAYS -delete

# Remove empty directories
find $RECORDING_BASE -type d -empty -delete

# Generate usage report
du -sh $RECORDING_BASE/* > $RECORDING_BASE/storage_usage.txt
EOF

chmod +x /etc/cron.daily/bastion-recording-cleanup

echo "Session recording system initialized"