diff --git a/SECURITY_AUDIT.md b/SECURITY_AUDIT.md
index ebec8fc..52551e5 100644
--- a/SECURITY_AUDIT.md
+++ b/SECURITY_AUDIT.md
@@ -2,7 +2,7 @@
-**Status:** PASSED | **Risk Level:** LOW | **Version:** 1.22.1 (2026-01-17)
+**Status:** PASSED | **Risk Level:** LOW | **Version:** 1.23.2 (2026-01-26)
@@ -12,24 +12,31 @@
| Attribute | Details |
|-----------|---------|
-| Audit Date | January 17, 2026 |
+| Audit Date | January 26, 2026 |
| Audit Conclusion | **PASSED** |
-| Mole Version | V1.22.0 |
+| Mole Version | V1.23.2 |
| Audited Branch | `main` (HEAD) |
| Scope | Shell scripts, Go binaries, Configuration |
| Methodology | Static analysis, Threat modeling, Code review |
| Review Cycle | Every 6 months or after major feature additions |
-| Next Review | June 2026 |
+| Next Review | July 2026 |
**Key Findings:**
- Multi-layer validation effectively blocks risky system modifications.
- Conservative cleaning logic ensures safety (e.g., 60-day dormancy rule).
- Comprehensive protection for VPNs, AI tools, and core system components.
+- Operations logging improves traceability while remaining optional (MO_NO_OPLOG=1).
- Atomic operations prevent state corruption during crashes.
- Dry-run and whitelist features give users full control.
- Installer cleanup scans safely and requires user confirmation.
+**Recent Remediations:**
+
+- Symlink cleanup in `bin/clean.sh` now routes through `safe_remove` for target validation.
+- Orphaned helper cleanup in `lib/clean/apps.sh` now uses `safe_sudo_remove`.
+- ByHost preference cleanup in `lib/uninstall/batch.sh` validates bundle IDs and deletes via `safe_remove`.
+
---
## Security Philosophy
diff --git a/bin/clean.sh b/bin/clean.sh
index b8858d4..8f700c1 100755
--- a/bin/clean.sh
+++ b/bin/clean.sh
@@ -69,10 +69,10 @@ if [[ -f "$HOME/.config/mole/whitelist" ]]; then
fi
case "$line" in
- / | /System | /System/* | /bin | /bin/* | /sbin | /sbin/* | /usr/bin | /usr/bin/* | /usr/sbin | /usr/sbin/* | /etc | /etc/* | /var/db | /var/db/*)
- WHITELIST_WARNINGS+=("Protected system path: $line")
- continue
- ;;
+ / | /System | /System/* | /bin | /bin/* | /sbin | /sbin/* | /usr/bin | /usr/bin/* | /usr/sbin | /usr/sbin/* | /etc | /etc/* | /var/db | /var/db/*)
+ WHITELIST_WARNINGS+=("Protected system path: $line")
+ continue
+ ;;
esac
duplicate="false"
@@ -86,7 +86,7 @@ if [[ -f "$HOME/.config/mole/whitelist" ]]; then
fi
[[ "$duplicate" == "true" ]] && continue
WHITELIST_PATTERNS+=("$line")
- done <"$HOME/.config/mole/whitelist"
+ done < "$HOME/.config/mole/whitelist"
else
WHITELIST_PATTERNS=("${DEFAULT_WHITELIST_PATTERNS[@]}")
fi
@@ -140,7 +140,7 @@ cleanup() {
fi
CLEANUP_DONE=true
- stop_inline_spinner 2>/dev/null || true
+ stop_inline_spinner 2> /dev/null || true
if [[ -t 1 ]]; then
printf "\r\033[K" >&2 || true
@@ -166,8 +166,8 @@ start_section() {
if [[ "$DRY_RUN" == "true" ]]; then
ensure_user_file "$EXPORT_LIST_FILE"
- echo "" >>"$EXPORT_LIST_FILE"
- echo "=== $1 ===" >>"$EXPORT_LIST_FILE"
+ echo "" >> "$EXPORT_LIST_FILE"
+ echo "=== $1 ===" >> "$EXPORT_LIST_FILE"
fi
}
@@ -220,7 +220,7 @@ normalize_paths_for_cleanup() {
done
fi
[[ "$is_child" == "true" ]] || result_paths+=("$path")
- done <<<"$sorted_paths"
+ done <<< "$sorted_paths"
if [[ ${#result_paths[@]} -gt 0 ]]; then
printf '%s\n' "${result_paths[@]}"
@@ -232,9 +232,9 @@ get_cleanup_path_size_kb() {
local path="$1"
if [[ -f "$path" && ! -L "$path" ]]; then
- if command -v stat >/dev/null 2>&1; then
+ if command -v stat > /dev/null 2>&1; then
local bytes
- bytes=$(stat -f%z "$path" 2>/dev/null || echo "0")
+ bytes=$(stat -f%z "$path" 2> /dev/null || echo "0")
if [[ "$bytes" =~ ^[0-9]+$ && "$bytes" -gt 0 ]]; then
echo $(((bytes + 1023) / 1024))
return 0
@@ -243,9 +243,9 @@ get_cleanup_path_size_kb() {
fi
if [[ -L "$path" ]]; then
- if command -v stat >/dev/null 2>&1; then
+ if command -v stat > /dev/null 2>&1; then
local bytes
- bytes=$(stat -f%z "$path" 2>/dev/null || echo "0")
+ bytes=$(stat -f%z "$path" 2> /dev/null || echo "0")
if [[ "$bytes" =~ ^[0-9]+$ && "$bytes" -gt 0 ]]; then
echo $(((bytes + 1023) / 1024))
else
@@ -465,9 +465,9 @@ safe_clean() {
[[ ! "$size" =~ ^[0-9]+$ ]] && size=0
if [[ "$size" -gt 0 ]]; then
- echo "$size 1" >"$temp_dir/result_${idx}"
+ echo "$size 1" > "$temp_dir/result_${idx}"
else
- echo "0 0" >"$temp_dir/result_${idx}"
+ echo "0 0" > "$temp_dir/result_${idx}"
fi
((idx++))
@@ -492,17 +492,17 @@ safe_clean() {
[[ ! "$size" =~ ^[0-9]+$ ]] && size=0
local tmp_file="$temp_dir/result_${idx}.$$"
if [[ "$size" -gt 0 ]]; then
- echo "$size 1" >"$tmp_file"
+ echo "$size 1" > "$tmp_file"
else
- echo "0 0" >"$tmp_file"
+ echo "0 0" > "$tmp_file"
fi
- mv "$tmp_file" "$temp_dir/result_${idx}" 2>/dev/null || true
+ mv "$tmp_file" "$temp_dir/result_${idx}" 2> /dev/null || true
) &
pids+=($!)
((idx++))
if ((${#pids[@]} >= MOLE_MAX_PARALLEL_JOBS)); then
- wait "${pids[0]}" 2>/dev/null || true
+ wait "${pids[0]}" 2> /dev/null || true
pids=("${pids[@]:1}")
((completed++))
@@ -515,7 +515,7 @@ safe_clean() {
if [[ ${#pids[@]} -gt 0 ]]; then
for pid in "${pids[@]}"; do
- wait "$pid" 2>/dev/null || true
+ wait "$pid" 2> /dev/null || true
((completed++))
if [[ "$show_spinner" == "true" && -t 1 ]]; then
@@ -536,15 +536,11 @@ safe_clean() {
for path in "${existing_paths[@]}"; do
local result_file="$temp_dir/result_${idx}"
if [[ -f "$result_file" ]]; then
- read -r size count <"$result_file" 2>/dev/null || true
+ read -r size count < "$result_file" 2> /dev/null || true
local removed=0
if [[ "$DRY_RUN" != "true" ]]; then
- if [[ -L "$path" ]]; then
- rm "$path" 2>/dev/null && removed=1
- else
- if safe_remove "$path" true; then
- removed=1
- fi
+ if safe_remove "$path" true; then
+ removed=1
fi
else
removed=1
@@ -581,12 +577,8 @@ safe_clean() {
local removed=0
if [[ "$DRY_RUN" != "true" ]]; then
- if [[ -L "$path" ]]; then
- rm "$path" 2>/dev/null && removed=1
- else
- if safe_remove "$path" true; then
- removed=1
- fi
+ if safe_remove "$path" true; then
+ removed=1
fi
else
removed=1
@@ -643,9 +635,9 @@ safe_clean() {
local size=0
if [[ -n "${temp_dir:-}" && -f "$temp_dir/result_${idx}" ]]; then
- read -r size count <"$temp_dir/result_${idx}" 2>/dev/null || true
+ read -r size count < "$temp_dir/result_${idx}" 2> /dev/null || true
else
- size=$(get_cleanup_path_size_kb "$path" 2>/dev/null || echo "0")
+ size=$(get_cleanup_path_size_kb "$path" 2> /dev/null || echo "0")
fi
[[ "$size" == "0" || -z "$size" ]] && {
@@ -653,7 +645,7 @@ safe_clean() {
continue
}
- echo "$(dirname "$path")|$size|$path" >>"$paths_temp"
+ echo "$(dirname "$path")|$size|$path" >> "$paths_temp"
((idx++))
done
fi
@@ -684,9 +676,9 @@ safe_clean() {
' | while IFS='|' read -r display_path total_size child_count; do
local size_human=$(bytes_to_human "$((total_size * 1024))")
if [[ $child_count -gt 1 ]]; then
- echo "$display_path # $size_human, $child_count items" >>"$EXPORT_LIST_FILE"
+ echo "$display_path # $size_human, $child_count items" >> "$EXPORT_LIST_FILE"
else
- echo "$display_path # $size_human" >>"$EXPORT_LIST_FILE"
+ echo "$display_path # $size_human" >> "$EXPORT_LIST_FILE"
fi
done
@@ -726,7 +718,7 @@ start_cleanup() {
SYSTEM_CLEAN=false
ensure_user_file "$EXPORT_LIST_FILE"
- cat >"$EXPORT_LIST_FILE" < "$EXPORT_LIST_FILE" << EOF
# Mole Cleanup Preview - $(date '+%Y-%m-%d %H:%M:%S')
#
# How to protect files:
@@ -742,7 +734,7 @@ EOF
fi
if [[ -t 0 ]]; then
- if sudo -n true 2>/dev/null; then
+ if sudo -n true 2> /dev/null; then
SYSTEM_CLEAN=true
echo -e "${GREEN}${ICON_SUCCESS}${NC} Admin access already available"
echo ""
@@ -782,7 +774,7 @@ EOF
else
echo ""
echo "Running in non-interactive mode"
- if sudo -n true 2>/dev/null; then
+ if sudo -n true 2> /dev/null; then
SYSTEM_CLEAN=true
echo " ${ICON_LIST} System-level cleanup enabled, sudo session active"
else
@@ -1033,7 +1025,7 @@ perform_cleanup() {
echo "# Potential cleanup: ${freed_gb}GB"
echo "# Items: $files_cleaned"
echo "# Categories: $total_items"
- } >>"$EXPORT_LIST_FILE"
+ } >> "$EXPORT_LIST_FILE"
summary_details+=("Detailed file list: ${GRAY}$EXPORT_LIST_FILE${NC}")
summary_details+=("Use ${GRAY}mo clean --whitelist${NC} to add protection rules")
@@ -1085,18 +1077,18 @@ perform_cleanup() {
main() {
for arg in "$@"; do
case "$arg" in
- "--debug")
- export MO_DEBUG=1
- ;;
- "--dry-run" | "-n")
- DRY_RUN=true
- export MOLE_DRY_RUN=1
- ;;
- "--whitelist")
- source "$SCRIPT_DIR/../lib/manage/whitelist.sh"
- manage_whitelist "clean"
- exit 0
- ;;
+ "--debug")
+ export MO_DEBUG=1
+ ;;
+ "--dry-run" | "-n")
+ DRY_RUN=true
+ export MOLE_DRY_RUN=1
+ ;;
+ "--whitelist")
+ source "$SCRIPT_DIR/../lib/manage/whitelist.sh"
+ manage_whitelist "clean"
+ exit 0
+ ;;
esac
done
diff --git a/lib/clean/apps.sh b/lib/clean/apps.sh
index f3e084b..9fffe40 100644
--- a/lib/clean/apps.sh
+++ b/lib/clean/apps.sh
@@ -500,7 +500,7 @@ clean_orphaned_system_services() {
if [[ "$orphan_file" == *.plist ]]; then
sudo launchctl unload "$orphan_file" 2> /dev/null || true
fi
- if sudo rm -f "$orphan_file" 2> /dev/null; then
+ if safe_sudo_remove "$orphan_file"; then
debug_log "Removed orphaned service: $orphan_file"
fi
fi
diff --git a/lib/uninstall/batch.sh b/lib/uninstall/batch.sh
index dba6e55..9c405ef 100755
--- a/lib/uninstall/batch.sh
+++ b/lib/uninstall/batch.sh
@@ -502,8 +502,14 @@ batch_uninstall_applications() {
fi
# ByHost preferences (machine-specific).
- if [[ -d ~/Library/Preferences/ByHost ]]; then
- find ~/Library/Preferences/ByHost -maxdepth 1 -name "${bundle_id}.*.plist" -delete 2> /dev/null || true
+ if [[ -d "$HOME/Library/Preferences/ByHost" ]]; then
+ if [[ "$bundle_id" =~ ^[A-Za-z0-9._-]+$ ]]; then
+ while IFS= read -r -d '' plist_file; do
+ safe_remove "$plist_file" true > /dev/null || true
+ done < <(command find "$HOME/Library/Preferences/ByHost" -maxdepth 1 -type f -name "${bundle_id}.*.plist" -print0 2> /dev/null || true)
+ else
+ debug_log "Skipping ByHost cleanup, invalid bundle id: $bundle_id"
+ fi
fi
fi