#!/usr/bin/env bash # Post-rotation handler for tcpdump -z # Compresses rotated PCAP with zstd, optionally encrypts with AES-256-GCM # # Called by tcpdump like: tcpdump -z /opt/.cache/bb/scripts/rotate_pcap.sh # tcpdump passes the completed PCAP filename as $1 # # Environment variables: # ZSTD_LEVEL - Compression level (default: 19, jumpbox: 3) # BB_ENCRYPT - Set to "1" to encrypt after compression # BB_KEY_FILE - Path to encryption key file (required if BB_ENCRYPT=1) # BB_VENV - Path to Python venv (for encryption via crypto module) # BB_MAX_PCAP_PCT - Max disk usage percent before purge (default: 85) set -euo pipefail PCAP="$1" ZSTD_LEVEL="${ZSTD_LEVEL:-19}" BB_ENCRYPT="${BB_ENCRYPT:-0}" BB_KEY_FILE="${BB_KEY_FILE:-}" BB_VENV="${BB_VENV:-/opt/.cache/bb/.venv}" BB_MAX_PCAP_PCT="${BB_MAX_PCAP_PCT:-85}" PCAP_DIR="$(dirname "$PCAP")" # Logging (silent by default, tcpdump -z doesn't have a tty) log() { logger -t "bb-pcap-rotate" "$*" 2>/dev/null || true } # Verify input file exists and is non-empty if [[ ! -f "$PCAP" ]]; then log "ERROR: PCAP file not found: $PCAP" exit 0 # Exit 0 so tcpdump doesn't complain fi if [[ ! -s "$PCAP" ]]; then log "WARNING: Empty PCAP file, removing: $PCAP" rm -f "$PCAP" exit 0 fi PCAP_SIZE=$(stat -c '%s' "$PCAP" 2>/dev/null || echo 0) log "Rotating PCAP: $PCAP ($PCAP_SIZE bytes, zstd -${ZSTD_LEVEL})" # ── Disk space check ─────────────────────────────────────────────────────── check_disk_space() { local usage usage=$(df --output=pcent "$PCAP_DIR" 2>/dev/null | tail -1 | tr -d '% ') if [[ -n "$usage" ]] && [[ "$usage" -ge "$BB_MAX_PCAP_PCT" ]]; then log "WARNING: Disk usage ${usage}% >= ${BB_MAX_PCAP_PCT}%, purging oldest PCAPs" # Delete oldest compressed PCAPs first, then oldest raw local oldest oldest=$(find "$PCAP_DIR" -name '*.pcap.zst*' -type f -printf '%T+ %p\n' 2>/dev/null \ | sort | head -5 | awk '{print $2}') for f in $oldest; do log "Purging: $f" rm -f "$f" done fi } check_disk_space # ── Compress with zstd ───────────────────────────────────────────────────── COMPRESSED="${PCAP}.zst" if command -v zstd &>/dev/null; then zstd "-${ZSTD_LEVEL}" --rm -q "$PCAP" -o "$COMPRESSED" 2>/dev/null if [[ -f "$COMPRESSED" ]]; then COMP_SIZE=$(stat -c '%s' "$COMPRESSED" 2>/dev/null || echo 0) log "Compressed: ${PCAP_SIZE} -> ${COMP_SIZE} bytes (zstd -${ZSTD_LEVEL})" else log "ERROR: zstd compression failed for $PCAP" exit 0 fi else log "ERROR: zstd not found, leaving PCAP uncompressed" exit 0 fi # ── Encrypt (optional) ──────────────────────────────────────────────────── if [[ "$BB_ENCRYPT" == "1" ]] && [[ -n "$BB_KEY_FILE" ]] && [[ -f "$BB_KEY_FILE" ]]; then ENCRYPTED="${COMPRESSED}.enc" if [[ -f "${BB_VENV}/bin/python3" ]]; then "${BB_VENV}/bin/python3" -c " import sys sys.path.insert(0, '$(dirname "$(dirname "$0")")') from utils.crypto import encrypt_file encrypt_file('${COMPRESSED}', '${ENCRYPTED}', open('${BB_KEY_FILE}', 'rb').read().strip()) " 2>/dev/null if [[ -f "$ENCRYPTED" ]]; then rm -f "$COMPRESSED" ENC_SIZE=$(stat -c '%s' "$ENCRYPTED" 2>/dev/null || echo 0) log "Encrypted: ${COMPRESSED} -> ${ENCRYPTED} (${ENC_SIZE} bytes)" else log "WARNING: Encryption failed, keeping compressed file" fi else log "WARNING: Python venv not found at ${BB_VENV}, skipping encryption" fi fi # ── Restrictive permissions ──────────────────────────────────────────────── chmod 600 "${PCAP_DIR}"/*.zst* 2>/dev/null || true log "PCAP rotation complete"