#!/bin/zsh # # Daily Config Audit # Checks for unauthorized changes to critical OpenClaw configuration files # Runs once daily via cron (recommended: 6 AM) # STATE_DIR="/Users/mattbruce/.openclaw/workspace/scripts/security-monitors/state" LOG_FILE="/Users/mattbruce/.openclaw/workspace/scripts/security-monitors/logs/config-audit.log" BASELINE_DIR="$STATE_DIR/baselines" AUDIT_REPORT="$STATE_DIR/daily-audit-report.txt" # Create directories mkdir -p "$(dirname $LOG_FILE)" "$STATE_DIR" "$BASELINE_DIR" # Timestamp helper timestamp() { date '+%Y-%m-%d %H:%M:%S %Z' } # Log to file log() { echo "[$(timestamp)] $1" >> "$LOG_FILE" } # Send alert to queue send_alert() { local level="$1" local message="$2" echo "$(timestamp) | $level | CONFIG | $message" >> "$STATE_DIR/alerts.queue" } # Define critical files to monitor get_critical_files() { cat << 'EOF' /Users/mattbruce/.openclaw/openclaw.json /Users/mattbruce/.openclaw/workspace/AGENTS.md /Users/mattbruce/.openclaw/workspace/TOOLS.md /Users/mattbruce/.openclaw/workspace/BRAIN.md /Users/mattbruce/.openclaw/workspace/SOUL.md /Users/mattbruce/.openclaw/workspace/HEARTBEAT.md /Users/mattbruce/.openclaw/workspace/.openclaw/workspace-state.json /etc/ssh/sshd_config /etc/hosts ~/.ssh/authorized_keys ~/.zshrc ~/.bash_profile EOF } # Calculate file hash (MD5 for speed, or SHA256 for security) calculate_hash() { local file="$1" if [[ -f "$file" ]]; then md5 -q "$file" 2>/dev/null || md5sum "$file" 2>/dev/null | awk '{print $1}' || echo "ERROR" else echo "MISSING" fi } # Get file metadata get_metadata() { local file="$1" if [[ -f "$file" ]]; then stat -f "%Sm|%Su|%Sg|%A" -t "%Y-%m-%d %H:%M:%S" "$file" 2>/dev/null || stat -c "%y|%U|%G|%a" "$file" 2>/dev/null else echo "MISSING" fi } # Build baseline for a file build_baseline() { local file="$1" local baseline_file="$BASELINE_DIR/$(echo "$file" | tr '/' '_').baseline" local hash hash=$(calculate_hash "$file") local metadata metadata=$(get_metadata "$file") echo "${hash}|${metadata}|$(timestamp)" > "$baseline_file" echo "Baseline created for $file" } # Check file against baseline check_file() { local file="$1" local baseline_file="$BASELINE_DIR/$(echo "$file" | tr '/' '_').baseline" if [[ ! -f "$baseline_file" ]]; then # No baseline exists, create one build_baseline "$file" echo "NEW|${file}|No baseline existed" return fi if [[ ! -f "$file" ]]; then # File was deleted! echo "DELETED|${file}|File no longer exists" return fi # Read baseline local baseline_data baseline_data=$(cat "$baseline_file") local baseline_hash=$(echo "$baseline_data" | cut -d'|' -f1) local baseline_meta=$(echo "$baseline_data" | cut -d'|' -f2) # Get current state local current_hash current_hash=$(calculate_hash "$file") local current_meta current_meta=$(get_metadata "$file") # Compare if [[ "$current_hash" != "$baseline_hash" ]]; then echo "MODIFIED|${file}|Hash changed: $baseline_hash -> $current_hash" elif [[ "$current_meta" != "$baseline_meta" ]]; then echo "METACHANGE|${file}|Metadata changed: $baseline_meta -> $current_meta" else echo "UNCHANGED|${file}|" fi } # Check git repositories for uncommitted changes check_git_repos() { local changes_found="" # Check ~/.openclaw if it's a git repo if [[ -d "$HOME/.openclaw/.git" ]]; then local git_status git_status=$(cd "$HOME/.openclaw" && git status --porcelain 2>/dev/null) if [[ -n "$git_status" ]]; then changes_found="${changes_found}OPENCLAW_CONFIG:\n${git_status}\n\n" fi fi # Check workspace if [[ -d "$HOME/.openclaw/workspace/.git" ]]; then local git_status git_status=$(cd "$HOME/.openclaw/workspace" && git status --porcelain 2>/dev/null) if [[ -n "$git_status" ]]; then changes_found="${changes_found}WORKSPACE:\n${git_status}\n\n" fi fi echo "$changes_found" } # Main audit logic main() { local mode="${1:-check}" # check, init, or report log "=== Starting Config Audit (mode: $mode) ===" if [[ "$mode" == "init" ]]; then # Initialize all baselines log "Initializing baselines for all critical files..." get_critical_files | while read -r file; do [[ -z "$file" ]] && continue # Expand ~ to $HOME file="${file/#\~/$HOME}" if [[ -f "$file" ]]; then build_baseline "$file" log "Baseline created: $file" else log "File not found (skipped): $file" fi done log "Baseline initialization complete" return fi # Perform the audit local changes=() local modifications=0 local deletions=0 local new_files=0 local meta_changes=0 # Clear previous report echo "OpenClaw Config Audit Report" > "$AUDIT_REPORT" echo "Generated: $(timestamp)" >> "$AUDIT_REPORT" echo "========================================" >> "$AUDIT_REPORT" echo "" >> "$AUDIT_REPORT" # Check each critical file while IFS= read -r file; do [[ -z "$file" ]] && continue # Expand ~ to $HOME file="${file/#\~/$HOME}" local result result=$(check_file "$file") local file_status=$(echo "$result" | cut -d'|' -f1) local filepath=$(echo "$result" | cut -d'|' -f2) local details=$(echo "$result" | cut -d'|' -f3-) case "$file_status" in MODIFIED) changes+=("📝 MODIFIED: $filepath") modifications=$((modifications + 1)) echo "📝 MODIFIED: $filepath" >> "$AUDIT_REPORT" echo " Details: $details" >> "$AUDIT_REPORT" ;; DELETED) changes+=("🗑️ DELETED: $filepath") deletions=$((deletions + 1)) echo "🗑️ DELETED: $filepath" >> "$AUDIT_REPORT" ;; NEW) changes+=("📄 NEW: $filepath") new_files=$((new_files + 1)) echo "📄 NEW: $filepath" >> "$AUDIT_REPORT" ;; METACHANGE) changes+=("🔧 META: $filepath") meta_changes=$((meta_changes + 1)) echo "🔧 METADATA CHANGED: $filepath" >> "$AUDIT_REPORT" echo " Details: $details" >> "$AUDIT_REPORT" ;; esac done <<< "$(get_critical_files)" # Check git repos local git_changes git_changes=$(check_git_repos) if [[ -n "$git_changes" ]]; then echo "" >> "$AUDIT_REPORT" echo "📦 UNCOMMITTED GIT CHANGES:" >> "$AUDIT_REPORT" echo "$git_changes" >> "$AUDIT_REPORT" fi # Summary local total_changes=$((modifications + deletions + new_files + meta_changes)) echo "" >> "$AUDIT_REPORT" echo "========================================" >> "$AUDIT_REPORT" echo "Summary:" >> "$AUDIT_REPORT" echo " Modified files: $modifications" >> "$AUDIT_REPORT" echo " Deleted files: $deletions" >> "$AUDIT_REPORT" echo " New files: $new_files" >> "$AUDIT_REPORT" echo " Metadata changes: $meta_changes" >> "$AUDIT_REPORT" echo " Total changes: $total_changes" >> "$AUDIT_REPORT" # Update baselines for any changes detected (so we don't re-alert) if [[ $total_changes -gt 0 ]]; then log "Detected $total_changes configuration changes" # Rebuild baselines for modified files while IFS= read -r file; do [[ -z "$file" ]] && continue file="${file/#\~/$HOME}" local result result=$(check_file "$file") local check_status=$(echo "$result" | cut -d'|' -f1) if [[ "$check_status" == "MODIFIED" ]] || [[ "$check_status" == "METACHANGE" ]]; then build_baseline "$file" fi done <<< "$(get_critical_files)" # Send alert if significant changes detected if [[ $total_changes -gt 0 ]] && [[ "$mode" == "check" ]]; then local hostname=$(hostname -s) local change_list="" for change in "${changes[@]}"; do change_list="${change_list}${change}\n" done local alert_msg="🔍 **Daily Config Audit Alert** 🔍 **Host:** $hostname **Time:** $(timestamp) **Changes Detected:** $total_changes **Summary:** • Modified: $modifications • Deleted: $deletions • New files: $new_files • Metadata changes: $meta_changes **Details:** $change_list $(if [[ -n "$git_changes" ]]; then echo "📦 **Uncommitted git changes also detected**"; fi) _Review these changes to ensure they were authorized._ _Detected by OpenClaw Config Audit_" send_alert "AUDIT" "$alert_msg" log "Audit alert sent with $total_changes changes" # Log to daily security log local daily_log="/Users/mattbruce/.openclaw/workspace/memory/$(date '+%Y-%m-%d')-security.log" echo "CONFIG_AUDIT|$(timestamp)|$total_changes changes detected" >> "$daily_log" fi else log "No configuration changes detected" echo "" >> "$AUDIT_REPORT" echo "✅ No changes detected - all clear!" >> "$AUDIT_REPORT" fi log "=== Config Audit Complete ===" # If report mode, output the report if [[ "$mode" == "report" ]]; then cat "$AUDIT_REPORT" fi } # Run main function main "$@"