|
5 | 5 | # Based on git-secrets update-all-repos.sh |
6 | 6 |
|
7 | 7 | # Usage examples: |
8 | | -# ./update-all-repos.sh # Updates all repos in current directory |
9 | | -# ./update-all-repos.sh ~/Projects # Updates all repos in ~/Projects |
| 8 | +# ./update-all-repos.sh # Updates all repos in current directory (recursively) |
| 9 | +# ./update-all-repos.sh ~/Projects # Updates all repos in ~/Projects (recursively) |
10 | 10 | # ./update-all-repos.sh ~/Sites ~/Projects # Updates repos in multiple directories |
| 11 | +# sudo ./update-all-repos.sh /var # Updates repos in system directories (requires root) |
| 12 | +# |
| 13 | +# Environment variables: |
| 14 | +# MAX_DEPTH=3 ./update-all-repos.sh ~/ # Limit recursion depth (default: unlimited) |
| 15 | +# |
| 16 | +# WARNING: Scanning large directories like ~ or / can take a very long time! |
| 17 | +# It's better to specify specific project directories. |
11 | 18 |
|
12 | 19 | HIGHLIGHT="\e[01;34m" |
13 | 20 | SUCCESS="\e[01;32m" |
14 | 21 | ERROR="\e[01;31m" |
15 | 22 | WARNING="\e[01;33m" |
16 | 23 | NORMAL='\e[00m' |
17 | 24 |
|
| 25 | +# Configuration |
| 26 | +MAX_DEPTH="${MAX_DEPTH:-}" # Default: unlimited depth |
| 27 | + |
| 28 | +# Temporary files for tracking stats across subshells |
| 29 | +STATS_DIR=$(mktemp -d) |
| 30 | +trap "rm -rf $STATS_DIR" EXIT |
| 31 | + |
| 32 | +touch "$STATS_DIR/found" |
| 33 | +touch "$STATS_DIR/updated" |
| 34 | +touch "$STATS_DIR/failed" |
| 35 | +touch "$STATS_DIR/skipped" |
| 36 | + |
| 37 | +function increment_stat { |
| 38 | + local stat_file="$STATS_DIR/$1" |
| 39 | + echo "1" >> "$stat_file" |
| 40 | +} |
| 41 | + |
| 42 | +function get_stat { |
| 43 | + local stat_file="$STATS_DIR/$1" |
| 44 | + wc -l < "$stat_file" 2>/dev/null | tr -d ' ' || echo "0" |
| 45 | +} |
| 46 | + |
18 | 47 | # Check if gitleaks is installed |
19 | 48 | if ! command -v gitleaks &> /dev/null; then |
20 | 49 | echo -e "${ERROR}Error: gitleaks is not installed${NORMAL}" |
|
27 | 56 |
|
28 | 57 | # Check if global template is set up |
29 | 58 | TEMPLATE_DIR="$HOME/.git-template" |
| 59 | +# Handle case when running with sudo - use the actual user's home |
| 60 | +if [ -n "$SUDO_USER" ]; then |
| 61 | + TEMPLATE_DIR=$(eval echo ~$SUDO_USER)/.git-template |
| 62 | +fi |
| 63 | + |
30 | 64 | if [ ! -d "$TEMPLATE_DIR/hooks" ]; then |
31 | 65 | echo -e "${ERROR}Error: Git template directory not found${NORMAL}" |
| 66 | + echo "Expected location: $TEMPLATE_DIR/hooks" |
32 | 67 | echo "Please run ./install-gitleaks-global.sh first" |
33 | 68 | exit 1 |
34 | 69 | fi |
@@ -224,111 +259,232 @@ function install_native_hooks { |
224 | 259 | return 0 |
225 | 260 | } |
226 | 261 |
|
227 | | -function update_repo { |
228 | | - local d="$1" |
| 262 | +# Function to process a single git repository |
| 263 | +function process_repo { |
| 264 | + local repodir="$1" |
229 | 265 |
|
230 | | - # Skip if not a directory or is a symbolic link |
231 | | - if [ ! -d "$d" ] || [ -L "$d" ]; then |
232 | | - return 0 |
233 | | - fi |
| 266 | + cd "$repodir" 2>/dev/null || { |
| 267 | + echo -e "${WARNING}⚠${NORMAL} Cannot access repository: $repodir (permission denied)" |
| 268 | + increment_stat "skipped" |
| 269 | + return 1 |
| 270 | + } |
234 | 271 |
|
235 | | - # Try to enter directory, return if fails |
236 | | - cd "$d" > /dev/null 2>&1 || return 0 |
| 272 | + increment_stat "found" |
| 273 | + printf "%b\n" "${HIGHLIGHT}Installing gitleaks hooks in $(pwd)${NORMAL}" |
237 | 274 |
|
238 | | - if [ -d ".git" ]; then |
239 | | - printf "%b\n" "${HIGHLIGHT}Installing gitleaks hooks in $(pwd)${NORMAL}" |
240 | | - |
241 | | - # CRITICAL: Check if core.hooksPath is configured but directory doesn't exist |
242 | | - HOOKS_PATH=$(git config core.hooksPath 2>/dev/null || echo "") |
243 | | - if [ -n "$HOOKS_PATH" ] && [ ! -d "$HOOKS_PATH" ]; then |
244 | | - echo -e " ${ERROR}🚨 CRITICAL${NORMAL}: Git is configured to use hooks from '$HOOKS_PATH' but directory doesn't exist!" |
245 | | - echo -e " ${WARNING}⚠${NORMAL} This means NO hooks are running - security bypass!" |
246 | | - echo -e " ${HIGHLIGHT}→${NORMAL} Fixing: Unsetting core.hooksPath and installing native hooks" |
247 | | - git config --unset core.hooksPath |
248 | | - install_native_hooks |
| 275 | + # Check if .git directory is writable |
| 276 | + if [ ! -w ".git" ]; then |
| 277 | + echo -e " ${ERROR}✗${NORMAL} Cannot write to .git directory (permission denied)" |
| 278 | + echo -e " ${WARNING}⚠${NORMAL} Repository owned by: $(stat -c '%U:%G' .git 2>/dev/null || echo 'unknown')" |
| 279 | + echo -e " ${WARNING}⚠${NORMAL} Current user: $(whoami)" |
| 280 | + if [ "$EUID" -ne 0 ]; then |
| 281 | + echo -e " ${HIGHLIGHT}→${NORMAL} Tip: Run script with sudo to update system repositories" |
| 282 | + fi |
| 283 | + increment_stat "failed" |
| 284 | + return 1 |
| 285 | + fi |
| 286 | + |
| 287 | + # CRITICAL: Check if core.hooksPath is configured but directory doesn't exist |
| 288 | + HOOKS_PATH=$(git config core.hooksPath 2>/dev/null || echo "") |
| 289 | + if [ -n "$HOOKS_PATH" ] && [ ! -d "$HOOKS_PATH" ]; then |
| 290 | + echo -e " ${ERROR}🚨 CRITICAL${NORMAL}: Git is configured to use hooks from '$HOOKS_PATH' but directory doesn't exist!" |
| 291 | + echo -e " ${WARNING}⚠${NORMAL} This means NO hooks are running - security bypass!" |
| 292 | + echo -e " ${HIGHLIGHT}→${NORMAL} Fixing: Unsetting core.hooksPath and installing native hooks" |
| 293 | + git config --unset core.hooksPath |
| 294 | + if install_native_hooks; then |
| 295 | + increment_stat "updated" |
249 | 296 | return 0 |
| 297 | + else |
| 298 | + increment_stat "failed" |
| 299 | + return 1 |
250 | 300 | fi |
| 301 | + fi |
| 302 | + |
| 303 | + # Detect if this repo uses Husky |
| 304 | + if [ -d ".husky" ]; then |
| 305 | + echo -e " ${HIGHLIGHT}→${NORMAL} Detected Husky repository" |
251 | 306 |
|
252 | | - # Detect if this repo uses Husky |
253 | | - if [ -d ".husky" ]; then |
254 | | - echo -e " ${HIGHLIGHT}→${NORMAL} Detected Husky repository" |
255 | | - |
256 | | - # Check if pre-commit exists |
257 | | - if [ -f ".husky/pre-commit" ]; then |
258 | | - inject_gitleaks_husky ".husky/pre-commit" |
| 307 | + # Check if pre-commit exists |
| 308 | + if [ -f ".husky/pre-commit" ]; then |
| 309 | + if inject_gitleaks_husky ".husky/pre-commit"; then |
| 310 | + increment_stat "updated" |
| 311 | + return 0 |
259 | 312 | else |
260 | | - # Check if husky.sh exists to confirm it's a valid Husky setup |
261 | | - if [ -f ".husky/_/husky.sh" ] || [ -f ".husky/husky.sh" ]; then |
262 | | - create_husky_precommit ".husky/pre-commit" |
263 | | - else |
264 | | - echo -e " ${WARNING}⚠${NORMAL} Husky directory exists but appears incomplete" |
265 | | - echo -e " ${HIGHLIGHT}→${NORMAL} Installing native Git hooks as fallback" |
266 | | - install_native_hooks |
267 | | - fi |
| 313 | + increment_stat "failed" |
| 314 | + return 1 |
268 | 315 | fi |
269 | 316 | else |
270 | | - # Check if core.hooksPath points to .husky but .husky doesn't exist |
271 | | - if [ "$HOOKS_PATH" = ".husky/_" ] || [ "$HOOKS_PATH" = ".husky" ]; then |
272 | | - echo -e " ${WARNING}⚠${NORMAL} Repo was using Husky but .husky/ is missing" |
273 | | - echo -e " ${HIGHLIGHT}→${NORMAL} Unsetting core.hooksPath and using native hooks" |
274 | | - git config --unset core.hooksPath |
| 317 | + # Check if husky.sh exists to confirm it's a valid Husky setup |
| 318 | + if [ -f ".husky/_/husky.sh" ] || [ -f ".husky/husky.sh" ]; then |
| 319 | + if create_husky_precommit ".husky/pre-commit"; then |
| 320 | + increment_stat "updated" |
| 321 | + return 0 |
| 322 | + else |
| 323 | + increment_stat "failed" |
| 324 | + return 1 |
| 325 | + fi |
| 326 | + else |
| 327 | + echo -e " ${WARNING}⚠${NORMAL} Husky directory exists but appears incomplete" |
| 328 | + echo -e " ${HIGHLIGHT}→${NORMAL} Installing native Git hooks as fallback" |
| 329 | + if install_native_hooks; then |
| 330 | + increment_stat "updated" |
| 331 | + return 0 |
| 332 | + else |
| 333 | + increment_stat "failed" |
| 334 | + return 1 |
| 335 | + fi |
275 | 336 | fi |
276 | | - |
277 | | - # Standard git hooks installation |
278 | | - echo -e " ${HIGHLIGHT}→${NORMAL} Using native Git hooks" |
279 | | - install_native_hooks |
280 | 337 | fi |
281 | 338 | else |
282 | | - # Not a git repo, scan subdirectories (but with protection) |
283 | | - scan_dirs * 2>/dev/null || true |
284 | | - fi |
285 | | - |
286 | | - cd .. > /dev/null 2>&1 || true |
287 | | -} |
288 | | - |
289 | | -function scan_dirs { |
290 | | - for x in "$@"; do |
291 | | - # Skip hidden directories and problematic directories |
292 | | - if [[ "$x" == .* ]] || [[ "$x" == "__MACOSX" ]] || [[ "$x" == "node_modules" ]]; then |
293 | | - continue |
| 339 | + # Check if core.hooksPath points to .husky but .husky doesn't exist |
| 340 | + if [ "$HOOKS_PATH" = ".husky/_" ] || [ "$HOOKS_PATH" = ".husky" ]; then |
| 341 | + echo -e " ${WARNING}⚠${NORMAL} Repo was using Husky but .husky/ is missing" |
| 342 | + echo -e " ${HIGHLIGHT}→${NORMAL} Unsetting core.hooksPath and using native hooks" |
| 343 | + git config --unset core.hooksPath |
294 | 344 | fi |
295 | | - update_repo "$x" || true # Continue even if update_repo fails |
296 | | - done |
| 345 | + |
| 346 | + # Standard git hooks installation |
| 347 | + echo -e " ${HIGHLIGHT}→${NORMAL} Using native Git hooks" |
| 348 | + if install_native_hooks; then |
| 349 | + increment_stat "updated" |
| 350 | + return 0 |
| 351 | + else |
| 352 | + increment_stat "failed" |
| 353 | + return 1 |
| 354 | + fi |
| 355 | + fi |
297 | 356 | } |
298 | 357 |
|
299 | 358 | function update_directory { |
300 | | - if [ "$1" != "" ]; then |
301 | | - cd "$1" > /dev/null 2>&1 || { |
302 | | - echo -e "${ERROR}✗${NORMAL} Cannot access directory: $1" |
| 359 | + local target_dir="$1" |
| 360 | + |
| 361 | + # Use current directory if none specified |
| 362 | + if [ -z "$target_dir" ]; then |
| 363 | + target_dir="$PWD" |
| 364 | + fi |
| 365 | + |
| 366 | + # Convert to absolute path |
| 367 | + if [[ "$target_dir" != /* ]]; then |
| 368 | + target_dir="$PWD/$target_dir" |
| 369 | + fi |
| 370 | + |
| 371 | + # Check if directory exists and is accessible |
| 372 | + if [ ! -d "$target_dir" ]; then |
| 373 | + echo -e "${ERROR}✗${NORMAL} Directory does not exist: $target_dir" |
| 374 | + return 1 |
| 375 | + fi |
| 376 | + |
| 377 | + if [ ! -r "$target_dir" ]; then |
| 378 | + echo -e "${ERROR}✗${NORMAL} Cannot read directory: $target_dir (permission denied)" |
| 379 | + if [ "$EUID" -ne 0 ]; then |
| 380 | + echo -e "${HIGHLIGHT}→${NORMAL} Try running with sudo: sudo ./update-all-repos.sh $target_dir" |
| 381 | + fi |
| 382 | + return 1 |
| 383 | + fi |
| 384 | + |
| 385 | + # Warn if trying to update system directories |
| 386 | + if [[ "$target_dir" == "/var"* ]] || [[ "$target_dir" == "/etc"* ]] || [[ "$target_dir" == "/sys"* ]] || [[ "$target_dir" == "/proc"* ]]; then |
| 387 | + echo -e "${WARNING}⚠${NORMAL} WARNING: Scanning system directory: ${target_dir}" |
| 388 | + if [ "$EUID" -ne 0 ]; then |
| 389 | + echo -e "${ERROR}✗${NORMAL} ERROR: System directories require root privileges" |
| 390 | + echo -e "${HIGHLIGHT}→${NORMAL} Please run with sudo: sudo ./update-all-repos.sh \"$target_dir\"" |
303 | 391 | return 1 |
304 | | - } |
| 392 | + fi |
| 393 | + echo -e "${SUCCESS}✓${NORMAL} Running with root privileges" |
305 | 394 | fi |
306 | | - printf "%b\n" "${HIGHLIGHT}Scanning ${PWD} for git repositories...${NORMAL}\n" |
307 | | - scan_dirs * 2>/dev/null || true |
| 395 | + |
| 396 | + printf "%b\n" "${HIGHLIGHT}Scanning ${target_dir} recursively for git repositories...${NORMAL}" |
| 397 | + |
| 398 | + # Build find command with optional depth limit |
| 399 | + local find_cmd="find \"$target_dir\"" |
| 400 | + if [ -n "$MAX_DEPTH" ]; then |
| 401 | + find_cmd="$find_cmd -maxdepth $MAX_DEPTH" |
| 402 | + echo -e "${HIGHLIGHT}→${NORMAL} Maximum depth: $MAX_DEPTH levels" |
| 403 | + else |
| 404 | + echo -e "${WARNING}⚠${NORMAL} WARNING: Unlimited depth - this may take a very long time for large directories!" |
| 405 | + echo -e "${HIGHLIGHT}→${NORMAL} Tip: Set MAX_DEPTH to limit recursion (e.g., MAX_DEPTH=3 ./update-all-repos.sh ~)" |
| 406 | + fi |
| 407 | + echo -e "${HIGHLIGHT}→${NORMAL} Press Ctrl+C to cancel if this takes too long" |
| 408 | + echo "" |
| 409 | + |
| 410 | + # Use find to recursively locate all .git directories |
| 411 | + # Exclude common large directories to speed up search |
| 412 | + # The -print0 and read -d '' handle filenames with spaces and special characters |
| 413 | + local count=0 |
| 414 | + while IFS= read -r -d '' gitdir; do |
| 415 | + local repodir=$(dirname "$gitdir") |
| 416 | + |
| 417 | + # Skip submodules (git repos inside .git directories) |
| 418 | + if [[ "$repodir" == *"/.git/"* ]] || [[ "$repodir" == *"/.git" ]]; then |
| 419 | + continue |
| 420 | + fi |
| 421 | + |
| 422 | + count=$((count + 1)) |
| 423 | + echo -e "${HIGHLIGHT}[$count]${NORMAL} Found: $repodir" |
| 424 | + |
| 425 | + # Process this repository in current shell (not subshell) to track stats |
| 426 | + (process_repo "$repodir") |
| 427 | + echo "" |
| 428 | + |
| 429 | + done < <(find "$target_dir" \ |
| 430 | + ${MAX_DEPTH:+-maxdepth $MAX_DEPTH} \ |
| 431 | + -type d \ |
| 432 | + \( -name "node_modules" -o -name ".npm" -o -name ".cache" -o -name "__pycache__" -o -name ".venv" -o -name "venv" -o -name ".local" -o -name ".cargo" -o -name ".rustup" -o -name ".m2" -o -name ".gradle" -o -name "target" -o -name "build" -o -name "dist" -o -name "vendor" -o -name ".bundle" \) -prune -o \ |
| 433 | + -type d -name ".git" -print0 2>/dev/null) |
308 | 434 | } |
309 | 435 |
|
310 | 436 | # Main execution |
311 | 437 | echo -e "${HIGHLIGHT}========================================${NORMAL}" |
312 | 438 | echo -e "${HIGHLIGHT}Gitleaks Hook Installer${NORMAL}" |
313 | 439 | echo -e "${HIGHLIGHT}========================================${NORMAL}\n" |
314 | 440 |
|
315 | | -if [ "$1" == "" ]; then |
316 | | - update_directory |
| 441 | +if [ "$EUID" -eq 0 ]; then |
| 442 | + echo -e "${WARNING}⚠${NORMAL} Running as root (sudo)" |
| 443 | + echo -e "${HIGHLIGHT}→${NORMAL} Will be able to update system-owned repositories" |
| 444 | + echo "" |
| 445 | +fi |
| 446 | + |
| 447 | +if [ "$#" -eq 0 ]; then |
| 448 | + update_directory "$PWD" |
317 | 449 | else |
318 | 450 | for dir in "$@"; do |
319 | 451 | update_directory "$dir" |
| 452 | + echo "" |
320 | 453 | done |
321 | 454 | fi |
322 | 455 |
|
| 456 | +# Get final statistics |
| 457 | +REPOS_FOUND=$(get_stat "found") |
| 458 | +REPOS_UPDATED=$(get_stat "updated") |
| 459 | +REPOS_FAILED=$(get_stat "failed") |
| 460 | +REPOS_SKIPPED=$(get_stat "skipped") |
| 461 | + |
323 | 462 | echo -e "\n${SUCCESS}========================================${NORMAL}" |
324 | 463 | echo -e "${SUCCESS}Update Complete!${NORMAL}" |
325 | 464 | echo -e "${SUCCESS}========================================${NORMAL}\n" |
326 | 465 |
|
327 | 466 | echo -e "${HIGHLIGHT}Summary:${NORMAL}" |
328 | | -echo " • Gitleaks pre-commit hooks have been installed in all git repositories" |
| 467 | +echo " • Git repositories found: $REPOS_FOUND" |
| 468 | +echo " • Successfully updated: $REPOS_UPDATED" |
| 469 | +echo " • Failed (permission denied): $REPOS_FAILED" |
| 470 | +echo " • Skipped (inaccessible): $REPOS_SKIPPED" |
329 | 471 | echo " • Both Husky and native Git hooks are supported" |
330 | 472 | echo " • Future commits will be scanned for blockchain private keys and secrets" |
331 | 473 | echo "" |
| 474 | + |
| 475 | +# Show warning if there were permission failures |
| 476 | +if [ "$REPOS_FAILED" -gt 0 ] || [ "$REPOS_SKIPPED" -gt 0 ]; then |
| 477 | + echo -e "${WARNING}⚠${NORMAL} ${WARNING}WARNING: Some repositories could not be updated due to permission issues${NORMAL}" |
| 478 | + if [ "$EUID" -ne 0 ]; then |
| 479 | + echo -e "${HIGHLIGHT}→${NORMAL} To update system repositories (in /var, /etc, etc.), run with sudo:" |
| 480 | + echo -e " ${HIGHLIGHT}sudo ./update-all-repos.sh /var${NORMAL}" |
| 481 | + else |
| 482 | + echo -e "${HIGHLIGHT}→${NORMAL} Some repositories may have additional access restrictions" |
| 483 | + echo -e " Check ownership and permissions of failed repositories" |
| 484 | + fi |
| 485 | + echo "" |
| 486 | +fi |
| 487 | + |
332 | 488 | echo -e "${HIGHLIGHT}Test the hooks:${NORMAL}" |
333 | 489 | echo " cd /path/to/any/repo" |
334 | 490 | echo " echo 'const key = \"abc\"' > test.js" |
|
0 commit comments