Finish the pass: log_inspect, service_manager, rsync_magic, disk_cleanup

log_inspect.sh discarded grep's stderr and ignored its exit status, so
an unprivileged search over root-owned logs was indistinguishable from a
search that genuinely found nothing. grep's three outcomes now mean
three different things: matched, matched nothing, or could not read
everything -- the last of which says so and exits non-zero. Confirmed
grep returns 2 rather than 1 in that case, which is why the naive
"status -eq 1" check would never have fired.

service_manager.sh validates the action before dispatch and requires
root for the five that change system state, leaving status and list open
to anyone. $action is quoted at both call sites.

rsync_magic.sh had --inplace on unconditionally. It writes straight into
destination files instead of to a temporary and renaming, so an
interrupted run leaves them partially overwritten -- the opposite of
what a backup tool should guarantee. Now opt-in, with a warning when
used. Its log lives under /var/log and every line pipes through tee, so
under pipefail an unprivileged run died on the first line with a bare
permission error; it now falls back to stdout rather than failing the
sync over its own logging. --delete also confirms before running, since
reversing the two arguments erases the backup.

disk_cleanup.sh moves from `set -o pipefail` to full strict mode, with
the two pipelines that legitimately return non-zero handled at their
call sites rather than by leaving the script lax. Its "largest files"
walk also gained -xdev, which it was missing while security_audit.sh
next door already had it -- without it the walk descends /proc, /sys and
every network mount.

All fifteen scripts now run under set -euo pipefail.
This commit is contained in:
2026-08-22 22:24:50 -07:00
parent f8a86736f9
commit 96774aba35
4 changed files with 124 additions and 29 deletions
+52 -6
View File
@@ -25,18 +25,28 @@ TIMESTAMP=$(date +"%Y-%m-%d_%H-%M-%S")
# ======= Help Function =======
usage() {
echo "Usage: $0 [--dry-run] <source> <destination>"
echo "Optional: --dry-run to simulate the sync"
echo "Usage: $0 [--dry-run] [--inplace] [--yes] <source> <destination>"
echo " --dry-run simulate the sync, change nothing"
echo " --inplace write into destination files directly (see note below)"
echo " --yes skip the confirmation prompt for --delete"
exit 1
}
# ======= Argument Parsing =======
DRY_RUN=0
INPLACE=0
ASSUME_YES=0
if [[ "${1:-}" == "--dry-run" ]]; then
DRY_RUN=1
while [[ "${1:-}" == --* ]]; do
case "$1" in
--dry-run) DRY_RUN=1 ;;
--inplace) INPLACE=1 ;;
--yes) ASSUME_YES=1 ;;
--help|-h) usage ;;
*) echo "Unknown option: $1" >&2; usage ;;
esac
shift
fi
done
SOURCE="${1:-}"
DEST="${2:-}"
@@ -66,11 +76,22 @@ RSYNC_OPTS=(
-X # preserve extended attributes
--delete # delete extraneous files from destination
--numeric-ids # don't map uid/gid numbers to usernames
--inplace # update destination files in place
--backup # backup overwritten files
--backup-dir="${DEST}/.backup-${TIMESTAMP}" # backup location
)
# --inplace was previously always on. It writes directly into the
# destination file rather than to a temporary and renaming, so an
# interrupted transfer leaves the destination partially overwritten and
# corrupt -- the opposite of what a backup should guarantee. rsync's
# default (temp file, atomic rename) costs extra space on the target and
# is worth it here, so --inplace is now opt-in for the cases that need
# it, such as very large files on space-constrained targets.
if [[ "$INPLACE" -eq 1 ]]; then
RSYNC_OPTS+=(--inplace)
echo "WARNING: --inplace means an interrupted run can leave corrupt files at the destination."
fi
# Add dry-run flag if needed
if [[ "$DRY_RUN" -eq 1 ]]; then
RSYNC_OPTS+=(--dry-run)
@@ -82,6 +103,31 @@ if [[ -f "$EXCLUDES" ]]; then
RSYNC_OPTS+=(--exclude-from="$EXCLUDES")
fi
# The log lives under /var/log, which needs root. Every echo below pipes
# through `tee -a`, so with set -o pipefail an unprivileged run died on
# the first line with a bare "Permission denied" and no explanation.
# Fall back to stdout instead of failing the sync over its logging.
if ! { [ -w "$LOG_FILE" ] || { [ ! -e "$LOG_FILE" ] && [ -w "$(dirname "$LOG_FILE")" ]; }; }; then
echo "NOTE: cannot write $LOG_FILE (need root); logging to stdout only." >&2
LOG_FILE=/dev/null
fi
# --delete removes anything at the destination that is not in the source.
# Reversing the two arguments therefore erases the backup. --backup-dir
# above catches the deleted files, but confirm anyway -- the prompt is
# cheaper than discovering the mistake later.
if [[ "$DRY_RUN" -ne 1 && "$ASSUME_YES" -ne 1 ]]; then
echo "About to sync with --delete:"
echo " FROM: $SOURCE/"
echo " TO: $DEST/ (extraneous files here will be removed)"
if [ ! -t 0 ]; then
echo "Refusing to run unattended without --yes." >&2
exit 1
fi
read -r -p "Proceed? (yes/NO): " reply
[ "$reply" = "yes" ] || { echo "Cancelled."; exit 0; }
fi
# ======= Run Rsync =======
echo "Starting rsync at $TIMESTAMP" | tee -a "$LOG_FILE"
echo "Source: $SOURCE" | tee -a "$LOG_FILE"