Last active
November 27, 2025 13:46
-
-
Save adyp/8cf272ae7217fe24eecacb0fe3ad147e to your computer and use it in GitHub Desktop.
find-world-writable
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| usage() { | |
| cat << EOF | |
| Usage: $0 [OPTIONS] [TARGET_PATHS...] | |
| Scan for world-writable files/directories (ls -l style output). | |
| OPTIONS: | |
| --user, -u Run in non-root user mode: skip inaccessible paths | |
| --out FILE, -o FILE Write output to FILE (also prints to stdout) | |
| --quiet, -q Batch mode: suppress progress/summary | |
| --help Show this help | |
| EXAMPLES: | |
| $0 --out report.txt # Root scan to file | |
| $0 -u -q . -o ~/ww-report.txt # Quiet user scan of current dir | |
| sudo $0 /var /tmp --quiet # Quiet root scan | |
| EOF | |
| exit 0 | |
| } | |
| # Parse options | |
| targets=("/") | |
| user_mode=false | |
| output_file="" | |
| quiet=false | |
| while [[ $# -gt 0 ]]; do | |
| case $1 in | |
| --user|-u) user_mode=true; shift ;; | |
| --out=*) output_file="${1#*=}"; shift ;; | |
| -o) output_file="$2"; shift 2 ;; | |
| --quiet|-q) quiet=true; shift ;; | |
| --help) usage ;; | |
| --*) echo "Unknown option: $1" >&2; usage ;; | |
| *) break ;; | |
| esac | |
| done | |
| if [[ $# -gt 0 ]]; then | |
| targets=("$@") | |
| fi | |
| # Setup tee command | |
| if [[ -n "$output_file" ]]; then | |
| [[ -f "$output_file" ]] && echo "Warning: Overwriting $output_file" >&2 | |
| tee_cmd="tee \"$output_file\"" | |
| else | |
| tee_cmd="cat" | |
| fi | |
| # Progress (unless quiet) | |
| [[ $quiet == false ]] && echo "Scanning world-writable files/directories on: ${targets[*]}" | |
| [[ $quiet == false && $user_mode == true ]] && echo " (User mode: skipping inaccessible paths)" | |
| # Exclusions | |
| exclusions=( | |
| '/run/user/*' '/proc/*' '*/containerd/*' '*/kubelet/pods/*' | |
| '/sys/kernel/security/apparmor/*' '/snap/*' | |
| '/sys/fs/cgroup/memory/*' '/sys/fs/selinux/*' | |
| ) | |
| path_excl=() | |
| for excl in "${exclusions[@]}"; do | |
| path_excl+=(-not -path "$excl") | |
| done | |
| nfs_excl=() | |
| while IFS= read -r target; do | |
| nfs_excl+=(-not -path "${target}/*") | |
| done < <(findmnt -Dkerno fstype,target 2>/dev/null | awk '$1 ~ /^(nfs|proc|smb)$/ {print $2}' | grep -E '^/' || true) | |
| # Counters | |
| files_found=0 dirs_found=0 skipped=0 invalid_targets=0 | |
| # STREAMING: Process each target immediately | |
| for target in "${targets[@]}"; do | |
| [[ ! -e "$target" ]] && { | |
| [[ $quiet == false ]] && echo "Skipping non-existent target: $target" >&2 | |
| ((invalid_targets++)) | |
| continue | |
| } | |
| # Use find -ls for standard ls -l output + null termination for paths with spaces | |
| find "$target" \ | |
| "${path_excl[@]}" \ | |
| "${nfs_excl[@]}" \ | |
| \( -fstype nfs -o -fstype proc -o -fstype smb \) -prune \ | |
| -o \( -type f -o -type d \) -perm /002 \ | |
| ${user_mode:+-readable} \ | |
| -print0 2>/dev/null | \ | |
| while IFS= read -r -d '' path || [[ -n $path ]]; do | |
| # Skip inaccessible in user mode | |
| [[ $user_mode == true && ! -r "$path" && ! -x "$path" ]] && { | |
| ((skipped++)); continue | |
| } | |
| # Get ls -l style output for this path | |
| ls_output=$(find "$path" -maxdepth 0 -ls 2>/dev/null) | |
| # Stream to tee immediately | |
| printf '%s\n' "$ls_output" | eval $tee_cmd | |
| # Update counters | |
| if [[ -f "$path" ]]; then | |
| ((files_found++)) | |
| elif [[ -d "$path" ]]; then | |
| ((dirs_found++)) | |
| fi | |
| done | |
| done | |
| # Final summary (unless quiet) | |
| [[ $quiet == false ]] && { | |
| echo "" | |
| echo "Scan complete:" | |
| echo "- Files: $files_found" | |
| echo "- Dirs: $dirs_found" | |
| [[ $user_mode == true ]] && echo "- Skipped: $skipped" | |
| ((invalid_targets > 0)) && echo "- Invalid targets: $invalid_targets" | |
| [[ -n "$output_file" ]] && echo "- Output also saved to: $output_file" | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment