Programming and Scripting for Cybersecurity: Part 3 – Automating Preventive Maintenance

Published on
7 mins read
--- views

Introduction

With the lab environment ready, it is time to automate real maintenance routines. Part 3 of the series guides you through designing a Bash script that backs up configuration data, cleans expired artefacts, and records every action in a timestamped log. By the end of this lesson, you will understand how conditionals, loops, and arguments work together to keep Linux servers healthy.

Learning Objectives

  • Build a Bash script that verifies prerequisites, creates compressed backups, and handles failure conditions gracefully.
  • Employ reusable logging helpers so each step emits structured messages for audit trails.
  • Iterate over collections using loops to identify and delete stale files without removing fresh data.
  • Parameterize the script with positional arguments to accommodate different environments.
  • Reflect on path strategies, portability, and security safeguards used in maintenance automation.

Scenario: Automating Preventive Maintenance at Acme

The security team at Acme recently launched a preventive monitoring programme. Instead of running one-off commands manually, they want a reliable script that operations can schedule through cron. The script must:

  1. Prepare or reuse a backup directory.
  2. Archive /etc (or another target) with a time-stamped filename.
  3. Remove .bak files older than 30 days.
  4. Log progress and errors for later review.

You have been tasked with producing the initial version, validating its behaviour, and documenting why each design choice supports security and portability.

Script Walkthrough — mantenimiento.sh

The implementation below follows the requirements and adds safeguards that we learned in earlier parts of the series.

#!/usr/bin/env bash
# Automate maintenance/auditing workflow with validations and logging.
# Args (optional):
#   1) SOURCE_DIR  - directory to back up (default: /etc)
#   2) DEST_FOLDER - destination folder name (default: backups)

# ===== Defaults & Args =====
SOURCE_DIR="${1:-/etc}"
DEST_FOLDER="${2:-backups}"

# Validate source directory before doing anything else
if [[ ! -d "$SOURCE_DIR" ]]; then
  printf 'Error: source directory does not exist: %s\n' "$SOURCE_DIR" >&2
  exit 1
fi

# Resolve an absolute path to avoid surprises with relative input
if ! SOURCE_ABS=$(cd "$SOURCE_DIR" 2>/dev/null && pwd -P); then
  printf 'Error: unable to access source directory: %s\n' "$SOURCE_DIR" >&2
  exit 1
fi

# Date stamps
STAMP="$(date '+%Y%m%d_%H%M%S')"

# Paths
SCRIPT_DIR="$(pwd)"
DEST_PATH="${SCRIPT_DIR}/${DEST_FOLDER}"

if ! mkdir -p "$DEST_PATH"; then
  printf 'Error: failed to create destination folder: %s\n' "$DEST_PATH" >&2
  exit 1
fi

LOG_FILE="${DEST_PATH}/mantenimiento_${STAMP}.log"
if ! touch "$LOG_FILE" 2>/dev/null; then
  printf 'Error: cannot write log file: %s\n' "$LOG_FILE" >&2
  exit 1
fi

timestamp() { date '+%Y-%m-%d %H:%M:%S'; }

log_line() {
  local level="$1"
  shift
  local target="$1"
  shift
  local message="$*"
  local formatted
  formatted=$(printf '[%s] %s | %s\n' "$level" "$(timestamp)" "$message")
  if [[ "$target" == "stderr" ]]; then
    printf '%s' "$formatted" | tee -a "$LOG_FILE" >&2
  else
    printf '%s' "$formatted" | tee -a "$LOG_FILE"
  fi
}

log_info()  { log_line "INFO" "stdout" "$@"; }
log_warn()  { log_line "WARN" "stderr" "$@"; }
log_error() { log_line "ERROR" "stderr" "$@"; }

log_info "=== Maintenance run started ==="
log_info "Source: ${SOURCE_ABS}"
log_info "Destination: ${DEST_PATH}"

# ===== A) Ensure backup folder exists =====
log_info "Ensuring backup folder exists: ${DEST_PATH}"
if [[ -d "$DEST_PATH" ]]; then
  log_info "Backup folder ready."
else
  log_error "Backup folder is not available after creation attempt."
  exit 1
fi

# ===== B) Compressed backup of SOURCE_DIR with date in name =====
SOURCE_LABEL="$(basename "$SOURCE_ABS")"
if [[ -z "$SOURCE_LABEL" || "$SOURCE_LABEL" == "/" ]]; then
  SOURCE_LABEL="root"
fi
BACKUP_FILE="${DEST_PATH}/${SOURCE_LABEL}_backup_${STAMP}.tar.gz"

log_info "Creating compressed backup: ${BACKUP_FILE}"

if [[ "$SOURCE_ABS" == "/" ]]; then
  TAR_BASE="/"
  TAR_TARGET="."
else
  TAR_BASE="$(dirname "$SOURCE_ABS")"
  TAR_TARGET="$(basename "$SOURCE_ABS")"
fi

if tar -czf "$BACKUP_FILE" -C "$TAR_BASE" "$TAR_TARGET" >>"$LOG_FILE" 2>&1; then
  if [[ -s "$BACKUP_FILE" ]]; then
    log_info "Backup created successfully: ${BACKUP_FILE}"
  else
    log_error "Backup file is empty. Aborting."
    exit 1
  fi
else
  log_error "Backup failed. Aborting."
  exit 1
fi

# ===== C) Delete .bak files older than 30 days inside SOURCE_DIR =====
log_info "Searching .bak files older than 30 days in: ${SOURCE_ABS}"

CANDIDATES_RAW=$(find "$SOURCE_ABS" -type f -name '*.bak' -mtime +30 -print 2>>"$LOG_FILE" || true)

if [[ -n "$CANDIDATES_RAW" ]]; then
  CANDIDATE_COUNT=$(printf '%s\n' "$CANDIDATES_RAW" | wc -l | tr -d ' ')
  log_info "${CANDIDATE_COUNT} .bak files scheduled for deletion."
  printf '%s\n' "$CANDIDATES_RAW" | while IFS= read -r file; do
    [[ -z "$file" ]] && continue
    log_info "Deleting: ${file}"
  done
  if find "$SOURCE_ABS" -type f -name '*.bak' -mtime +30 -delete >>"$LOG_FILE" 2>&1; then
    log_info ".bak cleanup completed."
  else
    log_warn "Cleanup encountered issues. Review log: ${LOG_FILE}"
  fi
else
  log_info "No .bak files older than 30 days found."
fi

# ===== D) Log bookkeeping =====
log_info "All steps completed."
log_info "Log file: ${LOG_FILE}"
log_info "Backup file: ${BACKUP_FILE}"
log_info "=== Maintenance run finished ==="

Concept Highlights

Validations and Early Exits

  • The script immediately checks whether SOURCE_DIR exists. Early failure prevents a cascade of misleading errors.
  • cd "$SOURCE_DIR" is avoided; instead we derive an absolute path (SOURCE_ABS) using pwd -P to handle symbolic links safely.
  • Every filesystem operation (mkdir, touch, tar) checks its result and aborts on failure, keeping logs trustworthy.

Logging Strategy

  • timestamp() provides ISO-formatted time stamps, making logs sortable in spreadsheets or SIEM tools.
  • log_info, log_warn, and log_error centralize message formatting, reducing duplication and clarifying severity levels.
  • Messages are streamed through tee so the operator sees progress on-screen while the log file captures the same content.

Looping Through Cleanup Targets

  • A find command gathers candidate .bak files older than 30 days. The script tallies them and logs each deletion for traceability.
  • Because deletions are potentially destructive, the script prints the directory names before invoking find … -delete and captures stdout/stderr for later inspection.

Parameterization and Portability

  • Default arguments (/etc, backups) support drop-in usage, while optional parameters allow other teams to repurpose the script.
  • Output paths are derived from the current working directory (SCRIPT_DIR), which is helpful when operations mount the script into containerized jobs.
  • The script uses POSIX-compliant constructs (with Bash extensions kept minimal) to remain portable across common Linux distributions.

Running the Script and Capturing Evidence

After saving the file as mantenimiento.sh, run:

chmod +x mantenimiento.sh
./mantenimiento.sh

To capture arguments (e.g., backing up /var/www to nightly_backups):

./mantenimiento.sh /var/www nightly_backups

Verify the backup archive and log by listing the destination directory and inspecting the log header:

ls -lh nightly_backups
head -n 10 nightly_backups/mantenimiento_*.log

Screenshots of these commands serve as artefacts of successful execution.

Reflection and Theoretical Justification

  • Conditionals gate every critical action, ensuring that later steps run only if prerequisites succeed. This prevents partial state and keeps logs accurate.
  • Loops manage dynamic sets of files (find results), replacing manual deletion with repeatable logic.
  • Arguments make the script reusable across servers with different directory structures, promoting consistency within the operations team.
  • Absolute vs. relative paths. The script relies on absolute paths for source directories and log locations to eliminate ambiguity. Within the backup directory, relative references (e.g., ./mantenimiento_${STAMP}.log) could be used safely.
  • Portability and security measures. Using set -euo pipefail (which you may add if running on Bash 4+) and verifying directory existence defend against misconfigurations. Logging to a dedicated file supports auditing, and the optional rotation or hashing strategies described earlier strengthen tamper detection.

Next Steps

To continue hardening the maintenance workflow, consider:

  • Adding email or webhook notifications when backups fail.
  • Incorporating checksum generation and verification for archived files.
  • Extending the script with command-line flags (using getopts) for verbose mode or dry runs.
  • Scheduling the script via cron with environment-safe wrappers (/usr/bin/env bash).

These iterations build on the skills introduced here, preparing you to manage larger automation projects in secure operations environments.