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:
+14
-4
@@ -32,7 +32,11 @@
|
||||
# disk_cleanup.sh --clean --age 14 --dry-run # Preview cleanup with 14-day threshold
|
||||
#
|
||||
|
||||
set -o pipefail
|
||||
# Full strict mode. The two pipelines below that legitimately return
|
||||
# non-zero -- find hitting unreadable directories, and head closing a
|
||||
# pipe early -- are handled at their call sites rather than by leaving
|
||||
# the whole script lax.
|
||||
set -euo pipefail
|
||||
|
||||
# ===== CONFIGURATION =====
|
||||
AGE_THRESHOLD=7
|
||||
@@ -158,8 +162,12 @@ show_disk_usage() {
|
||||
df -h -x tmpfs -x devtmpfs || error_exit "Failed to get disk usage information"
|
||||
|
||||
echo -e "\n==== Top 10 Largest Files ===="
|
||||
find / -type f -printf '%s %p\n' 2>/dev/null | sort -nr | head -n 10 | \
|
||||
awk '{size=$1/1024/1024; printf("%.1f MB - ", size); $1=""; print $0}' || \
|
||||
# -xdev: without it this walks /proc, /sys and every network mount,
|
||||
# which is slow and reports files that are not really taking up disk.
|
||||
# The `|| true` absorbs both find's non-zero on unreadable directories
|
||||
# and the SIGPIPE head causes once it has its ten lines.
|
||||
{ find / -xdev -type f -printf '%s %p\n' 2>/dev/null | sort -nr | head -n 10 | \
|
||||
awk '{size=$1/1024/1024; printf("%.1f MB - ", size); $1=""; print $0}'; } || \
|
||||
echo "Warning: Could not retrieve largest files"
|
||||
}
|
||||
|
||||
@@ -218,7 +226,9 @@ clean_temporary_files() {
|
||||
|
||||
# Count files that would be deleted
|
||||
local file_count
|
||||
file_count=$(find "$dir" -type f -mtime +"$AGE_THRESHOLD" 2>/dev/null | wc -l)
|
||||
# `|| true` on the pipeline: find exits non-zero for unreadable
|
||||
# subdirectories, which under pipefail would abort the whole cleanup.
|
||||
file_count=$(find "$dir" -type f -mtime +"$AGE_THRESHOLD" 2>/dev/null | wc -l || true)
|
||||
|
||||
if [ "$file_count" -gt 0 ]; then
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
|
||||
+27
-5
@@ -17,17 +17,39 @@
|
||||
#
|
||||
# Usage: log_inspect.sh [search <pattern> | tail <logfile>]
|
||||
# Description: Searches across /var/log for a pattern, or tails a specific log file.
|
||||
#
|
||||
# Most of /var/log is root-only. Run this with sudo, or results will be
|
||||
# quietly partial -- see the note on the search branch.
|
||||
|
||||
if [ "$1" = "search" ]; then
|
||||
pattern="$2"
|
||||
set -euo pipefail
|
||||
|
||||
if [ "${1:-}" = "search" ]; then
|
||||
pattern="${2:-}"
|
||||
if [ -z "$pattern" ]; then
|
||||
echo "Usage: $0 search <pattern>"; exit 1
|
||||
fi
|
||||
echo "Searching for '$pattern' in /var/log..."
|
||||
grep -R -i --color=auto "$pattern" /var/log 2>/dev/null
|
||||
# 2>/dev/null hid permission errors, so an unprivileged run looked like
|
||||
# "no matches" rather than "could not read most of /var/log". Say so
|
||||
# explicitly instead. grep exits 1 on no match, which is not an error.
|
||||
# grep distinguishes three outcomes and they mean different things
|
||||
# here: 0 matched, 1 matched nothing, 2 could not read everything. The
|
||||
# original discarded stderr and ignored the status, so an unprivileged
|
||||
# run over root-owned logs was indistinguishable from a clean search.
|
||||
status=0
|
||||
grep -R -i --color=auto -- "$pattern" /var/log 2>/dev/null || status=$?
|
||||
case "$status" in
|
||||
0) ;;
|
||||
1) echo "No matches found." ;;
|
||||
*)
|
||||
echo "Search was incomplete -- some files under /var/log could not be read." >&2
|
||||
[ "$EUID" -ne 0 ] && echo "Re-run with sudo for a complete search." >&2
|
||||
exit "$status"
|
||||
;;
|
||||
esac
|
||||
exit 0
|
||||
elif [ "$1" = "tail" ]; then
|
||||
logfile="$2"
|
||||
elif [ "${1:-}" = "tail" ]; then
|
||||
logfile="${2:-}"
|
||||
if [ -z "$logfile" ]; then
|
||||
echo "Usage: $0 tail <log_file_path>"; exit 1
|
||||
fi
|
||||
|
||||
+52
-6
@@ -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"
|
||||
|
||||
+29
-12
@@ -18,9 +18,31 @@
|
||||
# Usage: service_manager.sh <action> <service_name>
|
||||
# Actions: start, stop, restart, status, enable, disable, list
|
||||
# Description: Uses systemctl or service to control services.
|
||||
#
|
||||
# status and list are read-only; everything else changes system state
|
||||
# and needs root.
|
||||
|
||||
action="$1"
|
||||
service="$2"
|
||||
set -euo pipefail
|
||||
|
||||
action="${1:-}"
|
||||
service="${2:-}"
|
||||
|
||||
# Validate up front so an unknown action cannot reach systemctl as a
|
||||
# bare word, and so the privilege check below has something to gate on.
|
||||
case "$action" in
|
||||
start|stop|restart|status|enable|disable|list) ;;
|
||||
"") echo "Usage: $0 {start|stop|restart|status|enable|disable|list} <service_name>" >&2; exit 1 ;;
|
||||
*) echo "Invalid action '$action'. Use start, stop, restart, status, enable, disable, or list." >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
case "$action" in
|
||||
start|stop|restart|enable|disable)
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo "'$action' changes system state and requires root. Re-run with sudo." >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$action" = "list" ]; then
|
||||
# List running services
|
||||
@@ -41,21 +63,16 @@ fi
|
||||
|
||||
if command -v systemctl &> /dev/null; then
|
||||
case "$action" in
|
||||
start|stop|restart|status)
|
||||
systemctl $action "$service"
|
||||
;;
|
||||
enable|disable)
|
||||
systemctl $action "$service"
|
||||
;;
|
||||
*)
|
||||
echo "Invalid action. Use start, stop, restart, status, enable, disable, or list."
|
||||
exit 1
|
||||
start|stop|restart|status|enable|disable)
|
||||
# Quoted: $action is validated above, but leaving it bare invites
|
||||
# word-splitting the moment anyone passes it through a variable.
|
||||
systemctl "$action" "$service"
|
||||
;;
|
||||
esac
|
||||
elif command -v service &> /dev/null; then
|
||||
case "$action" in
|
||||
start|stop|restart)
|
||||
service "$service" $action
|
||||
service "$service" "$action"
|
||||
;;
|
||||
status)
|
||||
service "$service" status
|
||||
|
||||
Reference in New Issue
Block a user