1.8 Shell Scripting Practice

Practice Bash with realistic security and operations workflows

The previous lesson explained the building blocks. This lab turns those blocks into practical scripts, so try each scenario before opening the solution and treat the examples as starting points you can adapt.

Welcome to the Shell Scripting Practice Lab. This page wraps up the shell scripting series with 10 realistic, scenario-based challenges designed for cybersecurity analysts and DevOps engineers.

Each scenario follows a structured format:

  • Situation: a realistic problem you might see on the job
  • Objective: what the script needs to accomplish
  • Solution: a complete working script, hidden until you choose to open it
  • Best practices: production habits to carry forward

Try to write your own solution before revealing the answer.

Shell Scripting Best Practices for Security & DevOps

  1. Always start scripts with set -euo pipefail for strict error handling
  2. Quote variables like "$var" to prevent word splitting and glob expansion
  3. Use [[ ]] over [ ] for safer, modern conditional tests
  4. Validate inputs before using user-provided data in security scripts
  5. Log important actions with timestamps because audit trails matter during incident response
  6. Use local variables in functions to prevent namespace pollution
  7. Implement proper exit codes: 0 for success, non-zero for specific failure types
  8. Use shellcheck to lint your scripts before deploying to production
  9. Never hardcode credentials; use environment variables or secret managers
  10. Add usage messages and --help flags to all scripts shared with your team
Scenario 1

Failed SSH Login Monitor

Situation
Your company's cloud server has been experiencing brute-force SSH login attempts. The security team needs a script that monitors /var/log/auth.log in real-time, counts failed attempts per IP, and triggers an alert when any single IP exceeds a threshold, optionally adding the IP to a blocklist.
Objective
Parse auth logs for failed SSH login attempts, aggregate by source IP, and alert (or auto-block) when an IP exceeds N failed attempts within a time window.
Solution
#!/bin/bash
# ssh_monitor.sh - Failed SSH Login Monitor
# Usage: ./ssh_monitor.sh [threshold] [logfile]

set -euo pipefail

THRESHOLD="${1:-5}"
LOGFILE="${2:-/var/log/auth.log}"
ALERT_LOG="/var/log/ssh_alerts.log"

log_alert() {
    local msg="[$(date '+%Y-%m-%d %H:%M:%S')] ALERT: $1"
    echo "$msg" | tee -a "$ALERT_LOG"
}

check_failed_logins() {
    echo "=== SSH Failed Login Report ==="
    echo "Threshold: $THRESHOLD attempts"
    echo "Log file:  $LOGFILE"
    echo "-------------------------------"

    # Extract failed login IPs and count occurrences
    grep "Failed password" "$LOGFILE" 2>/dev/null \
        | grep -oP 'from \K[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' \
        | sort | uniq -c | sort -rn \
        | while read -r count ip; do
            if (( count >= THRESHOLD )); then
                log_alert "$ip has $count failed attempts, BLOCKING"
                # Auto-block with iptables (uncomment in production)
                # iptables -A INPUT -s "$ip" -j DROP
                echo "  [BLOCKED] $ip, $count attempts"
            else
                echo "  [OK]      $ip, $count attempts"
            fi
        done

    echo "-------------------------------"
    echo "Report generated: $(date)"
}

# Validate log file exists
if [[ ! -f "$LOGFILE" ]]; then
    echo "Error: Log file '$LOGFILE' not found" >&2
    exit 1
fi

check_failed_logins
Best Practices
  • Always validate input files exist before processing
  • Use set -euo pipefail for safer script execution
  • Log all security actions with timestamps for audit trails
  • Test blocking rules in dry-run mode before enabling auto-block
Scenario 2

Automated Backup with Integrity Verification

Situation
Your DevOps team manages multiple production servers. Critical configuration files and databases need daily backups with integrity verification. Old backups must be rotated to save disk space, and the team needs email notifications on failure.
Objective
Create a backup script that archives specified directories, generates SHA-256 checksums for integrity, implements backup rotation (keep last N backups), and reports success/failure.
Solution
#!/bin/bash
# secure_backup.sh - Automated Backup with Integrity Check
# Usage: ./secure_backup.sh

set -euo pipefail

# Configuration
BACKUP_DIRS=("/etc" "/var/www" "/home")
BACKUP_DEST="/backup"
RETENTION_DAYS=30
DATE_STAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="${BACKUP_DEST}/backup_${DATE_STAMP}.tar.gz"
CHECKSUM_FILE="${BACKUP_FILE}.sha256"
LOG_FILE="/var/log/backup.log"

log() { echo "[$(date '+%F %T')] $1" | tee -a "$LOG_FILE"; }

# Ensure backup directory exists
mkdir -p "$BACKUP_DEST"

log "Starting backup..."

# Create compressed archive
if tar -czf "$BACKUP_FILE" "${BACKUP_DIRS[@]}" 2>>"$LOG_FILE"; then
    log "Archive created: $BACKUP_FILE ($(du -h "$BACKUP_FILE" | cut -f1))"
else
    log "ERROR: Backup failed!"
    exit 1
fi

# Generate checksum
sha256sum "$BACKUP_FILE" > "$CHECKSUM_FILE"
log "Checksum generated: $(cat "$CHECKSUM_FILE")"

# Verify integrity immediately
if sha256sum -c "$CHECKSUM_FILE" &>/dev/null; then
    log "Integrity check: PASSED [OK]"
else
    log "ERROR: Integrity check FAILED!"
    exit 2
fi

# Rotate old backups
deleted=$(find "$BACKUP_DEST" -name "backup_*.tar.gz" \
    -mtime +"$RETENTION_DAYS" -delete -print | wc -l)
log "Rotation: Removed $deleted backups older than ${RETENTION_DAYS} days"

log "Backup completed successfully"
Best Practices
  • Always verify backup integrity immediately after creation
  • Implement backup rotation to prevent disk space exhaustion
  • Log every step for troubleshooting and audit compliance
  • Store backups on a separate volume or remote location
Scenario 3

Bash Port Scanner

Situation
During a penetration test engagement, you find yourself on a compromised host with no tools installed, no nmap, no netcat, nothing. You need to perform basic network reconnaissance against an internal target to identify open services using only bash built-ins.
Objective
Build a lightweight port scanner using bash's /dev/tcp pseudo-device to scan a target host for open ports, with timeout handling and clean output.
Solution
#!/bin/bash
# bash_scanner.sh - Lightweight Bash Port Scanner
# Usage: ./bash_scanner.sh <target> [start_port] [end_port]

set -uo pipefail

TARGET="${1:?Usage: $0 <target> [start_port] [end_port]}"
START_PORT="${2:-1}"
END_PORT="${3:-1024}"
TIMEOUT=1
OPEN_PORTS=()

echo "----------------------------------------"
echo "  Bash Port Scanner v1.0"
echo "  Target: $TARGET"
echo "  Range:  $START_PORT, $END_PORT"
echo "----------------------------------------"
echo ""
echo "Scanning..."

for (( port=START_PORT; port<=END_PORT; port++ )); do
    # Use /dev/tcp to attempt connection with timeout
    (echo >/dev/tcp/"$TARGET"/"$port") 2>/dev/null &
    pid=$!

    # Wait with timeout
    ( sleep "$TIMEOUT"; kill "$pid" 2>/dev/null ) &
    waiter=$!
    wait "$pid" 2>/dev/null
    result=$?
    kill "$waiter" 2>/dev/null
    wait "$waiter" 2>/dev/null

    if [[ "$result" -eq 0 ]]; then
        OPEN_PORTS+=("$port")
        printf "  [OPEN] Port %-5d\n" "$port"
    fi

    # Progress indicator every 100 ports
    (( port % 100 == 0 )) && echo "  ... scanned $port/$END_PORT"
done

echo ""
echo "----------------------------------------"
echo "  Scan Complete"
echo "  Open ports: ${#OPEN_PORTS[@]}"
for p in "${OPEN_PORTS[@]}"; do
    printf "    -> %d\n" "$p"
done
echo "----------------------------------------"
Best Practices
  • Only use in authorized penetration testing engagements
  • Implement timeouts to avoid hanging on filtered ports
  • This is slower than nmap; use it only when no tools are available
  • /dev/tcp is a bash-specific feature and is not available in all shells
Scenario 4

Log Rotation & Cleanup Daemon

Situation
A production web application generates massive log files that consume disk space rapidly. The server ran out of disk space last week causing a 2-hour outage. Management wants automated log management with compression, rotation, and disk space alerts.
Objective
Write a script that compresses logs older than 1 day, deletes compressed logs older than 30 days, monitors disk usage, and alerts when thresholds are exceeded.
Solution
#!/bin/bash
# log_manager.sh - Log Rotation & Cleanup
# Run via cron: 0 2 * * * /opt/scripts/log_manager.sh

set -euo pipefail

LOG_DIRS=("/var/log/nginx" "/var/log/app" "/var/log/syslog")
COMPRESS_AGE=1    # Compress logs older than N days
DELETE_AGE=30     # Delete compressed logs older than N days
DISK_THRESHOLD=80 # Alert if disk usage exceeds N%
REPORT="/var/log/log_manager_report.log"

log() { echo "[$(date '+%F %T')] $1" | tee -a "$REPORT"; }

log "=== Log Management Started ==="

total_compressed=0
total_deleted=0
space_saved=0

for dir in "${LOG_DIRS[@]}"; do
    [[ -d "$dir" ]] || continue
    log "Processing: $dir"

    # Compress old logs (skip already compressed)
    while IFS= read -r -d '' file; do
        size_before=$(stat -c%s "$file")
        if gzip "$file" 2>/dev/null; then
            size_after=$(stat -c%s "${file}.gz")
            saved=$(( size_before - size_after ))
            space_saved=$(( space_saved + saved ))
            ((total_compressed++))
        fi
    done < <(find "$dir" -name "*.log" -mtime +"$COMPRESS_AGE" \
        -not -name "*.gz" -print0)

    # Delete old compressed logs
    count=$(find "$dir" -name "*.gz" -mtime +"$DELETE_AGE" \
        -delete -print | wc -l)
    total_deleted=$((total_deleted + count))
done

# Check disk usage
disk_usage=$(df / | awk 'NR==2 {print $5}' | tr -d '%')
if (( disk_usage >= DISK_THRESHOLD )); then
    log "WARNING: Disk usage at ${disk_usage}% (threshold: ${DISK_THRESHOLD}%)"
fi

log "Compressed: $total_compressed files"
log "Deleted:    $total_deleted old archives"
log "Saved:      $(( space_saved / 1024 / 1024 )) MB"
log "Disk usage: ${disk_usage}%"
log "=== Log Management Complete ==="
Best Practices
  • Schedule via cron during low-traffic hours
  • Always check if directories exist before processing
  • Use -print0 with read -d '' for filenames with spaces
  • Monitor disk usage proactively; don't wait for outages
Scenario 5

Service Health Monitor with Auto-Recovery

Situation
Your team manages several critical microservices. Last month, the payment API went down for 45 minutes before anyone noticed. You need a monitoring script that checks service health endpoints, attempts auto-restart on failure, and sends alerts via webhook.
Objective
Monitor multiple HTTP endpoints, detect failures with retry logic, attempt service restarts, and send notifications when services are down.
Solution
#!/bin/bash
# health_monitor.sh - Service Health Monitor
# Usage: ./health_monitor.sh (run via cron every 1-5 min)

set -uo pipefail

# Service definitions: "name|url|service_name"
SERVICES=(
    "Payment API|http://localhost:8001/health|payment-api"
    "Auth Service|http://localhost:8002/health|auth-service"
    "Web Frontend|http://localhost:3000|nginx"
)

MAX_RETRIES=3
TIMEOUT=5
WEBHOOK_URL="${SLACK_WEBHOOK:-}"
LOG="/var/log/health_monitor.log"

log() { echo "[$(date '+%F %T')] $1" | tee -a "$LOG"; }

send_alert() {
    local msg="$1"
    log "ALERT: $msg"
    if [[ -n "$WEBHOOK_URL" ]]; then
        curl -sf -X POST "$WEBHOOK_URL" \
            -H 'Content-Type: application/json' \
            -d "{\"text\":\"ALERT: $msg\"}" &>/dev/null || true
    fi
}

check_service() {
    local name="$1" url="$2" svc="$3"
    local attempt=1

    while (( attempt <= MAX_RETRIES )); do
        status=$(curl -sf -o /dev/null -w "%{http_code}" \
            --max-time "$TIMEOUT" "$url" 2>/dev/null || echo "000")

        if [[ "$status" == "200" ]]; then
            log "$name: UP (attempt $attempt)"
            return 0
        fi
        log "$name: attempt $attempt failed (HTTP $status)"
        ((attempt++))
        sleep 2
    done

    # All retries failed; attempt restart
    log "$name: DOWN, attempting restart of $svc"
    if systemctl restart "$svc" 2>/dev/null; then
        send_alert "$name was down, auto-restarted $svc"
    else
        send_alert "$name is DOWN and restart FAILED! Manual intervention needed."
    fi
    return 1
}

log "=== Health Check Started ==="
failures=0

for entry in "${SERVICES[@]}"; do
    IFS='|' read -r name url svc <<< "$entry"
    check_service "$name" "$url" "$svc" || ((failures++))
done

log "=== Health Check Complete: $failures failures ==="
Best Practices
  • Implement retry logic before declaring a service down
  • Use timeouts on all HTTP requests to prevent hanging
  • Log all actions for post-incident analysis
  • Set up escalation: auto-restart first, then alert humans if that fails
Scenario 6

Firewall Rule Auditor

Situation
During a security audit, you need to review the firewall configuration on 20+ Linux servers. You need to identify dangerous rules like open SSH from any source, permissive ACCEPT policies, or rules that expose sensitive ports to the internet.
Objective
Parse iptables/ufw rules, flag dangerous configurations (wide-open ports, ANY source rules, missing DROP policies), and generate a security report.
Solution
#!/bin/bash
# firewall_audit.sh - Firewall Rule Auditor
# Usage: ./firewall_audit.sh

set -uo pipefail

REPORT="/tmp/firewall_audit_$(date +%Y%m%d).txt"
DANGEROUS_PORTS=(22 3306 5432 6379 27017 9200)
ISSUES=0

header() { echo -e "\n$1" | tee -a "$REPORT"; }
finding() { echo "  [WARN] $1" | tee -a "$REPORT"; ((ISSUES++)); }
ok() { echo "  [OK] $1" | tee -a "$REPORT"; }

echo "Firewall Security Audit, $(date)" | tee "$REPORT"
echo "Host: $(hostname)" | tee -a "$REPORT"

# Check if firewall is active
header "1. Firewall Status"
if command -v ufw &>/dev/null; then
    status=$(ufw status 2>/dev/null | head -1)
    if [[ "$status" == *"inactive"* ]]; then
        finding "UFW is INACTIVE, no firewall protection!"
    else
        ok "UFW is active"
    fi
fi

# Check default policies
header "2. Default Policies"
iptables -L -n 2>/dev/null | grep -E "^Chain (INPUT|FORWARD)" | \
while read -r line; do
    chain=$(echo "$line" | awk '{print $2}')
    policy=$(echo "$line" | grep -oP '\(policy \K\w+')
    if [[ "$policy" == "ACCEPT" ]]; then
        finding "$chain default policy is ACCEPT (should be DROP)"
    else
        ok "$chain default policy is $policy"
    fi
done

# Check for wide-open rules
header "3. Wide-Open Rules (source: 0.0.0.0/0)"
iptables -L INPUT -n 2>/dev/null | grep "ACCEPT" | \
while read -r rule; do
    for port in "${DANGEROUS_PORTS[@]}"; do
        if echo "$rule" | grep -qE "dpt:$port.*0\.0\.0\.0/0|0\.0\.0\.0/0.*dpt:$port"; then
            finding "Port $port is open to ANY source: $rule"
        fi
    done
done

# Check for rules without source restriction
header "4. Unrestricted ACCEPT Rules"
iptables -L INPUT -n --line-numbers 2>/dev/null | \
    grep "ACCEPT" | grep "0.0.0.0/0.*0.0.0.0/0" | \
while read -r rule; do
    finding "Unrestricted ACCEPT: $rule"
done

header "=== Audit Complete ==="
echo "Total issues found: $ISSUES" | tee -a "$REPORT"
echo "Report saved: $REPORT" | tee -a "$REPORT"
Best Practices
  • Default INPUT policy should be DROP, not ACCEPT
  • Never expose database ports (3306, 5432, 6379) to 0.0.0.0/0
  • Restrict SSH access to specific IP ranges or use a VPN
  • Run firewall audits regularly as part of compliance checks
Scenario 7

User Account Security Auditor

Situation
As part of quarterly access reviews (required by ISO 27001 and SOC 2), you need to audit all user accounts on Linux servers. You must identify accounts with no password, UID 0 (root-equivalent) accounts, users who haven't logged in for 90+ days, and accounts with shells that shouldn't have them.
Objective
Scan /etc/passwd, /etc/shadow, and login records to identify security risks in user accounts and generate an actionable report.
Solution
#!/bin/bash
# user_audit.sh - User Account Security Auditor
# Usage: sudo ./user_audit.sh

set -uo pipefail

[[ $EUID -eq 0 ]] || { echo "Run as root"; exit 1; }

REPORT="/tmp/user_audit_$(date +%Y%m%d).txt"
STALE_DAYS=90
ISSUES=0

log() { echo "$1" | tee -a "$REPORT"; }
warn() { echo "  [WARN] $1" | tee -a "$REPORT"; ((ISSUES++)); }
ok() { echo "  [OK] $1" | tee -a "$REPORT"; }

log "User Account Audit, $(date)"
log "Host: $(hostname)"
log ""

# Check for UID 0 accounts (root equivalents)
log "=== UID 0 Accounts (Root Equivalents) ==="
while IFS=: read -r user _ uid _; do
    if [[ "$uid" == "0" && "$user" != "root" ]]; then
        warn "$user has UID 0, root-equivalent access!"
    fi
done < /etc/passwd

# Check for accounts with no password
log ""
log "=== Password Status ==="
while IFS=: read -r user pass _; do
    case "$pass" in
        "")  warn "$user has NO password set" ;;
        "!"|"!!"|"*") ;; # Locked or system account
        *)   ok "$user has a password set" ;;
    esac
done < /etc/shadow

# Check for stale accounts
log ""
log "=== Stale Accounts (no login in ${STALE_DAYS}+ days) ==="
cutoff=$(date -d "-${STALE_DAYS} days" +%s 2>/dev/null || \
    date -v-${STALE_DAYS}d +%s)
while IFS=: read -r user _ uid _ _ _ shell; do
    (( uid < 1000 )) && continue  # Skip system accounts
    [[ "$shell" == */nologin || "$shell" == */false ]] && continue
    last_login=$(lastlog -u "$user" 2>/dev/null | tail -1 | awk '{print $4,$5,$6,$7}')
    if [[ "$last_login" == *"Never"* ]]; then
        warn "$user has NEVER logged in (shell: $shell)"
    fi
done < /etc/passwd

log ""
log "=== Audit Complete: $ISSUES issues found ==="
log "Report: $REPORT"
Best Practices
  • Run account audits quarterly for compliance frameworks
  • Disable accounts that haven't been used in 90+ days
  • Only root should have UID 0; any other UID 0 account is a red flag
  • Service accounts should use /sbin/nologin as their shell
Scenario 8

SSL Certificate Expiry Checker

Situation
Your organization manages 50+ domains and subdomains. Last quarter, a production API certificate expired causing a major incident. You need proactive monitoring that checks certificate expiry dates and alerts the team before any cert expires.
Objective
Connect to multiple domains via OpenSSL, extract certificate expiry dates, calculate days remaining, and flag certificates expiring within a threshold.
Solution
#!/bin/bash
# ssl_checker.sh - SSL Certificate Expiry Checker
# Usage: ./ssl_checker.sh [warn_days]

set -uo pipefail

WARN_DAYS="${1:-30}"
DOMAINS=(
    "example.com"
    "api.example.com"
    "mail.example.com"
    "staging.example.com"
)

RED='\033[0;31m' YEL='\033[0;33m'
GRN='\033[0;32m' NC='\033[0m'

echo "--------------------------------------------"
echo "  SSL Certificate Expiry Report"
echo "  Warning threshold: $WARN_DAYS days"
echo "  Checked: $(date)"
echo "--------------------------------------------"
echo ""

expiring=0

for domain in "${DOMAINS[@]}"; do
    # Get certificate expiry date
    expiry=$(echo | openssl s_client -servername "$domain" \
        -connect "$domain:443" 2>/dev/null \
        | openssl x509 -noout -enddate 2>/dev/null \
        | cut -d= -f2)

    if [[ -z "$expiry" ]]; then
        printf "${RED}  [ERROR] %-30s UNREACHABLE${NC}\n" "$domain"
        ((expiring++))
        continue
    fi

    # Calculate days until expiry
    expiry_epoch=$(date -d "$expiry" +%s 2>/dev/null || \
        date -jf "%b %d %T %Y %Z" "$expiry" +%s)
    now_epoch=$(date +%s)
    days_left=$(( (expiry_epoch - now_epoch) / 86400 ))

    if (( days_left <= 0 )); then
        printf "${RED}  [ERROR] %-30s EXPIRED (%d days ago)${NC}\n" \
            "$domain" "$(( -days_left ))"
        ((expiring++))
    elif (( days_left <= WARN_DAYS )); then
        printf "${YEL}  [WARN] %-30s EXPIRING in %d days${NC}\n" \
            "$domain" "$days_left"
        ((expiring++))
    else
        printf "${GRN}  [OK] %-30s OK (%d days remaining)${NC}\n" \
            "$domain" "$days_left"
    fi
done

echo ""
echo "--------------------------------------------"
if (( expiring > 0 )); then
    echo -e "${RED}  $expiring certificate(s) need attention!${NC}"
else
    echo -e "${GRN}  All certificates healthy [OK]${NC}"
fi
echo "--------------------------------------------"
Best Practices
  • Check certificates daily via cron and alert at 30-, 14-, and 7-day marks
  • Automate renewal with Let's Encrypt / certbot where possible
  • Include internal services and APIs, not just public-facing domains
  • Always test with openssl s_client; browser checks aren't enough
Scenario 9

Incident Response Data Collector

Situation
A security breach has been detected on a Linux server. The incident response team needs to quickly collect volatile forensic evidence before the attacker can cover their tracks: running processes, network connections, logged-in users, recent file changes, and system info.
Objective
Gather volatile forensic data from a compromised system, hash all evidence files for chain-of-custody integrity, and package everything for analysis.
Solution
#!/bin/bash
# ir_collector.sh - Incident Response Data Collector
# Usage: sudo ./ir_collector.sh

set -uo pipefail

[[ $EUID -eq 0 ]] || { echo "Must run as root!"; exit 1; }

CASE_ID="IR-$(date +%Y%m%d-%H%M%S)"
EVIDENCE_DIR="/tmp/${CASE_ID}"
mkdir -p "$EVIDENCE_DIR"

collect() {
    local name="$1" desc="$2"
    shift 2
    echo "  Collecting: $desc..."
    "$@" > "${EVIDENCE_DIR}/${name}.txt" 2>&1 || true
}

echo "----------------------------------------"
echo "Incident Response Data Collector"
echo "Case: $CASE_ID"
echo "----------------------------------------"
echo ""

# System information
collect "01_hostname"    "Hostname & OS info"   hostnamectl
collect "02_uptime"      "System uptime"        uptime
collect "03_uname"       "Kernel information"   uname -a
collect "04_date"        "Current timestamp"    date -u '+%Y-%m-%d %H:%M:%S UTC'

# User activity
collect "10_who"         "Currently logged users" who -a
collect "11_last"        "Recent logins"        last -n 50
collect "12_lastb"       "Failed logins"        lastb -n 50
collect "13_auth_log"    "Auth log (last 500)"  tail -n 500 /var/log/auth.log

# Process information
collect "20_processes"   "Running processes"    ps auxww
collect "21_pstree"      "Process tree"         pstree -pa

# Network state
collect "30_connections" "Network connections"  ss -tulpn
collect "31_established" "Established conns"    ss -tnp state established
collect "32_routes"      "Routing table"        ip route
collect "33_arp"         "ARP cache"            ip neigh
collect "34_dns"         "DNS config"           cat /etc/resolv.conf
collect "35_iptables"    "Firewall rules"       iptables -L -n -v

# File system
collect "40_mounts"      "Mounted filesystems"  mount
collect "41_recent_files" "Files modified <24h" \
    find /tmp /var/tmp /dev/shm -mtime -1 -ls

# Scheduled tasks
collect "50_crontab"     "Root crontab"         crontab -l
collect "51_cron_dirs"   "Cron directories"     ls -la /etc/cron.*

echo ""
echo "Generating checksums for chain of custody..."
cd "$EVIDENCE_DIR"
sha256sum *.txt > checksums.sha256
echo "  [OK] $(wc -l < checksums.sha256) files hashed"

# Package evidence
tar -czf "/tmp/${CASE_ID}.tar.gz" -C /tmp "$CASE_ID"
echo ""
echo "Evidence package: /tmp/${CASE_ID}.tar.gz"
echo "SHA256: $(sha256sum "/tmp/${CASE_ID}.tar.gz" | cut -d' ' -f1)"
echo ""
echo "=== Collection Complete ==="
Best Practices
  • Collect volatile data first (memory, processes, connections) before disk data
  • Never run tools from the compromised system; use trusted binaries from a USB
  • Hash everything for chain-of-custody and legal admissibility
  • Document your actions with timestamps; you may need to testify
Scenario 10

CI/CD Deployment Script with Rollback

Situation
Your team deploys a web application multiple times per day. Deployments sometimes fail silently, leaving the app in a broken state. You need a deployment script with pre-flight checks, health verification post-deploy, and automatic rollback if the new version is unhealthy.
Objective
Build a deployment pipeline that performs pre-deploy validation, deploys the new version, runs health checks, and automatically rolls back to the previous version if the deploy is unhealthy.
Solution
#!/bin/bash
# deploy.sh - CI/CD Deployment with Rollback
# Usage: ./deploy.sh <version>

set -uo pipefail

VERSION="${1:?Usage: $0 <version>}"
APP_DIR="/var/www/app"
BACKUP_DIR="/var/www/backups"
HEALTH_URL="http://localhost:3000/health"
MAX_HEALTH_CHECKS=10
LOG="/var/log/deploy.log"

log() { echo "[$(date '+%F %T')] $1" | tee -a "$LOG"; }
fail() { log "FATAL: $1"; exit 1; }

# Pre-flight Checks
log "=== Deploying version $VERSION ==="

[[ -d "$APP_DIR" ]] || fail "App directory not found"
[[ -f "releases/${VERSION}.tar.gz" ]] || fail "Release artifact not found"

# Backup Current Version
current_ver=$(cat "$APP_DIR/VERSION" 2>/dev/null || echo "unknown")
log "Backing up current version: $current_ver"
mkdir -p "$BACKUP_DIR"
cp -a "$APP_DIR" "${BACKUP_DIR}/rollback_${current_ver}_$(date +%s)"

# Deploy New Version
log "Deploying version $VERSION..."
tar -xzf "releases/${VERSION}.tar.gz" -C "$APP_DIR" --strip-components=1
echo "$VERSION" > "$APP_DIR/VERSION"

# Restart application
log "Restarting application..."
systemctl restart app 2>/dev/null || pm2 restart app 2>/dev/null

# Health Check
log "Running health checks..."
healthy=false
for ((i=1; i<=MAX_HEALTH_CHECKS; i++)); do
    sleep 3
    status=$(curl -sf -o /dev/null -w "%{http_code}" \
        --max-time 5 "$HEALTH_URL" 2>/dev/null || echo "000")

    if [[ "$status" == "200" ]]; then
        healthy=true
        log "Health check $i/$MAX_HEALTH_CHECKS: PASS"
        break
    fi
    log "Health check $i/$MAX_HEALTH_CHECKS: FAIL (HTTP $status)"
done

if $healthy; then
    log "[OK] Deployment successful! Version $VERSION is live."
    # Clean old backups (keep last 5)
    ls -dt "${BACKUP_DIR}"/rollback_* 2>/dev/null | \
        tail -n +6 | xargs rm -rf
    exit 0
fi

# Rollback
log "[ERROR] Health checks failed, initiating rollback."
latest_backup=$(ls -dt "${BACKUP_DIR}"/rollback_* | head -1)
if [[ -d "$latest_backup" ]]; then
    rm -rf "$APP_DIR"
    cp -a "$latest_backup" "$APP_DIR"
    systemctl restart app 2>/dev/null || pm2 restart app 2>/dev/null
    log "Rolled back to: $current_ver"
else
    fail "No backup found for rollback!"
fi

exit 1
Best Practices
  • Always backup before deploying; never skip this step
  • Implement health checks with retry logic after every deployment
  • Automatic rollback prevents extended outages from bad deploys
  • Keep deployment logs for audit and debugging purposes

Real Use Cases

The scripts above map directly to real responsibilities in cybersecurity and DevOps roles:

SOC Analyst

Automate log monitoring, detect anomalies, collect incident response data, and generate security reports.

Penetration Tester

Build lightweight recon tools for restricted environments where standard toolkits aren't available.

DevOps Engineer

Automate deployments with health checks and rollback, manage infrastructure, and monitor services.

Compliance Officer

Audit user accounts, firewall rules, and SSL certificates for ISO 27001 and SOC 2 compliance.

SRE / Platform Engineer

Build self-healing systems with automated health monitoring, log rotation, and backup verification.

Cybersecurity Student

Practice real-world scripting challenges that mirror actual job responsibilities and interview questions.

Practice complete. You've completed all 10 shell scripting practice scenarios! These skills are directly applicable to cybersecurity operations, DevOps pipelines, and system administration. Keep practicing by modifying these scripts for your own environment.