mirror of
https://github.com/tw93/Mole.git
synced 2026-02-04 15:04:42 +00:00
refactor: enhance pattern detection and symlink safety
- Expand sensitive data patterns (credentials, cloud configs, media folders) - Add symlink target validation in path deletion checks - Remove shared Gradle cache from Android Studio cleanup
This commit is contained in:
@@ -39,6 +39,25 @@ validate_path_for_deletion() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Check symlink target if path is a symbolic link
|
||||||
|
if [[ -L "$path" ]]; then
|
||||||
|
local link_target
|
||||||
|
link_target=$(readlink "$path" 2>/dev/null) || {
|
||||||
|
log_error "Cannot read symlink: $path"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# If symlink points to absolute path, validate target
|
||||||
|
if [[ "$link_target" == /* ]]; then
|
||||||
|
case "$link_target" in
|
||||||
|
/System/* | /usr/bin/* | /usr/lib/* | /bin/* | /sbin/* | /private/etc/*)
|
||||||
|
log_error "Symlink points to protected system path: $path -> $link_target"
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# Check path is absolute
|
# Check path is absolute
|
||||||
if [[ "$path" != /* ]]; then
|
if [[ "$path" != /* ]]; then
|
||||||
log_error "Path validation failed: path must be absolute: $path"
|
log_error "Path validation failed: path must be absolute: $path"
|
||||||
@@ -61,47 +80,47 @@ validate_path_for_deletion() {
|
|||||||
|
|
||||||
# Allow deletion of coresymbolicationd cache (safe system cache that can be rebuilt)
|
# Allow deletion of coresymbolicationd cache (safe system cache that can be rebuilt)
|
||||||
case "$path" in
|
case "$path" in
|
||||||
/System/Library/Caches/com.apple.coresymbolicationd/data | /System/Library/Caches/com.apple.coresymbolicationd/data/*)
|
/System/Library/Caches/com.apple.coresymbolicationd/data | /System/Library/Caches/com.apple.coresymbolicationd/data/*)
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# Allow known safe paths under /private
|
# Allow known safe paths under /private
|
||||||
case "$path" in
|
case "$path" in
|
||||||
/private/tmp | /private/tmp/* | \
|
/private/tmp | /private/tmp/* | \
|
||||||
/private/var/tmp | /private/var/tmp/* | \
|
/private/var/tmp | /private/var/tmp/* | \
|
||||||
/private/var/log | /private/var/log/* | \
|
/private/var/log | /private/var/log/* | \
|
||||||
/private/var/folders | /private/var/folders/* | \
|
/private/var/folders | /private/var/folders/* | \
|
||||||
/private/var/db/diagnostics | /private/var/db/diagnostics/* | \
|
/private/var/db/diagnostics | /private/var/db/diagnostics/* | \
|
||||||
/private/var/db/DiagnosticPipeline | /private/var/db/DiagnosticPipeline/* | \
|
/private/var/db/DiagnosticPipeline | /private/var/db/DiagnosticPipeline/* | \
|
||||||
/private/var/db/powerlog | /private/var/db/powerlog/* | \
|
/private/var/db/powerlog | /private/var/db/powerlog/* | \
|
||||||
/private/var/db/reportmemoryexception | /private/var/db/reportmemoryexception/*)
|
/private/var/db/reportmemoryexception | /private/var/db/reportmemoryexception/*)
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# Check path isn't critical system directory
|
# Check path isn't critical system directory
|
||||||
case "$path" in
|
case "$path" in
|
||||||
/ | /bin | /bin/* | /sbin | /sbin/* | /usr | /usr/bin | /usr/bin/* | /usr/sbin | /usr/sbin/* | /usr/lib | /usr/lib/* | /System | /System/* | /Library/Extensions)
|
/ | /bin | /bin/* | /sbin | /sbin/* | /usr | /usr/bin | /usr/bin/* | /usr/sbin | /usr/sbin/* | /usr/lib | /usr/lib/* | /System | /System/* | /Library/Extensions)
|
||||||
log_error "Path validation failed: critical system directory: $path"
|
log_error "Path validation failed: critical system directory: $path"
|
||||||
return 1
|
return 1
|
||||||
;;
|
;;
|
||||||
/private)
|
/private)
|
||||||
log_error "Path validation failed: critical system directory: $path"
|
log_error "Path validation failed: critical system directory: $path"
|
||||||
return 1
|
return 1
|
||||||
;;
|
;;
|
||||||
/etc | /etc/* | /private/etc | /private/etc/*)
|
/etc | /etc/* | /private/etc | /private/etc/*)
|
||||||
log_error "Path validation failed: /etc contains critical system files: $path"
|
log_error "Path validation failed: /etc contains critical system files: $path"
|
||||||
return 1
|
return 1
|
||||||
;;
|
;;
|
||||||
/var | /var/db | /var/db/* | /private/var | /private/var/db | /private/var/db/*)
|
/var | /var/db | /var/db/* | /private/var | /private/var/db | /private/var/db/*)
|
||||||
log_error "Path validation failed: /var/db contains system databases: $path"
|
log_error "Path validation failed: /var/db contains system databases: $path"
|
||||||
return 1
|
return 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# Check if path is protected (keychains, system settings, etc)
|
# Check if path is protected (keychains, system settings, etc)
|
||||||
if declare -f should_protect_path > /dev/null 2>&1; then
|
if declare -f should_protect_path >/dev/null 2>&1; then
|
||||||
if should_protect_path "$path"; then
|
if should_protect_path "$path"; then
|
||||||
if [[ "${MO_DEBUG:-0}" == "1" ]]; then
|
if [[ "${MO_DEBUG:-0}" == "1" ]]; then
|
||||||
log_warning "Path validation: protected path skipped: $path"
|
log_warning "Path validation: protected path skipped: $path"
|
||||||
@@ -144,16 +163,16 @@ safe_remove() {
|
|||||||
|
|
||||||
if [[ -e "$path" ]]; then
|
if [[ -e "$path" ]]; then
|
||||||
local size_kb
|
local size_kb
|
||||||
size_kb=$(get_path_size_kb "$path" 2> /dev/null || echo "0")
|
size_kb=$(get_path_size_kb "$path" 2>/dev/null || echo "0")
|
||||||
if [[ "$size_kb" -gt 0 ]]; then
|
if [[ "$size_kb" -gt 0 ]]; then
|
||||||
file_size=$(bytes_to_human "$((size_kb * 1024))")
|
file_size=$(bytes_to_human "$((size_kb * 1024))")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -f "$path" || -d "$path" ]] && ! [[ -L "$path" ]]; then
|
if [[ -f "$path" || -d "$path" ]] && ! [[ -L "$path" ]]; then
|
||||||
local mod_time
|
local mod_time
|
||||||
mod_time=$(stat -f%m "$path" 2> /dev/null || echo "0")
|
mod_time=$(stat -f%m "$path" 2>/dev/null || echo "0")
|
||||||
local now
|
local now
|
||||||
now=$(date +%s 2> /dev/null || echo "0")
|
now=$(date +%s 2>/dev/null || echo "0")
|
||||||
if [[ "$mod_time" -gt 0 && "$now" -gt 0 ]]; then
|
if [[ "$mod_time" -gt 0 && "$now" -gt 0 ]]; then
|
||||||
file_age=$(((now - mod_time) / 86400))
|
file_age=$(((now - mod_time) / 86400))
|
||||||
fi
|
fi
|
||||||
@@ -221,18 +240,18 @@ safe_sudo_remove() {
|
|||||||
local file_size=""
|
local file_size=""
|
||||||
local file_age=""
|
local file_age=""
|
||||||
|
|
||||||
if sudo test -e "$path" 2> /dev/null; then
|
if sudo test -e "$path" 2>/dev/null; then
|
||||||
local size_kb
|
local size_kb
|
||||||
size_kb=$(sudo du -sk "$path" 2> /dev/null | awk '{print $1}' || echo "0")
|
size_kb=$(sudo du -sk "$path" 2>/dev/null | awk '{print $1}' || echo "0")
|
||||||
if [[ "$size_kb" -gt 0 ]]; then
|
if [[ "$size_kb" -gt 0 ]]; then
|
||||||
file_size=$(bytes_to_human "$((size_kb * 1024))")
|
file_size=$(bytes_to_human "$((size_kb * 1024))")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if sudo test -f "$path" 2> /dev/null || sudo test -d "$path" 2> /dev/null; then
|
if sudo test -f "$path" 2>/dev/null || sudo test -d "$path" 2>/dev/null; then
|
||||||
local mod_time
|
local mod_time
|
||||||
mod_time=$(sudo stat -f%m "$path" 2> /dev/null || echo "0")
|
mod_time=$(sudo stat -f%m "$path" 2>/dev/null || echo "0")
|
||||||
local now
|
local now
|
||||||
now=$(date +%s 2> /dev/null || echo "0")
|
now=$(date +%s 2>/dev/null || echo "0")
|
||||||
if [[ "$mod_time" -gt 0 && "$now" -gt 0 ]]; then
|
if [[ "$mod_time" -gt 0 && "$now" -gt 0 ]]; then
|
||||||
file_age=$(((now - mod_time) / 86400))
|
file_age=$(((now - mod_time) / 86400))
|
||||||
fi
|
fi
|
||||||
@@ -249,7 +268,7 @@ safe_sudo_remove() {
|
|||||||
debug_log "Removing (sudo): $path"
|
debug_log "Removing (sudo): $path"
|
||||||
|
|
||||||
# Perform the deletion
|
# Perform the deletion
|
||||||
if sudo rm -rf "$path" 2> /dev/null; then # SAFE: safe_sudo_remove implementation
|
if sudo rm -rf "$path" 2>/dev/null; then # SAFE: safe_sudo_remove implementation
|
||||||
return 0
|
return 0
|
||||||
else
|
else
|
||||||
log_error "Failed to remove (sudo): $path"
|
log_error "Failed to remove (sudo): $path"
|
||||||
@@ -298,7 +317,7 @@ safe_find_delete() {
|
|||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
safe_remove "$match" true || true
|
safe_remove "$match" true || true
|
||||||
done < <(command find "$base_dir" "${find_args[@]}" -print0 2> /dev/null || true)
|
done < <(command find "$base_dir" "${find_args[@]}" -print0 2>/dev/null || true)
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
@@ -311,12 +330,12 @@ safe_sudo_find_delete() {
|
|||||||
local type_filter="${4:-f}"
|
local type_filter="${4:-f}"
|
||||||
|
|
||||||
# Validate base directory (use sudo for permission-restricted dirs)
|
# Validate base directory (use sudo for permission-restricted dirs)
|
||||||
if ! sudo test -d "$base_dir" 2> /dev/null; then
|
if ! sudo test -d "$base_dir" 2>/dev/null; then
|
||||||
debug_log "Directory does not exist (skipping): $base_dir"
|
debug_log "Directory does not exist (skipping): $base_dir"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if sudo test -L "$base_dir" 2> /dev/null; then
|
if sudo test -L "$base_dir" 2>/dev/null; then
|
||||||
log_error "Refusing to search symlinked directory: $base_dir"
|
log_error "Refusing to search symlinked directory: $base_dir"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
@@ -340,7 +359,7 @@ safe_sudo_find_delete() {
|
|||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
safe_sudo_remove "$match" || true
|
safe_sudo_remove "$match" || true
|
||||||
done < <(sudo find "$base_dir" "${find_args[@]}" -print0 2> /dev/null || true)
|
done < <(sudo find "$base_dir" "${find_args[@]}" -print0 2>/dev/null || true)
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
@@ -360,7 +379,7 @@ get_path_size_kb() {
|
|||||||
# Use || echo 0 to ensure failure in du (e.g. permission error) doesn't exit script under set -e
|
# Use || echo 0 to ensure failure in du (e.g. permission error) doesn't exit script under set -e
|
||||||
# Pipefail would normally cause the pipeline to fail if du fails, but || handle catches it.
|
# Pipefail would normally cause the pipeline to fail if du fails, but || handle catches it.
|
||||||
local size
|
local size
|
||||||
size=$(command du -sk "$path" 2> /dev/null | awk 'NR==1 {print $1; exit}' || true)
|
size=$(command du -sk "$path" 2>/dev/null | awk 'NR==1 {print $1; exit}' || true)
|
||||||
|
|
||||||
# Ensure size is a valid number (fix for non-numeric du output)
|
# Ensure size is a valid number (fix for non-numeric du output)
|
||||||
if [[ "$size" =~ ^[0-9]+$ ]]; then
|
if [[ "$size" =~ ^[0-9]+$ ]]; then
|
||||||
@@ -381,7 +400,7 @@ calculate_total_size() {
|
|||||||
size_kb=$(get_path_size_kb "$file")
|
size_kb=$(get_path_size_kb "$file")
|
||||||
((total_kb += size_kb))
|
((total_kb += size_kb))
|
||||||
fi
|
fi
|
||||||
done <<< "$files"
|
done <<<"$files"
|
||||||
|
|
||||||
echo "$total_kb"
|
echo "$total_kb"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,32 @@ SENSITIVE_DATA_REGEX=$(
|
|||||||
echo "${SENSITIVE_DATA_PATTERNS[*]}"
|
echo "${SENSITIVE_DATA_PATTERNS[*]}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# High-performance sensitive data detection (pure Bash, no subprocess)
|
||||||
|
# Faster than grep for batch operations, especially when processing many apps
|
||||||
|
has_sensitive_data() {
|
||||||
|
local files="$1"
|
||||||
|
[[ -z "$files" ]] && return 1
|
||||||
|
|
||||||
|
while IFS= read -r file; do
|
||||||
|
[[ -z "$file" ]] && continue
|
||||||
|
|
||||||
|
# Use Bash native pattern matching (faster than spawning grep)
|
||||||
|
case "$file" in
|
||||||
|
*/.warp* | */.config/* | */themes/* | */settings/* | */User\ Data/* | \
|
||||||
|
*/.ssh/* | */.gnupg/* | */Documents/* | */Preferences/*.plist | \
|
||||||
|
*/Desktop/* | */Downloads/* | */Movies/* | */Music/* | */Pictures/* | \
|
||||||
|
*/.password* | */.token* | */.auth* | */keychain* | \
|
||||||
|
*/Passwords/* | */Accounts/* | */Cookies/* | \
|
||||||
|
*/.aws/* | */.docker/config.json | */.kube/* | \
|
||||||
|
*/credentials/* | */secrets/*)
|
||||||
|
return 0 # Found sensitive data
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done <<<"$files"
|
||||||
|
|
||||||
|
return 1 # Not found
|
||||||
|
}
|
||||||
|
|
||||||
# Decode and validate base64 file list (safe for set -e).
|
# Decode and validate base64 file list (safe for set -e).
|
||||||
decode_file_list() {
|
decode_file_list() {
|
||||||
local encoded="$1"
|
local encoded="$1"
|
||||||
@@ -37,8 +63,8 @@ decode_file_list() {
|
|||||||
local decoded
|
local decoded
|
||||||
|
|
||||||
# macOS uses -D, GNU uses -d. Always return 0 for set -e safety.
|
# macOS uses -D, GNU uses -d. Always return 0 for set -e safety.
|
||||||
if ! decoded=$(printf '%s' "$encoded" | base64 -D 2> /dev/null); then
|
if ! decoded=$(printf '%s' "$encoded" | base64 -D 2>/dev/null); then
|
||||||
if ! decoded=$(printf '%s' "$encoded" | base64 -d 2> /dev/null); then
|
if ! decoded=$(printf '%s' "$encoded" | base64 -d 2>/dev/null); then
|
||||||
log_error "Failed to decode file list for $app_name" >&2
|
log_error "Failed to decode file list for $app_name" >&2
|
||||||
echo ""
|
echo ""
|
||||||
return 0 # Return success with empty string
|
return 0 # Return success with empty string
|
||||||
@@ -57,7 +83,7 @@ decode_file_list() {
|
|||||||
echo ""
|
echo ""
|
||||||
return 0 # Return success with empty string
|
return 0 # Return success with empty string
|
||||||
fi
|
fi
|
||||||
done <<< "$decoded"
|
done <<<"$decoded"
|
||||||
|
|
||||||
echo "$decoded"
|
echo "$decoded"
|
||||||
return 0
|
return 0
|
||||||
@@ -73,20 +99,20 @@ stop_launch_services() {
|
|||||||
|
|
||||||
if [[ -d ~/Library/LaunchAgents ]]; then
|
if [[ -d ~/Library/LaunchAgents ]]; then
|
||||||
while IFS= read -r -d '' plist; do
|
while IFS= read -r -d '' plist; do
|
||||||
launchctl unload "$plist" 2> /dev/null || true
|
launchctl unload "$plist" 2>/dev/null || true
|
||||||
done < <(find ~/Library/LaunchAgents -maxdepth 1 -name "${bundle_id}*.plist" -print0 2> /dev/null)
|
done < <(find ~/Library/LaunchAgents -maxdepth 1 -name "${bundle_id}*.plist" -print0 2>/dev/null)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$has_system_files" == "true" ]]; then
|
if [[ "$has_system_files" == "true" ]]; then
|
||||||
if [[ -d /Library/LaunchAgents ]]; then
|
if [[ -d /Library/LaunchAgents ]]; then
|
||||||
while IFS= read -r -d '' plist; do
|
while IFS= read -r -d '' plist; do
|
||||||
sudo launchctl unload "$plist" 2> /dev/null || true
|
sudo launchctl unload "$plist" 2>/dev/null || true
|
||||||
done < <(find /Library/LaunchAgents -maxdepth 1 -name "${bundle_id}*.plist" -print0 2> /dev/null)
|
done < <(find /Library/LaunchAgents -maxdepth 1 -name "${bundle_id}*.plist" -print0 2>/dev/null)
|
||||||
fi
|
fi
|
||||||
if [[ -d /Library/LaunchDaemons ]]; then
|
if [[ -d /Library/LaunchDaemons ]]; then
|
||||||
while IFS= read -r -d '' plist; do
|
while IFS= read -r -d '' plist; do
|
||||||
sudo launchctl unload "$plist" 2> /dev/null || true
|
sudo launchctl unload "$plist" 2>/dev/null || true
|
||||||
done < <(find /Library/LaunchDaemons -maxdepth 1 -name "${bundle_id}*.plist" -print0 2> /dev/null)
|
done < <(find /Library/LaunchDaemons -maxdepth 1 -name "${bundle_id}*.plist" -print0 2>/dev/null)
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -108,7 +134,7 @@ remove_login_item() {
|
|||||||
local escaped_name="${clean_name//\\/\\\\}"
|
local escaped_name="${clean_name//\\/\\\\}"
|
||||||
escaped_name="${escaped_name//\"/\\\"}"
|
escaped_name="${escaped_name//\"/\\\"}"
|
||||||
|
|
||||||
osascript <<- EOF > /dev/null 2>&1 || true
|
osascript <<-EOF >/dev/null 2>&1 || true
|
||||||
tell application "System Events"
|
tell application "System Events"
|
||||||
try
|
try
|
||||||
set itemCount to count of login items
|
set itemCount to count of login items
|
||||||
@@ -142,9 +168,9 @@ remove_file_list() {
|
|||||||
|
|
||||||
if [[ -L "$file" ]]; then
|
if [[ -L "$file" ]]; then
|
||||||
if [[ "$use_sudo" == "true" ]]; then
|
if [[ "$use_sudo" == "true" ]]; then
|
||||||
sudo rm "$file" 2> /dev/null && ((++count)) || true
|
sudo rm "$file" 2>/dev/null && ((++count)) || true
|
||||||
else
|
else
|
||||||
rm "$file" 2> /dev/null && ((++count)) || true
|
rm "$file" 2>/dev/null && ((++count)) || true
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
if [[ "$use_sudo" == "true" ]]; then
|
if [[ "$use_sudo" == "true" ]]; then
|
||||||
@@ -153,7 +179,7 @@ remove_file_list() {
|
|||||||
safe_remove "$file" true && ((++count)) || true
|
safe_remove "$file" true && ((++count)) || true
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
done <<< "$file_list"
|
done <<<"$file_list"
|
||||||
|
|
||||||
echo "$count"
|
echo "$count"
|
||||||
}
|
}
|
||||||
@@ -177,15 +203,15 @@ batch_uninstall_applications() {
|
|||||||
if [[ -t 1 ]]; then start_inline_spinner "Scanning files..."; fi
|
if [[ -t 1 ]]; then start_inline_spinner "Scanning files..."; fi
|
||||||
for selected_app in "${selected_apps[@]}"; do
|
for selected_app in "${selected_apps[@]}"; do
|
||||||
[[ -z "$selected_app" ]] && continue
|
[[ -z "$selected_app" ]] && continue
|
||||||
IFS='|' read -r _ app_path app_name bundle_id _ _ <<< "$selected_app"
|
IFS='|' read -r _ app_path app_name bundle_id _ _ <<<"$selected_app"
|
||||||
|
|
||||||
# Check running app by bundle executable if available.
|
# Check running app by bundle executable if available.
|
||||||
local exec_name=""
|
local exec_name=""
|
||||||
if [[ -e "$app_path/Contents/Info.plist" ]]; then
|
if [[ -e "$app_path/Contents/Info.plist" ]]; then
|
||||||
exec_name=$(defaults read "$app_path/Contents/Info.plist" CFBundleExecutable 2> /dev/null || echo "")
|
exec_name=$(defaults read "$app_path/Contents/Info.plist" CFBundleExecutable 2>/dev/null || echo "")
|
||||||
fi
|
fi
|
||||||
local check_pattern="${exec_name:-$app_name}"
|
local check_pattern="${exec_name:-$app_name}"
|
||||||
if pgrep -x "$check_pattern" > /dev/null 2>&1; then
|
if pgrep -x "$check_pattern" >/dev/null 2>&1; then
|
||||||
running_apps+=("$app_name")
|
running_apps+=("$app_name")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -230,7 +256,7 @@ batch_uninstall_applications() {
|
|||||||
|
|
||||||
# Check for sensitive user data once.
|
# Check for sensitive user data once.
|
||||||
local has_sensitive_data="false"
|
local has_sensitive_data="false"
|
||||||
if [[ -n "$related_files" ]] && echo "$related_files" | grep -qE "$SENSITIVE_DATA_REGEX"; then
|
if has_sensitive_data "$related_files"; then
|
||||||
has_sensitive_data="true"
|
has_sensitive_data="true"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -252,7 +278,7 @@ batch_uninstall_applications() {
|
|||||||
# Warn if user data is detected.
|
# Warn if user data is detected.
|
||||||
local has_user_data=false
|
local has_user_data=false
|
||||||
for detail in "${app_details[@]}"; do
|
for detail in "${app_details[@]}"; do
|
||||||
IFS='|' read -r _ _ _ _ _ _ has_sensitive_data <<< "$detail"
|
IFS='|' read -r _ _ _ _ _ _ has_sensitive_data <<<"$detail"
|
||||||
if [[ "$has_sensitive_data" == "true" ]]; then
|
if [[ "$has_sensitive_data" == "true" ]]; then
|
||||||
has_user_data=true
|
has_user_data=true
|
||||||
break
|
break
|
||||||
@@ -265,7 +291,7 @@ batch_uninstall_applications() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
for detail in "${app_details[@]}"; do
|
for detail in "${app_details[@]}"; do
|
||||||
IFS='|' read -r app_name app_path bundle_id total_kb encoded_files encoded_system_files has_sensitive_data needs_sudo_flag is_brew_cask cask_name <<< "$detail"
|
IFS='|' read -r app_name app_path bundle_id total_kb encoded_files encoded_system_files has_sensitive_data needs_sudo_flag is_brew_cask cask_name <<<"$detail"
|
||||||
local app_size_display=$(bytes_to_human "$((total_kb * 1024))")
|
local app_size_display=$(bytes_to_human "$((total_kb * 1024))")
|
||||||
|
|
||||||
local brew_tag=""
|
local brew_tag=""
|
||||||
@@ -288,7 +314,7 @@ batch_uninstall_applications() {
|
|||||||
fi
|
fi
|
||||||
((file_count++))
|
((file_count++))
|
||||||
fi
|
fi
|
||||||
done <<< "$related_files"
|
done <<<"$related_files"
|
||||||
|
|
||||||
# Show system files (limit to 5).
|
# Show system files (limit to 5).
|
||||||
local sys_file_count=0
|
local sys_file_count=0
|
||||||
@@ -299,7 +325,7 @@ batch_uninstall_applications() {
|
|||||||
fi
|
fi
|
||||||
((sys_file_count++))
|
((sys_file_count++))
|
||||||
fi
|
fi
|
||||||
done <<< "$system_files"
|
done <<<"$system_files"
|
||||||
|
|
||||||
local total_hidden=$((file_count > max_files ? file_count - max_files : 0))
|
local total_hidden=$((file_count > max_files ? file_count - max_files : 0))
|
||||||
((total_hidden += sys_file_count > max_files ? sys_file_count - max_files : 0))
|
((total_hidden += sys_file_count > max_files ? sys_file_count - max_files : 0))
|
||||||
@@ -325,24 +351,24 @@ batch_uninstall_applications() {
|
|||||||
IFS= read -r -s -n1 key || key=""
|
IFS= read -r -s -n1 key || key=""
|
||||||
drain_pending_input # Clean up any escape sequence remnants
|
drain_pending_input # Clean up any escape sequence remnants
|
||||||
case "$key" in
|
case "$key" in
|
||||||
$'\e' | q | Q)
|
$'\e' | q | Q)
|
||||||
echo ""
|
echo ""
|
||||||
echo ""
|
echo ""
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
"" | $'\n' | $'\r' | y | Y)
|
"" | $'\n' | $'\r' | y | Y)
|
||||||
echo "" # Move to next line
|
echo "" # Move to next line
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo ""
|
echo ""
|
||||||
echo ""
|
echo ""
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# Request sudo if needed.
|
# Request sudo if needed.
|
||||||
if [[ ${#sudo_apps[@]} -gt 0 ]]; then
|
if [[ ${#sudo_apps[@]} -gt 0 ]]; then
|
||||||
if ! sudo -n true 2> /dev/null; then
|
if ! sudo -n true 2>/dev/null; then
|
||||||
if ! request_sudo_access "Admin required for system apps: ${sudo_apps[*]}"; then
|
if ! request_sudo_access "Admin required for system apps: ${sudo_apps[*]}"; then
|
||||||
echo ""
|
echo ""
|
||||||
log_error "Admin access denied"
|
log_error "Admin access denied"
|
||||||
@@ -352,12 +378,12 @@ batch_uninstall_applications() {
|
|||||||
# Keep sudo alive during uninstall.
|
# Keep sudo alive during uninstall.
|
||||||
parent_pid=$$
|
parent_pid=$$
|
||||||
(while true; do
|
(while true; do
|
||||||
if ! kill -0 "$parent_pid" 2> /dev/null; then
|
if ! kill -0 "$parent_pid" 2>/dev/null; then
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
sudo -n true
|
sudo -n true
|
||||||
sleep 60
|
sleep 60
|
||||||
done 2> /dev/null) &
|
done 2>/dev/null) &
|
||||||
sudo_keepalive_pid=$!
|
sudo_keepalive_pid=$!
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -369,7 +395,7 @@ batch_uninstall_applications() {
|
|||||||
local current_index=0
|
local current_index=0
|
||||||
for detail in "${app_details[@]}"; do
|
for detail in "${app_details[@]}"; do
|
||||||
((current_index++))
|
((current_index++))
|
||||||
IFS='|' read -r app_name app_path bundle_id total_kb encoded_files encoded_system_files has_sensitive_data needs_sudo is_brew_cask cask_name <<< "$detail"
|
IFS='|' read -r app_name app_path bundle_id total_kb encoded_files encoded_system_files has_sensitive_data needs_sudo is_brew_cask cask_name <<<"$detail"
|
||||||
local related_files=$(decode_file_list "$encoded_files" "$app_name")
|
local related_files=$(decode_file_list "$encoded_files" "$app_name")
|
||||||
local system_files=$(decode_file_list "$encoded_system_files" "$app_name")
|
local system_files=$(decode_file_list "$encoded_system_files" "$app_name")
|
||||||
local reason=""
|
local reason=""
|
||||||
@@ -432,24 +458,24 @@ batch_uninstall_applications() {
|
|||||||
|
|
||||||
# Remove related files if app removal succeeded.
|
# Remove related files if app removal succeeded.
|
||||||
if [[ -z "$reason" ]]; then
|
if [[ -z "$reason" ]]; then
|
||||||
remove_file_list "$related_files" "false" > /dev/null
|
remove_file_list "$related_files" "false" >/dev/null
|
||||||
|
|
||||||
# If brew successfully uninstalled the cask, avoid deleting
|
# If brew successfully uninstalled the cask, avoid deleting
|
||||||
# system-level files Mole discovered. Brew manages its own
|
# system-level files Mole discovered. Brew manages its own
|
||||||
# receipts/symlinks and we don't want to fight it.
|
# receipts/symlinks and we don't want to fight it.
|
||||||
if [[ "$used_brew_successfully" != "true" ]]; then
|
if [[ "$used_brew_successfully" != "true" ]]; then
|
||||||
remove_file_list "$system_files" "true" > /dev/null
|
remove_file_list "$system_files" "true" >/dev/null
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Clean up macOS defaults (preference domains).
|
# Clean up macOS defaults (preference domains).
|
||||||
if [[ -n "$bundle_id" && "$bundle_id" != "unknown" ]]; then
|
if [[ -n "$bundle_id" && "$bundle_id" != "unknown" ]]; then
|
||||||
if defaults read "$bundle_id" &> /dev/null; then
|
if defaults read "$bundle_id" &>/dev/null; then
|
||||||
defaults delete "$bundle_id" 2> /dev/null || true
|
defaults delete "$bundle_id" 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ByHost preferences (machine-specific).
|
# ByHost preferences (machine-specific).
|
||||||
if [[ -d ~/Library/Preferences/ByHost ]]; then
|
if [[ -d ~/Library/Preferences/ByHost ]]; then
|
||||||
find ~/Library/Preferences/ByHost -maxdepth 1 -name "${bundle_id}.*.plist" -delete 2> /dev/null || true
|
find ~/Library/Preferences/ByHost -maxdepth 1 -name "${bundle_id}.*.plist" -delete 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -547,11 +573,11 @@ batch_uninstall_applications() {
|
|||||||
if [[ $failed_count -eq 1 ]]; then
|
if [[ $failed_count -eq 1 ]]; then
|
||||||
local first_reason=${failed_items[0]#*:}
|
local first_reason=${failed_items[0]#*:}
|
||||||
case "$first_reason" in
|
case "$first_reason" in
|
||||||
still*running*) reason_summary="is still running" ;;
|
still*running*) reason_summary="is still running" ;;
|
||||||
remove*failed*) reason_summary="could not be removed" ;;
|
remove*failed*) reason_summary="could not be removed" ;;
|
||||||
permission*denied*) reason_summary="permission denied" ;;
|
permission*denied*) reason_summary="permission denied" ;;
|
||||||
owned*by*) reason_summary="$first_reason (try with sudo)" ;;
|
owned*by*) reason_summary="$first_reason (try with sudo)" ;;
|
||||||
*) reason_summary="$first_reason" ;;
|
*) reason_summary="$first_reason" ;;
|
||||||
esac
|
esac
|
||||||
fi
|
fi
|
||||||
summary_details+=("Failed: ${RED}${failed_list}${NC} ${reason_summary}")
|
summary_details+=("Failed: ${RED}${failed_list}${NC} ${reason_summary}")
|
||||||
@@ -579,7 +605,7 @@ batch_uninstall_applications() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
local autoremove_output removed_count
|
local autoremove_output removed_count
|
||||||
autoremove_output=$(HOMEBREW_NO_ENV_HINTS=1 brew autoremove 2> /dev/null) || true
|
autoremove_output=$(HOMEBREW_NO_ENV_HINTS=1 brew autoremove 2>/dev/null) || true
|
||||||
removed_count=$(printf '%s\n' "$autoremove_output" | grep -c "^Uninstalling" || true)
|
removed_count=$(printf '%s\n' "$autoremove_output" | grep -c "^Uninstalling" || true)
|
||||||
removed_count=${removed_count:-0}
|
removed_count=${removed_count:-0}
|
||||||
|
|
||||||
@@ -597,7 +623,7 @@ batch_uninstall_applications() {
|
|||||||
if [[ $success_count -gt 0 ]]; then
|
if [[ $success_count -gt 0 ]]; then
|
||||||
local -a removed_paths=()
|
local -a removed_paths=()
|
||||||
for detail in "${app_details[@]}"; do
|
for detail in "${app_details[@]}"; do
|
||||||
IFS='|' read -r app_name app_path _ _ _ _ <<< "$detail"
|
IFS='|' read -r app_name app_path _ _ _ _ <<<"$detail"
|
||||||
for success_name in "${success_items[@]}"; do
|
for success_name in "${success_items[@]}"; do
|
||||||
if [[ "$success_name" == "$app_name" ]]; then
|
if [[ "$success_name" == "$app_name" ]]; then
|
||||||
removed_paths+=("$app_path")
|
removed_paths+=("$app_path")
|
||||||
@@ -606,21 +632,21 @@ batch_uninstall_applications() {
|
|||||||
done
|
done
|
||||||
done
|
done
|
||||||
if [[ ${#removed_paths[@]} -gt 0 ]]; then
|
if [[ ${#removed_paths[@]} -gt 0 ]]; then
|
||||||
remove_apps_from_dock "${removed_paths[@]}" 2> /dev/null || true
|
remove_apps_from_dock "${removed_paths[@]}" 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Clean up sudo keepalive if it was started.
|
# Clean up sudo keepalive if it was started.
|
||||||
if [[ -n "${sudo_keepalive_pid:-}" ]]; then
|
if [[ -n "${sudo_keepalive_pid:-}" ]]; then
|
||||||
kill "$sudo_keepalive_pid" 2> /dev/null || true
|
kill "$sudo_keepalive_pid" 2>/dev/null || true
|
||||||
wait "$sudo_keepalive_pid" 2> /dev/null || true
|
wait "$sudo_keepalive_pid" 2>/dev/null || true
|
||||||
sudo_keepalive_pid=""
|
sudo_keepalive_pid=""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Invalidate cache if any apps were successfully uninstalled.
|
# Invalidate cache if any apps were successfully uninstalled.
|
||||||
if [[ $success_count -gt 0 ]]; then
|
if [[ $success_count -gt 0 ]]; then
|
||||||
local cache_file="$HOME/.cache/mole/app_scan_cache"
|
local cache_file="$HOME/.cache/mole/app_scan_cache"
|
||||||
rm -f "$cache_file" 2> /dev/null || true
|
rm -f "$cache_file" 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
((total_size_cleaned += total_size_freed))
|
((total_size_cleaned += total_size_freed))
|
||||||
|
|||||||
Reference in New Issue
Block a user