314 lines
8.8 KiB
Bash
Executable File
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 "$@"
|