mirror of
https://github.com/tw93/Mole.git
synced 2026-02-15 04:40:11 +00:00
security: harden BOM processing and LaunchAgents detection
- Add path traversal protection in BOM receipt parsing - Remove invalid ~/Library/LaunchDaemons path references - Strengthen LaunchAgents matching (min 5 chars, exclude com.apple.*) - Add 300s timeout to brew cask uninstall to prevent hangs Addresses security review findings from V1.21.0 audit.
This commit is contained in:
@@ -13,7 +13,7 @@ _MOLE_CORE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|||||||
[[ -z "${MOLE_BASE_LOADED:-}" ]] && source "$_MOLE_CORE_DIR/base.sh"
|
[[ -z "${MOLE_BASE_LOADED:-}" ]] && source "$_MOLE_CORE_DIR/base.sh"
|
||||||
|
|
||||||
# Declare WHITELIST_PATTERNS if not already set (used by is_path_whitelisted)
|
# Declare WHITELIST_PATTERNS if not already set (used by is_path_whitelisted)
|
||||||
if ! declare -p WHITELIST_PATTERNS &> /dev/null; then
|
if ! declare -p WHITELIST_PATTERNS &>/dev/null; then
|
||||||
declare -a WHITELIST_PATTERNS=()
|
declare -a WHITELIST_PATTERNS=()
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -439,12 +439,12 @@ is_critical_system_component() {
|
|||||||
lower=$(echo "$token" | LC_ALL=C tr '[:upper:]' '[:lower:]')
|
lower=$(echo "$token" | LC_ALL=C tr '[:upper:]' '[:lower:]')
|
||||||
|
|
||||||
case "$lower" in
|
case "$lower" in
|
||||||
*backgroundtaskmanagement* | *loginitems* | *systempreferences* | *systemsettings* | *settings* | *preferences* | *controlcenter* | *biometrickit* | *sfl* | *tcc*)
|
*backgroundtaskmanagement* | *loginitems* | *systempreferences* | *systemsettings* | *settings* | *preferences* | *controlcenter* | *biometrickit* | *sfl* | *tcc*)
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
return 1
|
return 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -522,25 +522,25 @@ should_protect_path() {
|
|||||||
# 2. Protect caches critical for system UI rendering
|
# 2. Protect caches critical for system UI rendering
|
||||||
# These caches are essential for modern macOS (Sonoma/Sequoia) system UI rendering
|
# These caches are essential for modern macOS (Sonoma/Sequoia) system UI rendering
|
||||||
case "$path" in
|
case "$path" in
|
||||||
# System Settings and Control Center caches (CRITICAL - prevents blank panel bug)
|
# System Settings and Control Center caches (CRITICAL - prevents blank panel bug)
|
||||||
*com.apple.systempreferences.cache* | *com.apple.Settings.cache* | *com.apple.controlcenter.cache*)
|
*com.apple.systempreferences.cache* | *com.apple.Settings.cache* | *com.apple.controlcenter.cache*)
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
# Finder and Dock (system essential)
|
# Finder and Dock (system essential)
|
||||||
*com.apple.finder.cache* | *com.apple.dock.cache*)
|
*com.apple.finder.cache* | *com.apple.dock.cache*)
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
# System XPC services and sandboxed containers
|
# System XPC services and sandboxed containers
|
||||||
*/Library/Containers/com.apple.Settings* | */Library/Containers/com.apple.SystemSettings* | */Library/Containers/com.apple.controlcenter*)
|
*/Library/Containers/com.apple.Settings* | */Library/Containers/com.apple.SystemSettings* | */Library/Containers/com.apple.controlcenter*)
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
*/Library/Group\ Containers/com.apple.systempreferences* | */Library/Group\ Containers/com.apple.Settings*)
|
*/Library/Group\ Containers/com.apple.systempreferences* | */Library/Group\ Containers/com.apple.Settings*)
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
# Shared file lists for System Settings (macOS Sequoia) - Issue #136
|
# Shared file lists for System Settings (macOS Sequoia) - Issue #136
|
||||||
*/com.apple.sharedfilelist/*com.apple.Settings* | */com.apple.sharedfilelist/*com.apple.SystemSettings* | */com.apple.sharedfilelist/*systempreferences*)
|
*/com.apple.sharedfilelist/*com.apple.Settings* | */com.apple.sharedfilelist/*com.apple.SystemSettings* | */com.apple.sharedfilelist/*systempreferences*)
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# 3. Extract bundle ID from sandbox paths
|
# 3. Extract bundle ID from sandbox paths
|
||||||
@@ -555,24 +555,24 @@ should_protect_path() {
|
|||||||
|
|
||||||
# 4. Check for specific hardcoded critical patterns
|
# 4. Check for specific hardcoded critical patterns
|
||||||
case "$path" in
|
case "$path" in
|
||||||
*com.apple.Settings* | *com.apple.SystemSettings* | *com.apple.controlcenter* | *com.apple.finder* | *com.apple.dock*)
|
*com.apple.Settings* | *com.apple.SystemSettings* | *com.apple.controlcenter* | *com.apple.finder* | *com.apple.dock*)
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# 5. Protect critical preference files and user data
|
# 5. Protect critical preference files and user data
|
||||||
case "$path" in
|
case "$path" in
|
||||||
*/Library/Preferences/com.apple.dock.plist | */Library/Preferences/com.apple.finder.plist)
|
*/Library/Preferences/com.apple.dock.plist | */Library/Preferences/com.apple.finder.plist)
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
# Bluetooth and WiFi configurations
|
# Bluetooth and WiFi configurations
|
||||||
*/ByHost/com.apple.bluetooth.* | */ByHost/com.apple.wifi.*)
|
*/ByHost/com.apple.bluetooth.* | */ByHost/com.apple.wifi.*)
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
# iCloud Drive - protect user's cloud synced data
|
# iCloud Drive - protect user's cloud synced data
|
||||||
*/Library/Mobile\ Documents* | */Mobile\ Documents*)
|
*/Library/Mobile\ Documents* | */Mobile\ Documents*)
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# 6. Match full path against protected patterns
|
# 6. Match full path against protected patterns
|
||||||
@@ -611,9 +611,9 @@ is_path_whitelisted() {
|
|||||||
local check_pattern="${pattern%/}"
|
local check_pattern="${pattern%/}"
|
||||||
local has_glob="false"
|
local has_glob="false"
|
||||||
case "$check_pattern" in
|
case "$check_pattern" in
|
||||||
*\** | *\?* | *\[*)
|
*\** | *\?* | *\[*)
|
||||||
has_glob="true"
|
has_glob="true"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# Check for exact match or glob pattern match
|
# Check for exact match or glob pattern match
|
||||||
@@ -643,6 +643,14 @@ is_path_whitelisted() {
|
|||||||
find_app_files() {
|
find_app_files() {
|
||||||
local bundle_id="$1"
|
local bundle_id="$1"
|
||||||
local app_name="$2"
|
local app_name="$2"
|
||||||
|
|
||||||
|
# Early validation: require at least one valid identifier
|
||||||
|
# Skip scanning if both bundle_id and app_name are invalid
|
||||||
|
if [[ -z "$bundle_id" || "$bundle_id" == "unknown" ]] &&
|
||||||
|
[[ -z "$app_name" || ${#app_name} -lt 2 ]]; then
|
||||||
|
return 0 # Silent return to avoid invalid scanning
|
||||||
|
fi
|
||||||
|
|
||||||
local -a files_to_clean=()
|
local -a files_to_clean=()
|
||||||
|
|
||||||
# Normalize app name for matching
|
# Normalize app name for matching
|
||||||
@@ -665,7 +673,6 @@ find_app_files() {
|
|||||||
"$HOME/Library/HTTPStorages/$bundle_id"
|
"$HOME/Library/HTTPStorages/$bundle_id"
|
||||||
"$HOME/Library/Cookies/$bundle_id.binarycookies"
|
"$HOME/Library/Cookies/$bundle_id.binarycookies"
|
||||||
"$HOME/Library/LaunchAgents/$bundle_id.plist"
|
"$HOME/Library/LaunchAgents/$bundle_id.plist"
|
||||||
"$HOME/Library/LaunchDaemons/$bundle_id.plist"
|
|
||||||
"$HOME/Library/Application Scripts/$bundle_id"
|
"$HOME/Library/Application Scripts/$bundle_id"
|
||||||
"$HOME/Library/Services/$app_name.workflow"
|
"$HOME/Library/Services/$app_name.workflow"
|
||||||
"$HOME/Library/QuickLook/$app_name.qlgenerator"
|
"$HOME/Library/QuickLook/$app_name.qlgenerator"
|
||||||
@@ -709,17 +716,17 @@ find_app_files() {
|
|||||||
# Safety check: Skip if path ends with a common directory name (indicates empty app_name/bundle_id)
|
# Safety check: Skip if path ends with a common directory name (indicates empty app_name/bundle_id)
|
||||||
# This prevents deletion of entire Library subdirectories when bundle_id is empty
|
# This prevents deletion of entire Library subdirectories when bundle_id is empty
|
||||||
case "$expanded_path" in
|
case "$expanded_path" in
|
||||||
*/Library/Application\ Support | */Library/Application\ Support/ | \
|
*/Library/Application\ Support | */Library/Application\ Support/ | \
|
||||||
*/Library/Caches | */Library/Caches/ | \
|
*/Library/Caches | */Library/Caches/ | \
|
||||||
*/Library/Logs | */Library/Logs/ | \
|
*/Library/Logs | */Library/Logs/ | \
|
||||||
*/Library/Containers | */Library/Containers/ | \
|
*/Library/Containers | */Library/Containers/ | \
|
||||||
*/Library/WebKit | */Library/WebKit/ | \
|
*/Library/WebKit | */Library/WebKit/ | \
|
||||||
*/Library/HTTPStorages | */Library/HTTPStorages/ | \
|
*/Library/HTTPStorages | */Library/HTTPStorages/ | \
|
||||||
*/Library/Application\ Scripts | */Library/Application\ Scripts/ | \
|
*/Library/Application\ Scripts | */Library/Application\ Scripts/ | \
|
||||||
*/Library/Autosave\ Information | */Library/Autosave\ Information/ | \
|
*/Library/Autosave\ Information | */Library/Autosave\ Information/ | \
|
||||||
*/Library/Group\ Containers | */Library/Group\ Containers/)
|
*/Library/Group\ Containers | */Library/Group\ Containers/)
|
||||||
continue
|
continue
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
files_to_clean+=("$expanded_path")
|
files_to_clean+=("$expanded_path")
|
||||||
@@ -730,28 +737,26 @@ find_app_files() {
|
|||||||
[[ -f ~/Library/Preferences/"$bundle_id".plist ]] && files_to_clean+=("$HOME/Library/Preferences/$bundle_id.plist")
|
[[ -f ~/Library/Preferences/"$bundle_id".plist ]] && files_to_clean+=("$HOME/Library/Preferences/$bundle_id.plist")
|
||||||
[[ -d ~/Library/Preferences/ByHost ]] && while IFS= read -r -d '' pref; do
|
[[ -d ~/Library/Preferences/ByHost ]] && while IFS= read -r -d '' pref; do
|
||||||
files_to_clean+=("$pref")
|
files_to_clean+=("$pref")
|
||||||
done < <(command find ~/Library/Preferences/ByHost -maxdepth 1 \( -name "$bundle_id*.plist" \) -print0 2> /dev/null)
|
done < <(command find ~/Library/Preferences/ByHost -maxdepth 1 \( -name "$bundle_id*.plist" \) -print0 2>/dev/null)
|
||||||
|
|
||||||
# Group Containers (special handling)
|
# Group Containers (special handling)
|
||||||
if [[ -d ~/Library/Group\ Containers ]]; then
|
if [[ -d ~/Library/Group\ Containers ]]; then
|
||||||
while IFS= read -r -d '' container; do
|
while IFS= read -r -d '' container; do
|
||||||
files_to_clean+=("$container")
|
files_to_clean+=("$container")
|
||||||
done < <(command find ~/Library/Group\ Containers -maxdepth 1 \( -name "*$bundle_id*" \) -print0 2> /dev/null)
|
done < <(command find ~/Library/Group\ Containers -maxdepth 1 \( -name "*$bundle_id*" \) -print0 2>/dev/null)
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Launch Agents and Daemons by name (special handling)
|
# Launch Agents by name (special handling)
|
||||||
if [[ ${#app_name} -gt 3 ]]; then
|
# Note: LaunchDaemons are system-level and handled in find_app_system_files()
|
||||||
if [[ -d ~/Library/LaunchAgents ]]; then
|
if [[ ${#app_name} -ge 5 ]] && [[ -d ~/Library/LaunchAgents ]]; then
|
||||||
while IFS= read -r -d '' plist; do
|
while IFS= read -r -d '' plist; do
|
||||||
files_to_clean+=("$plist")
|
local plist_name=$(basename "$plist")
|
||||||
done < <(command find ~/Library/LaunchAgents -maxdepth 1 \( -name "*$app_name*.plist" \) -print0 2> /dev/null)
|
if [[ "$plist_name" =~ ^com\.apple\. ]]; then
|
||||||
fi
|
continue
|
||||||
if [[ -d ~/Library/LaunchDaemons ]]; then
|
fi
|
||||||
while IFS= read -r -d '' plist; do
|
files_to_clean+=("$plist")
|
||||||
files_to_clean+=("$plist")
|
done < <(command find ~/Library/LaunchAgents -maxdepth 1 -name "*$app_name*.plist" -print0 2>/dev/null)
|
||||||
done < <(command find ~/Library/LaunchDaemons -maxdepth 1 \( -name "*$app_name*.plist" \) -print0 2> /dev/null)
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Handle specialized toolchains and development environments
|
# Handle specialized toolchains and development environments
|
||||||
@@ -764,10 +769,10 @@ find_app_files() {
|
|||||||
|
|
||||||
# 2. Android Studio (Google)
|
# 2. Android Studio (Google)
|
||||||
if [[ "$app_name" =~ Android.*Studio|android.*studio ]] || [[ "$bundle_id" =~ google.*android.*studio|jetbrains.*android ]]; then
|
if [[ "$app_name" =~ Android.*Studio|android.*studio ]] || [[ "$bundle_id" =~ google.*android.*studio|jetbrains.*android ]]; then
|
||||||
for d in ~/AndroidStudioProjects ~/Library/Android ~/.android ~/.gradle; do
|
for d in ~/AndroidStudioProjects ~/Library/Android ~/.android; do
|
||||||
[[ -d "$d" ]] && files_to_clean+=("$d")
|
[[ -d "$d" ]] && files_to_clean+=("$d")
|
||||||
done
|
done
|
||||||
[[ -d ~/Library/Application\ Support/Google ]] && while IFS= read -r -d '' d; do files_to_clean+=("$d"); done < <(command find ~/Library/Application\ Support/Google -maxdepth 1 -name "AndroidStudio*" -print0 2> /dev/null)
|
[[ -d ~/Library/Application\ Support/Google ]] && while IFS= read -r -d '' d; do files_to_clean+=("$d"); done < <(command find ~/Library/Application\ Support/Google -maxdepth 1 -name "AndroidStudio*" -print0 2>/dev/null)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 3. Xcode (Apple)
|
# 3. Xcode (Apple)
|
||||||
@@ -779,7 +784,7 @@ find_app_files() {
|
|||||||
# 4. JetBrains (IDE settings)
|
# 4. JetBrains (IDE settings)
|
||||||
if [[ "$bundle_id" =~ jetbrains ]] || [[ "$app_name" =~ IntelliJ|PyCharm|WebStorm|GoLand|RubyMine|PhpStorm|CLion|DataGrip|Rider ]]; then
|
if [[ "$bundle_id" =~ jetbrains ]] || [[ "$app_name" =~ IntelliJ|PyCharm|WebStorm|GoLand|RubyMine|PhpStorm|CLion|DataGrip|Rider ]]; then
|
||||||
for base in ~/Library/Application\ Support/JetBrains ~/Library/Caches/JetBrains ~/Library/Logs/JetBrains; do
|
for base in ~/Library/Application\ Support/JetBrains ~/Library/Caches/JetBrains ~/Library/Logs/JetBrains; do
|
||||||
[[ -d "$base" ]] && while IFS= read -r -d '' d; do files_to_clean+=("$d"); done < <(command find "$base" -maxdepth 1 -name "${app_name}*" -print0 2> /dev/null)
|
[[ -d "$base" ]] && while IFS= read -r -d '' d; do files_to_clean+=("$d"); done < <(command find "$base" -maxdepth 1 -name "${app_name}*" -print0 2>/dev/null)
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -846,11 +851,11 @@ find_app_system_files() {
|
|||||||
|
|
||||||
# Safety check: Skip if path ends with a common directory name (indicates empty app_name/bundle_id)
|
# Safety check: Skip if path ends with a common directory name (indicates empty app_name/bundle_id)
|
||||||
case "$p" in
|
case "$p" in
|
||||||
/Library/Application\ Support | /Library/Application\ Support/ | \
|
/Library/Application\ Support | /Library/Application\ Support/ | \
|
||||||
/Library/Caches | /Library/Caches/ | \
|
/Library/Caches | /Library/Caches/ | \
|
||||||
/Library/Logs | /Library/Logs/)
|
/Library/Logs | /Library/Logs/)
|
||||||
continue
|
continue
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
system_files+=("$p")
|
system_files+=("$p")
|
||||||
@@ -861,7 +866,7 @@ find_app_system_files() {
|
|||||||
for base in /Library/LaunchAgents /Library/LaunchDaemons; do
|
for base in /Library/LaunchAgents /Library/LaunchDaemons; do
|
||||||
[[ -d "$base" ]] && while IFS= read -r -d '' plist; do
|
[[ -d "$base" ]] && while IFS= read -r -d '' plist; do
|
||||||
system_files+=("$plist")
|
system_files+=("$plist")
|
||||||
done < <(command find "$base" -maxdepth 1 \( -name "*$app_name*.plist" \) -print0 2> /dev/null)
|
done < <(command find "$base" -maxdepth 1 \( -name "*$app_name*.plist" \) -print0 2>/dev/null)
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -870,11 +875,11 @@ find_app_system_files() {
|
|||||||
if [[ -n "$bundle_id" && "$bundle_id" != "unknown" && ${#bundle_id} -gt 3 ]]; then
|
if [[ -n "$bundle_id" && "$bundle_id" != "unknown" && ${#bundle_id} -gt 3 ]]; then
|
||||||
[[ -d /Library/PrivilegedHelperTools ]] && while IFS= read -r -d '' helper; do
|
[[ -d /Library/PrivilegedHelperTools ]] && while IFS= read -r -d '' helper; do
|
||||||
system_files+=("$helper")
|
system_files+=("$helper")
|
||||||
done < <(command find /Library/PrivilegedHelperTools -maxdepth 1 \( -name "$bundle_id*" \) -print0 2> /dev/null)
|
done < <(command find /Library/PrivilegedHelperTools -maxdepth 1 \( -name "$bundle_id*" \) -print0 2>/dev/null)
|
||||||
|
|
||||||
[[ -d /private/var/db/receipts ]] && while IFS= read -r -d '' receipt; do
|
[[ -d /private/var/db/receipts ]] && while IFS= read -r -d '' receipt; do
|
||||||
system_files+=("$receipt")
|
system_files+=("$receipt")
|
||||||
done < <(command find /private/var/db/receipts -maxdepth 1 \( -name "*$bundle_id*" \) -print0 2> /dev/null)
|
done < <(command find /private/var/db/receipts -maxdepth 1 \( -name "*$bundle_id*" \) -print0 2>/dev/null)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local receipt_files=""
|
local receipt_files=""
|
||||||
@@ -904,6 +909,13 @@ find_app_receipt_files() {
|
|||||||
# Skip if no bundle ID
|
# Skip if no bundle ID
|
||||||
[[ -z "$bundle_id" || "$bundle_id" == "unknown" ]] && return 0
|
[[ -z "$bundle_id" || "$bundle_id" == "unknown" ]] && return 0
|
||||||
|
|
||||||
|
# Validate bundle_id format to prevent wildcard injection
|
||||||
|
# Only allow alphanumeric characters, dots, hyphens, and underscores
|
||||||
|
if [[ ! "$bundle_id" =~ ^[a-zA-Z0-9._-]+$ ]]; then
|
||||||
|
debug_log "Invalid bundle_id format: $bundle_id"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
local -a receipt_files=()
|
local -a receipt_files=()
|
||||||
local -a bom_files=()
|
local -a bom_files=()
|
||||||
|
|
||||||
@@ -912,7 +924,7 @@ find_app_receipt_files() {
|
|||||||
if [[ -d /private/var/db/receipts ]]; then
|
if [[ -d /private/var/db/receipts ]]; then
|
||||||
while IFS= read -r -d '' bom; do
|
while IFS= read -r -d '' bom; do
|
||||||
bom_files+=("$bom")
|
bom_files+=("$bom")
|
||||||
done < <(find /private/var/db/receipts -maxdepth 1 -name "${bundle_id}*.bom" -print0 2> /dev/null)
|
done < <(find /private/var/db/receipts -maxdepth 1 -name "${bundle_id}*.bom" -print0 2>/dev/null)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Process bom files if any found
|
# Process bom files if any found
|
||||||
@@ -924,7 +936,7 @@ find_app_receipt_files() {
|
|||||||
# lsbom -f: file paths only
|
# lsbom -f: file paths only
|
||||||
# -s: suppress output (convert to text)
|
# -s: suppress output (convert to text)
|
||||||
local bom_content
|
local bom_content
|
||||||
bom_content=$(lsbom -f -s "$bom_file" 2> /dev/null)
|
bom_content=$(lsbom -f -s "$bom_file" 2>/dev/null)
|
||||||
|
|
||||||
while IFS= read -r file_path; do
|
while IFS= read -r file_path; do
|
||||||
# Standardize path (remove leading dot)
|
# Standardize path (remove leading dot)
|
||||||
@@ -935,6 +947,15 @@ find_app_receipt_files() {
|
|||||||
clean_path="/$clean_path"
|
clean_path="/$clean_path"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Path traversal protection: reject paths containing ..
|
||||||
|
if [[ "$clean_path" =~ \.\. ]]; then
|
||||||
|
debug_log "Rejected path traversal in BOM: $clean_path"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Normalize path (remove duplicate slashes)
|
||||||
|
clean_path=$(echo "$clean_path" | sed 's#//*#/#g')
|
||||||
|
|
||||||
# ------------------------------------------------------------------------
|
# ------------------------------------------------------------------------
|
||||||
# Safety check: restrict removal to trusted paths
|
# Safety check: restrict removal to trusted paths
|
||||||
# ------------------------------------------------------------------------
|
# ------------------------------------------------------------------------
|
||||||
@@ -942,21 +963,21 @@ find_app_receipt_files() {
|
|||||||
|
|
||||||
# Whitelisted prefixes (exclude /Users, /usr, /opt)
|
# Whitelisted prefixes (exclude /Users, /usr, /opt)
|
||||||
case "$clean_path" in
|
case "$clean_path" in
|
||||||
/Applications/*) is_safe=true ;;
|
/Applications/*) is_safe=true ;;
|
||||||
/Library/Application\ Support/*) is_safe=true ;;
|
/Library/Application\ Support/*) is_safe=true ;;
|
||||||
/Library/Caches/*) is_safe=true ;;
|
/Library/Caches/*) is_safe=true ;;
|
||||||
/Library/Logs/*) is_safe=true ;;
|
/Library/Logs/*) is_safe=true ;;
|
||||||
/Library/Preferences/*) is_safe=true ;;
|
/Library/Preferences/*) is_safe=true ;;
|
||||||
/Library/LaunchAgents/*) is_safe=true ;;
|
/Library/LaunchAgents/*) is_safe=true ;;
|
||||||
/Library/LaunchDaemons/*) is_safe=true ;;
|
/Library/LaunchDaemons/*) is_safe=true ;;
|
||||||
/Library/PrivilegedHelperTools/*) is_safe=true ;;
|
/Library/PrivilegedHelperTools/*) is_safe=true ;;
|
||||||
/Library/Extensions/*) is_safe=false ;;
|
/Library/Extensions/*) is_safe=false ;;
|
||||||
*) is_safe=false ;;
|
*) is_safe=false ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# Hard blocks
|
# Hard blocks
|
||||||
case "$clean_path" in
|
case "$clean_path" in
|
||||||
/System/* | /usr/bin/* | /usr/lib/* | /bin/* | /sbin/* | /private/*) is_safe=false ;;
|
/System/* | /usr/bin/* | /usr/lib/* | /bin/* | /sbin/* | /private/*) is_safe=false ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
if [[ "$is_safe" == "true" && -e "$clean_path" ]]; then
|
if [[ "$is_safe" == "true" && -e "$clean_path" ]]; then
|
||||||
@@ -965,7 +986,7 @@ find_app_receipt_files() {
|
|||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
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 "$clean_path"; then
|
if should_protect_path "$clean_path"; then
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
@@ -974,7 +995,7 @@ find_app_receipt_files() {
|
|||||||
receipt_files+=("$clean_path")
|
receipt_files+=("$clean_path")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
done <<< "$bom_content"
|
done <<<"$bom_content"
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
if [[ ${#receipt_files[@]} -gt 0 ]]; then
|
if [[ ${#receipt_files[@]} -gt 0 ]]; then
|
||||||
@@ -991,34 +1012,34 @@ force_kill_app() {
|
|||||||
# Get the executable name from bundle if app_path is provided
|
# Get the executable name from bundle if app_path is provided
|
||||||
local exec_name=""
|
local exec_name=""
|
||||||
if [[ -n "$app_path" && -e "$app_path/Contents/Info.plist" ]]; then
|
if [[ -n "$app_path" && -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
|
||||||
|
|
||||||
# Use executable name for precise matching, fallback to app name
|
# Use executable name for precise matching, fallback to app name
|
||||||
local match_pattern="${exec_name:-$app_name}"
|
local match_pattern="${exec_name:-$app_name}"
|
||||||
|
|
||||||
# Check if process is running using exact match only
|
# Check if process is running using exact match only
|
||||||
if ! pgrep -x "$match_pattern" > /dev/null 2>&1; then
|
if ! pgrep -x "$match_pattern" >/dev/null 2>&1; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Try graceful termination first
|
# Try graceful termination first
|
||||||
pkill -x "$match_pattern" 2> /dev/null || true
|
pkill -x "$match_pattern" 2>/dev/null || true
|
||||||
sleep 2
|
sleep 2
|
||||||
|
|
||||||
# Check again after graceful kill
|
# Check again after graceful kill
|
||||||
if ! pgrep -x "$match_pattern" > /dev/null 2>&1; then
|
if ! pgrep -x "$match_pattern" >/dev/null 2>&1; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Force kill if still running
|
# Force kill if still running
|
||||||
pkill -9 -x "$match_pattern" 2> /dev/null || true
|
pkill -9 -x "$match_pattern" 2>/dev/null || true
|
||||||
sleep 2
|
sleep 2
|
||||||
|
|
||||||
# If still running and sudo is available, try with sudo
|
# If still running and sudo is available, try with sudo
|
||||||
if pgrep -x "$match_pattern" > /dev/null 2>&1; then
|
if pgrep -x "$match_pattern" >/dev/null 2>&1; then
|
||||||
if sudo -n true 2> /dev/null; then
|
if sudo -n true 2>/dev/null; then
|
||||||
sudo pkill -9 -x "$match_pattern" 2> /dev/null || true
|
sudo pkill -9 -x "$match_pattern" 2>/dev/null || true
|
||||||
sleep 2
|
sleep 2
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
@@ -1026,7 +1047,7 @@ force_kill_app() {
|
|||||||
# Final check with longer timeout for stubborn processes
|
# Final check with longer timeout for stubborn processes
|
||||||
local retries=3
|
local retries=3
|
||||||
while [[ $retries -gt 0 ]]; do
|
while [[ $retries -gt 0 ]]; do
|
||||||
if ! pgrep -x "$match_pattern" > /dev/null 2>&1; then
|
if ! pgrep -x "$match_pattern" >/dev/null 2>&1; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
sleep 1
|
sleep 1
|
||||||
@@ -1034,7 +1055,7 @@ force_kill_app() {
|
|||||||
done
|
done
|
||||||
|
|
||||||
# Still running after all attempts
|
# Still running after all attempts
|
||||||
pgrep -x "$match_pattern" > /dev/null 2>&1 && return 1 || return 0
|
pgrep -x "$match_pattern" >/dev/null 2>&1 && return 1 || return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
# Note: calculate_total_size() is defined in lib/core/file_ops.sh
|
# Note: calculate_total_size() is defined in lib/core/file_ops.sh
|
||||||
|
|||||||
@@ -18,13 +18,13 @@ resolve_path() {
|
|||||||
[[ -e "$p" ]] || return 1
|
[[ -e "$p" ]] || return 1
|
||||||
|
|
||||||
# macOS 12.3+ and Linux have realpath
|
# macOS 12.3+ and Linux have realpath
|
||||||
if realpath "$p" 2> /dev/null; then
|
if realpath "$p" 2>/dev/null; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Fallback: use cd -P to resolve directory, then append basename
|
# Fallback: use cd -P to resolve directory, then append basename
|
||||||
local dir base
|
local dir base
|
||||||
dir=$(cd -P "$(dirname "$p")" 2> /dev/null && pwd) || return 1
|
dir=$(cd -P "$(dirname "$p")" 2>/dev/null && pwd) || return 1
|
||||||
base=$(basename "$p")
|
base=$(basename "$p")
|
||||||
echo "$dir/$base"
|
echo "$dir/$base"
|
||||||
}
|
}
|
||||||
@@ -32,7 +32,7 @@ resolve_path() {
|
|||||||
# Check if Homebrew is installed and accessible
|
# Check if Homebrew is installed and accessible
|
||||||
# Returns: 0 if brew is available, 1 otherwise
|
# Returns: 0 if brew is available, 1 otherwise
|
||||||
is_homebrew_available() {
|
is_homebrew_available() {
|
||||||
command -v brew > /dev/null 2>&1
|
command -v brew >/dev/null 2>&1
|
||||||
}
|
}
|
||||||
|
|
||||||
# Extract cask token from a Caskroom path
|
# Extract cask token from a Caskroom path
|
||||||
@@ -44,8 +44,8 @@ _extract_cask_token_from_path() {
|
|||||||
|
|
||||||
# Check if path is inside Caskroom
|
# Check if path is inside Caskroom
|
||||||
case "$path" in
|
case "$path" in
|
||||||
/opt/homebrew/Caskroom/* | /usr/local/Caskroom/*) ;;
|
/opt/homebrew/Caskroom/* | /usr/local/Caskroom/*) ;;
|
||||||
*) return 1 ;;
|
*) return 1 ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# Extract token from path: /opt/homebrew/Caskroom/<token>/<version>/...
|
# Extract token from path: /opt/homebrew/Caskroom/<token>/<version>/...
|
||||||
@@ -87,9 +87,9 @@ _detect_cask_via_caskroom_search() {
|
|||||||
[[ -d "$room" ]] || continue
|
[[ -d "$room" ]] || continue
|
||||||
while IFS= read -r match; do
|
while IFS= read -r match; do
|
||||||
[[ -n "$match" ]] || continue
|
[[ -n "$match" ]] || continue
|
||||||
token=$(_extract_cask_token_from_path "$match" 2> /dev/null) || continue
|
token=$(_extract_cask_token_from_path "$match" 2>/dev/null) || continue
|
||||||
[[ -n "$token" ]] && tokens+=("$token")
|
[[ -n "$token" ]] && tokens+=("$token")
|
||||||
done < <(find "$room" -maxdepth 3 -name "$app_bundle_name" 2> /dev/null)
|
done < <(find "$room" -maxdepth 3 -name "$app_bundle_name" 2>/dev/null)
|
||||||
done
|
done
|
||||||
|
|
||||||
# Need at least one token
|
# Need at least one token
|
||||||
@@ -101,7 +101,7 @@ _detect_cask_via_caskroom_search() {
|
|||||||
|
|
||||||
# Only succeed if exactly one unique token found and it's installed
|
# Only succeed if exactly one unique token found and it's installed
|
||||||
if ((${#uniq[@]} == 1)) && [[ -n "${uniq[0]}" ]]; then
|
if ((${#uniq[@]} == 1)) && [[ -n "${uniq[0]}" ]]; then
|
||||||
HOMEBREW_NO_ENV_HINTS=1 brew list --cask 2> /dev/null | grep -qxF "${uniq[0]}" || return 1
|
HOMEBREW_NO_ENV_HINTS=1 brew list --cask 2>/dev/null | grep -qxF "${uniq[0]}" || return 1
|
||||||
echo "${uniq[0]}"
|
echo "${uniq[0]}"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
@@ -115,7 +115,7 @@ _detect_cask_via_symlink_check() {
|
|||||||
[[ -L "$app_path" ]] || return 1
|
[[ -L "$app_path" ]] || return 1
|
||||||
|
|
||||||
local target
|
local target
|
||||||
target=$(readlink "$app_path" 2> /dev/null) || return 1
|
target=$(readlink "$app_path" 2>/dev/null) || return 1
|
||||||
_extract_cask_token_from_path "$target"
|
_extract_cask_token_from_path "$target"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,10 +127,10 @@ _detect_cask_via_brew_list() {
|
|||||||
app_name_lower=$(echo "${app_bundle_name%.app}" | LC_ALL=C tr '[:upper:]' '[:lower:]')
|
app_name_lower=$(echo "${app_bundle_name%.app}" | LC_ALL=C tr '[:upper:]' '[:lower:]')
|
||||||
|
|
||||||
local cask_name
|
local cask_name
|
||||||
cask_name=$(HOMEBREW_NO_ENV_HINTS=1 brew list --cask 2> /dev/null | grep -Fix "$app_name_lower") || return 1
|
cask_name=$(HOMEBREW_NO_ENV_HINTS=1 brew list --cask 2>/dev/null | grep -Fix "$app_name_lower") || return 1
|
||||||
|
|
||||||
# Verify this cask actually owns this app path
|
# Verify this cask actually owns this app path
|
||||||
HOMEBREW_NO_ENV_HINTS=1 brew info --cask "$cask_name" 2> /dev/null | grep -qF "$app_path" || return 1
|
HOMEBREW_NO_ENV_HINTS=1 brew info --cask "$cask_name" 2>/dev/null | grep -qF "$app_path" || return 1
|
||||||
echo "$cask_name"
|
echo "$cask_name"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,30 +173,27 @@ brew_uninstall_cask() {
|
|||||||
|
|
||||||
debug_log "Attempting brew uninstall --cask $cask_name"
|
debug_log "Attempting brew uninstall --cask $cask_name"
|
||||||
|
|
||||||
# Run uninstall with timeout (suppress hints/auto-update)
|
|
||||||
debug_log "Attempting brew uninstall --cask $cask_name"
|
|
||||||
|
|
||||||
# Ensure we have sudo access if needed, to prevent brew from hanging on password prompt
|
# Ensure we have sudo access if needed, to prevent brew from hanging on password prompt
|
||||||
# Many brew casks need sudo to uninstall
|
if ! sudo -n true 2>/dev/null; then
|
||||||
if ! sudo -n true 2> /dev/null; then
|
|
||||||
# If we don't have sudo, try to get it (visibly)
|
|
||||||
sudo -v
|
sudo -v
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local uninstall_ok=false
|
local uninstall_ok=false
|
||||||
|
local brew_exit=0
|
||||||
|
|
||||||
# Run directly without output capture to allow user interaction/visibility
|
# Run with timeout to prevent hangs from problematic cask scripts
|
||||||
# This avoids silence/hangs when brew asks for passwords or confirmation
|
if run_with_timeout 300 \
|
||||||
if HOMEBREW_NO_ENV_HINTS=1 HOMEBREW_NO_AUTO_UPDATE=1 NONINTERACTIVE=1 \
|
env HOMEBREW_NO_ENV_HINTS=1 HOMEBREW_NO_AUTO_UPDATE=1 NONINTERACTIVE=1 \
|
||||||
brew uninstall --cask "$cask_name"; then
|
brew uninstall --cask "$cask_name" 2>&1; then
|
||||||
uninstall_ok=true
|
uninstall_ok=true
|
||||||
else
|
else
|
||||||
debug_log "brew uninstall failed with exit code $?"
|
brew_exit=$?
|
||||||
|
debug_log "brew uninstall timeout or failed with exit code: $brew_exit"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Verify removal
|
# Verify removal
|
||||||
local cask_gone=true app_gone=true
|
local cask_gone=true app_gone=true
|
||||||
HOMEBREW_NO_ENV_HINTS=1 brew list --cask 2> /dev/null | grep -qxF "$cask_name" && cask_gone=false
|
HOMEBREW_NO_ENV_HINTS=1 brew list --cask 2>/dev/null | grep -qxF "$cask_name" && cask_gone=false
|
||||||
[[ -n "$app_path" && -e "$app_path" ]] && app_gone=false
|
[[ -n "$app_path" && -e "$app_path" ]] && app_gone=false
|
||||||
|
|
||||||
# Success: uninstall worked and both are gone, or already uninstalled
|
# Success: uninstall worked and both are gone, or already uninstalled
|
||||||
|
|||||||
Reference in New Issue
Block a user