test-repo/scripts/security-monitors/config-audit.sh

314 lines
8.8 KiB
Bash
Executable File

#!/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 "$@"