Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257#!/usr/bin/env bash# Pre-commit hook to:# 1. Warn about large deletions in critical files (prevents clobbering during merges)# 2. Block commits containing secrets/credentials (prevents leaks)RED='\033[0;31m'YELLOW='\033[1;33m'GREEN='\033[0;32m'NC='\033[0m' # No ColorNODE_BIN="${NODE_BIN:-node}"################################################################################ PART 1: SECRET DETECTION###############################################################################echo -e "${GREEN}๐ Scanning for secrets...${NC}"# Patterns to detect secrets (add new patterns here as needed)# Format: "PATTERN_NAME:REGEX"# NOTE: Use POSIX extended regex compatible with grep -ESECRET_PATTERNS=( # SMTP/Email credentials - hardcoded passwords "SMTP_PASSWORD:(^|[[:space:](;])SMTP_PASS[[:space:]]*=[[:space:]]*['\"][^'\"[:space:]]{8,}['\"]" # Google App Passwords (4 groups of 4 lowercase letters) # Only match when it looks like a credential assignment, not prose "GOOGLE_APP_PASSWORD:Pass(word)?[[:space:]]*[:=][[:space:]]*.*[a-z]{4} [a-z]{4} [a-z]{4} [a-z]{4}" # API Keys and Tokens (generic patterns) "AWS_KEY:AKIA[0-9A-Z]{16}" "GITHUB_TOKEN:gh[pousr]_[A-Za-z0-9_]{36,}" "PRIVATE_KEY:BEGIN.*PRIVATE KEY" "JWT_SECRET:jwt[_-]?secret[[:space:]]*[=:][[:space:]]*['\"][^'\"]{16,}['\"]" # Database connection strings with passwords "MONGODB_URI:mongodb.*://[^:]+:[^@]+@" "POSTGRES_URI:postgres.*://[^:]+:[^@]+@" # Generic secret patterns (hardcoded, not env var references) "PASSWORD_HARDCODE:pass(word)?[[:space:]]*[:=][[:space:]]*['\"][^'\"$]{8,}['\"]" "SECRET_HARDCODE:secret[[:space:]]*[:=][[:space:]]*['\"][^'\"$]{16,}['\"]" "API_KEY_HARDCODE:api[_-]?key[[:space:]]*[:=][[:space:]]*['\"][^'\"$]{16,}['\"]")# Files/paths to exclude from secret scanningEXCLUDE_PATTERNS=( "*.lock" "package-lock.json" "yarn.lock" ".githooks/pre-commit" # Don't flag this file's patterns "*.md" # Be more lenient with docs (still scan but warn only))# Known false positives to ignore (exact strings that trigger patterns but aren't secrets)FALSE_POSITIVE_ALLOWLIST=( "Styles" # UI label, triggers PASSWORD_HARDCODE via case-insensitive "s****s" "STYLES" # Same as above "styles" # Same as above "Second pass" # Code comment in chat.mjs "class=" # HTML class attributes "className=" # React className "AC_PASS" # Default wifi AP password (public, not a secret) "enter password" # UI label in wifi.mjs password prompt "Seed pass" # GPU shader comment in gpu-effects.mjs)secret_errors=0secret_warnings=0# Get list of staged files (excluding deletions)staged_files=$(git diff --cached --name-only --diff-filter=d)for file in $staged_files; do # Skip binary files if file "$file" 2>/dev/null | grep -q "binary"; then continue fi # Check if file should be excluded skip_file=false warn_only=false for exclude in "${EXCLUDE_PATTERNS[@]}"; do if [[ "$file" == $exclude ]] || [[ "$file" == *"$exclude" ]]; then if [[ "$exclude" == "*.md" ]]; then warn_only=true else skip_file=true fi break fi done if $skip_file; then continue fi # Get the staged content of the file staged_content=$(git show ":$file" 2>/dev/null) for pattern_entry in "${SECRET_PATTERNS[@]}"; do pattern_name="${pattern_entry%%:*}" pattern_regex="${pattern_entry#*:}" # Search for pattern in staged content if echo "$staged_content" | grep -qiE "$pattern_regex"; then matching_lines=$(echo "$staged_content" | grep -niE "$pattern_regex" | head -3) # Check if all matches are in the allowlist (false positives) is_false_positive=true while IFS= read -r line; do line_is_allowed=false for allowed in "${FALSE_POSITIVE_ALLOWLIST[@]}"; do if echo "$line" | grep -qi "$allowed"; then line_is_allowed=true break fi done if ! $line_is_allowed; then is_false_positive=false break fi done <<< "$matching_lines" # Skip if this is a known false positive if $is_false_positive; then continue fi if $warn_only; then echo -e "${YELLOW}โ ๏ธ Possible secret in ${file} (${pattern_name})${NC}" echo "$matching_lines" | while read -r line; do echo -e " ${YELLOW}$line${NC}" done secret_warnings=$((secret_warnings + 1)) else echo -e "${RED}๐จ SECRET DETECTED in ${file} (${pattern_name})${NC}" echo "$matching_lines" | while read -r line; do # Mask the actual secret value masked=$(echo "$line" | sed -E 's/([=:][[:space:]]*['"'"'"]?)([^'"'"'"]{4})[^'"'"'"]*([^'"'"'"]{2})(['"'"'"]?)/\1\2****\3\4/g') echo -e " ${RED}$masked${NC}" done secret_errors=$((secret_errors + 1)) fi fi donedoneif [ "$secret_errors" -gt 0 ]; then echo "" echo -e "${RED}โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ${NC}" echo -e "${RED} ๐จ $secret_errors SECRET(S) DETECTED - COMMIT BLOCKED${NC}" echo -e "${RED}โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ${NC}" echo "" echo "To fix:" echo " 1. Remove the secrets from the staged files" echo " 2. Use environment variables instead" echo " 3. Store secrets in vault (aesthetic-computer-vault/)" echo "" echo "To bypass (NOT RECOMMENDED): git commit --no-verify" echo "" exit 1fiif [ "$secret_warnings" -gt 0 ]; then echo -e "${YELLOW}โ ๏ธ $secret_warnings possible secret(s) in docs - review carefully${NC}"fiecho -e "${GREEN}โ No secrets detected${NC}"echo ""################################################################################ PART 2: CRITICAL FILE PROTECTION (prevents merge clobbering)################################################################################ Critical files that should never lose significant contentCRITICAL_FILES=( "artery/artery-tui.mjs" "dotfiles/dot_config/emacs.el" "system/public/aesthetic.computer/lib/udp.mjs" "system/public/aesthetic.computer/lib/logs.mjs" "system/public/aesthetic.computer/lib/speaker.mjs" "system/public/aesthetic.computer/bios.mjs")# Minimum expected line counts for critical files (set conservatively)declare -A MIN_LINESMIN_LINES["artery/artery-tui.mjs"]=5000MIN_LINES["dotfiles/dot_config/emacs.el"]=1000MIN_LINES["system/public/aesthetic.computer/lib/udp.mjs"]=150MIN_LINES["system/public/aesthetic.computer/lib/logs.mjs"]=100MIN_LINES["system/public/aesthetic.computer/lib/speaker.mjs"]=800MIN_LINES["system/public/aesthetic.computer/bios.mjs"]=200# Threshold for warning about large deletions (percentage)DELETION_THRESHOLD=30RED='\033[0;31m'YELLOW='\033[1;33m'NC='\033[0m' # No Colorwarnings=0for file in "${CRITICAL_FILES[@]}"; do # Check if file is being modified in this commit if git diff --cached --name-only | grep -q "^${file}$"; then # Get current staged line count staged_lines=$(git show :${file} 2>/dev/null | wc -l) # Get HEAD line count (before this commit) head_lines=$(git show HEAD:${file} 2>/dev/null | wc -l) if [ "$head_lines" -gt 0 ] && [ "$staged_lines" -gt 0 ]; then # Calculate deletion percentage deleted=$((head_lines - staged_lines)) if [ "$deleted" -gt 0 ]; then pct=$((deleted * 100 / head_lines)) if [ "$pct" -ge "$DELETION_THRESHOLD" ]; then echo -e "${RED}โ ๏ธ WARNING: Large deletion detected in ${file}${NC}" echo -e " ${YELLOW}Before: ${head_lines} lines โ After: ${staged_lines} lines (${pct}% deleted)${NC}" warnings=$((warnings + 1)) fi fi fi # Check minimum line count min=${MIN_LINES[$file]} if [ -n "$min" ] && [ "$staged_lines" -lt "$min" ]; then echo -e "${RED}โ ๏ธ WARNING: ${file} is below minimum expected size${NC}" echo -e " ${YELLOW}Current: ${staged_lines} lines, Expected minimum: ${min} lines${NC}" warnings=$((warnings + 1)) fi fidoneif [ "$warnings" -gt 0 ]; then echo "" echo -e "${YELLOW}โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ${NC}" echo -e "${YELLOW} $warnings file(s) may have been accidentally clobbered!${NC}" echo -e "${YELLOW} This often happens during merge conflict resolution.${NC}" echo -e "${YELLOW}โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ${NC}" echo "" echo "To proceed anyway, use: git commit --no-verify" echo "To abort and fix, use: git reset HEAD" echo "" exit 1fi################################################################################ PART 3: PAPERS โ oven builds PDFs automatically on push to main# (see papers/SCORE.md ยง Building ยง Automatic)# No local rebuild needed in pre-commit.###############################################################################exit 0