Files
dotfiles/bin/dotfiles-diff.sh
2025-12-25 16:11:04 -05:00

434 lines
13 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# ============================================================================
# Dotfiles Diff & Audit Tool
# ============================================================================
# Compare configurations, audit for issues, and track changes.
#
# Usage:
# dotfiles-diff.sh # Show uncommitted changes
# dotfiles-diff.sh --symlinks # Verify symlink integrity
# dotfiles-diff.sh --secrets # Audit for exposed secrets
# dotfiles-diff.sh --permissions # Check file permissions
# dotfiles-diff.sh --audit # Full security audit
# ============================================================================
set +e
# Source bootstrap
source "${DOTFILES_HOME:-$HOME/.dotfiles}/zsh/lib/bootstrap.zsh" 2>/dev/null || {
DF_GREEN=$'\033[0;32m' DF_YELLOW=$'\033[1;33m' DF_RED=$'\033[0;31m'
DF_CYAN=$'\033[0;36m' DF_BLUE=$'\033[0;34m' DF_NC=$'\033[0m'
DF_DIM=$'\033[2m'
DOTFILES_HOME="${DOTFILES_HOME:-$HOME/.dotfiles}"
df_print_header() { echo "=== $1 ==="; }
df_print_success() { echo -e "${DF_GREEN}${DF_NC} $1"; }
df_print_error() { echo -e "${DF_RED}${DF_NC} $1" >&2; }
df_print_warning() { echo -e "${DF_YELLOW}${DF_NC} $1"; }
df_print_info() { echo -e "${DF_CYAN}${DF_NC} $1"; }
df_print_step() { echo -e "${DF_BLUE}==>${DF_NC} $1"; }
df_print_section() { echo -e "${DF_CYAN}$1:${DF_NC}"; }
df_print_indent() { echo " $1"; }
}
DOTFILES_DIR="${DOTFILES_HOME:-$HOME/.dotfiles}"
# ============================================================================
# Diff Functions
# ============================================================================
# Show git diff for uncommitted changes
show_git_diff() {
df_print_section "Uncommitted Changes"
if [[ ! -d "$DOTFILES_DIR/.git" ]]; then
df_print_warning "Not a git repository"
return 1
fi
cd "$DOTFILES_DIR"
local changes=$(git status --porcelain 2>/dev/null)
if [[ -z "$changes" ]]; then
df_print_success "No uncommitted changes"
return 0
fi
echo ""
echo "$changes" | while read -r status file; do
case "$status" in
M*|" M") echo -e " ${DF_YELLOW}modified:${DF_NC} $file" ;;
A*|"A ") echo -e " ${DF_GREEN}added:${DF_NC} $file" ;;
D*|" D") echo -e " ${DF_RED}deleted:${DF_NC} $file" ;;
R*) echo -e " ${DF_BLUE}renamed:${DF_NC} $file" ;;
\?\?) echo -e " ${DF_DIM}untracked:${DF_NC} $file" ;;
*) echo -e " ${status}: $file" ;;
esac
done
echo ""
df_print_step "View full diff: git -C $DOTFILES_DIR diff"
}
# Show what's different between repo and installed files
show_installed_diff() {
df_print_section "Installed File Differences"
echo ""
local files_to_check=(
"$HOME/.zshrc:$DOTFILES_DIR/zsh/.zshrc"
"$HOME/.gitconfig:$DOTFILES_DIR/git/.gitconfig"
"$HOME/.vimrc:$DOTFILES_DIR/vim/.vimrc"
"$HOME/.tmux.conf:$DOTFILES_DIR/tmux/.tmux.conf"
)
local has_diff=false
for pair in "${files_to_check[@]}"; do
local installed="${pair%%:*}"
local source="${pair#*:}"
local name=$(basename "$installed")
if [[ ! -e "$installed" ]]; then
echo -e " ${DF_YELLOW}${DF_NC} $name: not installed"
continue
fi
if [[ -L "$installed" ]]; then
local target=$(readlink -f "$installed")
if [[ "$target" == "$source" ]] || [[ "$target" == "$(readlink -f "$source")" ]]; then
echo -e " ${DF_GREEN}${DF_NC} $name: symlink OK"
else
echo -e " ${DF_YELLOW}${DF_NC} $name: symlink points elsewhere → $target"
has_diff=true
fi
else
# Regular file - check if different
if diff -q "$installed" "$source" &>/dev/null; then
echo -e " ${DF_GREEN}${DF_NC} $name: content matches (regular file)"
else
echo -e " ${DF_RED}${DF_NC} $name: differs from source"
has_diff=true
fi
fi
done
if [[ "$has_diff" == true ]]; then
echo ""
df_print_warning "Some files differ. Run installer to sync: ./install.sh"
fi
}
# ============================================================================
# Symlink Verification
# ============================================================================
check_symlinks() {
df_print_section "Symlink Integrity Check"
echo ""
local symlinks=(
"$HOME/.zshrc"
"$HOME/.gitconfig"
"$HOME/.vimrc"
"$HOME/.tmux.conf"
"$HOME/.config/nvim"
)
local broken=0
local missing=0
local ok=0
for link in "${symlinks[@]}"; do
local name=$(basename "$link")
if [[ ! -e "$link" && ! -L "$link" ]]; then
echo -e " ${DF_DIM}${DF_NC} $name: not installed"
((missing++))
elif [[ -L "$link" ]]; then
if [[ -e "$link" ]]; then
echo -e " ${DF_GREEN}${DF_NC} $name$(readlink "$link")"
((ok++))
else
echo -e " ${DF_RED}${DF_NC} $name: BROKEN → $(readlink "$link")"
((broken++))
fi
else
echo -e " ${DF_YELLOW}${DF_NC} $name: regular file (not symlink)"
fi
done
# Check bin scripts
echo ""
df_print_section "Bin Script Symlinks"
if [[ -d "$HOME/.local/bin" ]]; then
for script in "$HOME/.local/bin"/dotfiles-*.sh; do
[[ -e "$script" ]] || continue
local name=$(basename "$script")
if [[ -L "$script" ]]; then
if [[ -e "$script" ]]; then
echo -e " ${DF_GREEN}${DF_NC} $name"
((ok++))
else
echo -e " ${DF_RED}${DF_NC} $name: BROKEN"
((broken++))
fi
fi
done
fi
echo ""
df_print_section "Summary"
df_print_indent "OK: $ok | Missing: $missing | Broken: $broken"
if (( broken > 0 )); then
echo ""
df_print_error "Found $broken broken symlinks!"
df_print_indent "Fix with: dffix"
fi
}
# ============================================================================
# Security Audit
# ============================================================================
audit_secrets() {
df_print_section "Secret Detection Audit"
echo ""
cd "$DOTFILES_DIR"
local issues=0
# Patterns that might indicate secrets
local patterns=(
'api[_-]?key\s*[:=]'
'secret[_-]?key\s*[:=]'
'password\s*[:=]'
'token\s*[:=]'
'private[_-]?key'
'BEGIN (RSA|DSA|EC|OPENSSH) PRIVATE KEY'
'aws_access_key_id'
'aws_secret_access_key'
)
df_print_step "Scanning tracked files..."
for pattern in "${patterns[@]}"; do
local matches=$(git grep -l -i -E "$pattern" 2>/dev/null || true)
if [[ -n "$matches" ]]; then
echo ""
df_print_warning "Pattern '$pattern' found in:"
echo "$matches" | while read -r file; do
df_print_indent " $file"
((issues++))
done
fi
done
if (( issues == 0 )); then
df_print_success "No obvious secrets found in tracked files"
else
echo ""
df_print_error "Found potential secrets in $issues location(s)"
df_print_indent "Review these files and use the vault for sensitive data"
fi
# Check git history (limited)
echo ""
df_print_step "Scanning recent git history (last 50 commits)..."
local history_issues=$(git log -50 --all -p 2>/dev/null | grep -c -i -E 'password|secret|api.?key|token' || echo 0)
if (( history_issues > 0 )); then
df_print_warning "Found $history_issues potential matches in git history"
df_print_indent "Consider: git filter-branch or BFG Repo Cleaner"
else
df_print_success "No obvious secrets in recent history"
fi
}
audit_permissions() {
df_print_section "File Permission Audit"
echo ""
cd "$DOTFILES_DIR"
local issues=0
# Check for world-writable files
df_print_step "Checking for world-writable files..."
local world_writable=$(find . -type f -perm -o+w 2>/dev/null | grep -v ".git" || true)
if [[ -n "$world_writable" ]]; then
df_print_warning "World-writable files found:"
echo "$world_writable" | while read -r file; do
df_print_indent "$file"
((issues++))
done
else
df_print_success "No world-writable files"
fi
# Check bin scripts are executable
echo ""
df_print_step "Checking bin script permissions..."
for script in "$DOTFILES_DIR/bin"/*.sh; do
[[ -f "$script" ]] || continue
local name=$(basename "$script")
if [[ -x "$script" ]]; then
echo -e " ${DF_GREEN}${DF_NC} $name"
else
echo -e " ${DF_RED}${DF_NC} $name: not executable"
((issues++))
fi
done
# Check sensitive directories
echo ""
df_print_step "Checking sensitive directories..."
if [[ -d "$DOTFILES_DIR/vault" ]]; then
local vault_perms=$(stat -c %a "$DOTFILES_DIR/vault" 2>/dev/null || stat -f %Lp "$DOTFILES_DIR/vault" 2>/dev/null)
if [[ "$vault_perms" == "700" ]]; then
df_print_success "vault/ directory: 700 (secure)"
else
df_print_warning "vault/ directory: $vault_perms (should be 700)"
((issues++))
fi
fi
echo ""
if (( issues == 0 )); then
df_print_success "All permission checks passed"
else
df_print_warning "Found $issues permission issues"
fi
}
full_audit() {
df_print_step "Running full security audit..."
echo ""
audit_secrets
echo ""
audit_permissions
echo ""
check_symlinks
}
# ============================================================================
# Machine Comparison
# ============================================================================
compare_machines() {
df_print_section "Machine Configuration Comparison"
echo ""
local machines_dir="$DOTFILES_DIR/machines"
if [[ ! -d "$machines_dir" ]] || [[ -z "$(ls -A "$machines_dir" 2>/dev/null)" ]]; then
df_print_info "No machine configs to compare"
df_print_indent "Create with: df_machine_create"
return
fi
# Get list of config files (bash-compatible)
local configs=()
for f in "$machines_dir"/*.zsh; do
[[ -f "$f" ]] && configs+=("$f")
done
if [[ ${#configs[@]} -lt 2 ]]; then
df_print_info "Need at least 2 machine configs to compare"
return
fi
df_print_step "Available configs:"
for config in "${configs[@]}"; do
df_print_indent "$(basename "$config" .zsh)"
done
echo ""
read -p "Compare which two? (e.g., 'laptop server'): " config1 config2
if [[ -f "$machines_dir/$config1.zsh" && -f "$machines_dir/$config2.zsh" ]]; then
echo ""
diff -u --color=always "$machines_dir/$config1.zsh" "$machines_dir/$config2.zsh" || true
else
df_print_error "Config not found"
fi
}
# ============================================================================
# Help
# ============================================================================
show_help() {
cat << 'EOF'
Dotfiles Diff & Audit Tool
Usage: dotfiles-diff.sh [OPTIONS]
Options:
(none) Show uncommitted git changes
--installed Compare installed files with source
--symlinks Verify symlink integrity
--secrets Scan for exposed secrets
--permissions Check file permissions
--audit Full security audit (secrets + permissions + symlinks)
--machines Compare machine configurations
--help Show this help
Examples:
dotfiles-diff.sh # Quick git status
dotfiles-diff.sh --symlinks # Verify all symlinks are valid
dotfiles-diff.sh --audit # Full security audit
dotfiles-diff.sh --machines # Compare laptop vs server configs
EOF
}
# ============================================================================
# Main
# ============================================================================
main() {
df_print_header "dotfiles-diff"
case "${1:-}" in
--installed|-i)
show_installed_diff
;;
--symlinks|-s)
check_symlinks
;;
--secrets)
audit_secrets
;;
--permissions|-p)
audit_permissions
;;
--audit|-a)
full_audit
;;
--machines|-m)
compare_machines
;;
--help|-h)
show_help
;;
*)
show_git_diff
echo ""
show_installed_diff
;;
esac
}
main "$@"