277 lines
9.6 KiB
YAML
277 lines
9.6 KiB
YAML
---
|
|
# Cron Audit Playbook
|
|
# Inventories all scheduled tasks across every host and flags basic security concerns.
|
|
# Covers /etc/crontab, /etc/cron.d/, /etc/cron.{hourly,daily,weekly,monthly},
|
|
# user crontab spools, and systemd timers.
|
|
# Usage: ansible-playbook playbooks/cron_audit.yml
|
|
# Usage: ansible-playbook playbooks/cron_audit.yml -e "host_target=rpi"
|
|
|
|
- name: Cron Audit — Scheduled Task Inventory
|
|
hosts: "{{ host_target | default('active') }}"
|
|
gather_facts: yes
|
|
ignore_unreachable: true
|
|
|
|
vars:
|
|
report_dir: "/tmp/cron_audit"
|
|
|
|
tasks:
|
|
|
|
# ---------- Setup ----------
|
|
|
|
- name: Create cron audit report directory
|
|
ansible.builtin.file:
|
|
path: "{{ report_dir }}"
|
|
state: directory
|
|
mode: '0755'
|
|
delegate_to: localhost
|
|
run_once: true
|
|
|
|
# ---------- /etc/crontab ----------
|
|
|
|
- name: Read /etc/crontab
|
|
ansible.builtin.shell: cat /etc/crontab 2>/dev/null || echo "(not present)"
|
|
register: etc_crontab
|
|
changed_when: false
|
|
failed_when: false
|
|
|
|
# ---------- /etc/cron.d/ ----------
|
|
|
|
- name: Read /etc/cron.d/ entries
|
|
ansible.builtin.shell: |
|
|
if [ -d /etc/cron.d ] && [ -n "$(ls /etc/cron.d/ 2>/dev/null)" ]; then
|
|
for f in /etc/cron.d/*; do
|
|
[ -f "$f" ] || continue
|
|
echo "=== $f ==="
|
|
cat "$f" 2>/dev/null
|
|
echo ""
|
|
done
|
|
else
|
|
echo "(not present or empty)"
|
|
fi
|
|
register: cron_d_entries
|
|
changed_when: false
|
|
failed_when: false
|
|
|
|
# ---------- /etc/cron.{hourly,daily,weekly,monthly} ----------
|
|
|
|
- name: Read /etc/cron.{hourly,daily,weekly,monthly} script names
|
|
ansible.builtin.shell: |
|
|
for dir in hourly daily weekly monthly; do
|
|
path="/etc/cron.$dir"
|
|
if [ -d "$path" ]; then
|
|
echo "=== $path ==="
|
|
ls "$path" 2>/dev/null || echo "(empty)"
|
|
echo ""
|
|
fi
|
|
done
|
|
if [ ! -d /etc/cron.hourly ] && [ ! -d /etc/cron.daily ] && \
|
|
[ ! -d /etc/cron.weekly ] && [ ! -d /etc/cron.monthly ]; then
|
|
echo "(no cron period directories present)"
|
|
fi
|
|
register: cron_period_dirs
|
|
changed_when: false
|
|
failed_when: false
|
|
|
|
# ---------- List users with crontabs ----------
|
|
|
|
- name: List users with crontabs
|
|
ansible.builtin.shell: |
|
|
# Debian/Ubuntu path
|
|
if [ -d /var/spool/cron/crontabs ]; then
|
|
spool_dir="/var/spool/cron/crontabs"
|
|
elif [ -d /var/spool/cron ]; then
|
|
spool_dir="/var/spool/cron"
|
|
else
|
|
echo "(no crontab spool directory found)"
|
|
exit 0
|
|
fi
|
|
files=$(ls "$spool_dir" 2>/dev/null)
|
|
if [ -z "$files" ]; then
|
|
echo "(no user crontabs found in $spool_dir)"
|
|
else
|
|
echo "$files"
|
|
fi
|
|
register: crontab_users
|
|
changed_when: false
|
|
failed_when: false
|
|
|
|
# ---------- Dump user crontab contents ----------
|
|
|
|
- name: Dump user crontab contents
|
|
ansible.builtin.shell: |
|
|
# Debian/Ubuntu path
|
|
if [ -d /var/spool/cron/crontabs ]; then
|
|
spool_dir="/var/spool/cron/crontabs"
|
|
elif [ -d /var/spool/cron ]; then
|
|
spool_dir="/var/spool/cron"
|
|
else
|
|
echo "(no crontab spool directory found)"
|
|
exit 0
|
|
fi
|
|
found=0
|
|
for f in "$spool_dir"/*; do
|
|
[ -f "$f" ] || continue
|
|
found=1
|
|
echo "=== $f ==="
|
|
cat "$f" 2>/dev/null || echo "(unreadable)"
|
|
echo ""
|
|
done
|
|
if [ "$found" -eq 0 ]; then
|
|
echo "(no user crontab files found)"
|
|
fi
|
|
register: crontab_contents
|
|
changed_when: false
|
|
failed_when: false
|
|
|
|
# ---------- Systemd timers ----------
|
|
|
|
- name: List systemd timers
|
|
ansible.builtin.shell: |
|
|
if command -v systemctl >/dev/null 2>&1; then
|
|
systemctl list-timers --all --no-pager 2>/dev/null
|
|
else
|
|
echo "(not a systemd host)"
|
|
fi
|
|
register: systemd_timers
|
|
changed_when: false
|
|
failed_when: false
|
|
|
|
# ---------- Security flag: REDACTED_APP_PASSWORD world-writable paths ----------
|
|
|
|
- name: Security flag - REDACTED_APP_PASSWORD world-writable path references
|
|
ansible.builtin.shell: |
|
|
flagged=""
|
|
|
|
# Collect root cron entries from /etc/crontab
|
|
if [ -f /etc/crontab ]; then
|
|
while IFS= read -r line; do
|
|
# Skip comments, empty lines, and variable assignment lines (e.g. MAILTO="")
|
|
case "$line" in
|
|
'#'*|''|*'='*) continue ;;
|
|
esac
|
|
# Lines where 6th field indicates root user (field 6) — format: min hr dom mon dow user cmd
|
|
user=$(echo "$line" | awk '{print $6}')
|
|
if [ "$user" = "root" ]; then
|
|
cmd=$(echo "$line" | awk '{for(i=7;i<=NF;i++) printf "%s ", $i; print ""}')
|
|
bin=$(echo "$cmd" | awk '{print $1}')
|
|
if [ -n "$bin" ] && [ -f "$bin" ]; then
|
|
if [ "$(find "$bin" -maxdepth 0 -perm -002 2>/dev/null)" = "$bin" ]; then
|
|
flagged="$flagged\nFLAGGED: /etc/crontab root job uses world-writable binary: $bin"
|
|
fi
|
|
fi
|
|
fi
|
|
done < /etc/crontab
|
|
fi
|
|
|
|
# Collect root cron entries from /etc/cron.d/*
|
|
if [ -d /etc/cron.d ]; then
|
|
for f in /etc/cron.d/*; do
|
|
[ -f "$f" ] || continue
|
|
while IFS= read -r line; do
|
|
case "$line" in
|
|
'#'*|''|*'='*) continue ;;
|
|
esac
|
|
user=$(echo "$line" | awk '{print $6}')
|
|
if [ "$user" = "root" ]; then
|
|
cmd=$(echo "$line" | awk '{for(i=7;i<=NF;i++) printf "%s ", $i; print ""}')
|
|
bin=$(echo "$cmd" | awk '{print $1}')
|
|
if [ -n "$bin" ] && [ -f "$bin" ]; then
|
|
if [ "$(find "$bin" -maxdepth 0 -perm -002 2>/dev/null)" = "$bin" ]; then
|
|
flagged="$flagged\nFLAGGED: $f root job uses world-writable binary: $bin"
|
|
fi
|
|
fi
|
|
fi
|
|
done < "$f"
|
|
done
|
|
fi
|
|
|
|
# Collect root crontab from spool
|
|
for spool in /var/spool/cron/crontabs/root /var/spool/cron/root; do
|
|
if [ -f "$spool" ]; then
|
|
while IFS= read -r line; do
|
|
case "$line" in
|
|
'#'*|'') continue ;;
|
|
esac
|
|
# User crontab format: min hr dom mon dow cmd (no user field)
|
|
cmd=$(echo "$line" | awk '{for(i=6;i<=NF;i++) printf "%s ", $i; print ""}')
|
|
bin=$(echo "$cmd" | awk '{print $1}')
|
|
if [ -n "$bin" ] && [ -f "$bin" ]; then
|
|
if [ "$(find "$bin" -maxdepth 0 -perm -002 2>/dev/null)" = "$bin" ]; then
|
|
flagged="$flagged\nFLAGGED: $spool job uses world-writable binary: $bin"
|
|
fi
|
|
fi
|
|
done < "$spool"
|
|
fi
|
|
done
|
|
|
|
# Check /etc/cron.{hourly,daily,weekly,monthly} scripts (run as root by run-parts)
|
|
for dir in /etc/cron.hourly /etc/cron.daily /etc/cron.weekly /etc/cron.monthly; do
|
|
[ -d "$dir" ] || continue
|
|
for f in "$dir"/*; do
|
|
[ -f "$f" ] || continue
|
|
if [ "$(find "$f" -maxdepth 0 -perm -002 2>/dev/null)" = "$f" ]; then
|
|
flagged="${flagged}\nFLAGGED: $f (run-parts cron dir) is world-writable"
|
|
fi
|
|
done
|
|
done
|
|
|
|
if [ -z "$flagged" ]; then
|
|
echo "No world-writable cron script paths found"
|
|
else
|
|
printf "%b\n" "$flagged"
|
|
fi
|
|
register: security_flags
|
|
changed_when: false
|
|
failed_when: false
|
|
|
|
# ---------- Per-host summary ----------
|
|
|
|
- name: Per-host cron audit summary
|
|
ansible.builtin.debug:
|
|
msg: |
|
|
==========================================
|
|
CRON AUDIT SUMMARY: {{ inventory_hostname }}
|
|
==========================================
|
|
|
|
=== /etc/crontab ===
|
|
{{ etc_crontab.stdout | default('(not collected)') }}
|
|
|
|
=== /etc/cron.d/ ===
|
|
{{ cron_d_entries.stdout | default('(not collected)') }}
|
|
|
|
=== Cron Period Directories ===
|
|
{{ cron_period_dirs.stdout | default('(not collected)') }}
|
|
|
|
=== Users with Crontabs ===
|
|
{{ crontab_users.stdout | default('(not collected)') }}
|
|
|
|
=== User Crontab Contents ===
|
|
{{ crontab_contents.stdout | default('(not collected)') }}
|
|
|
|
=== Systemd Timers ===
|
|
{{ systemd_timers.stdout | default('(not collected)') }}
|
|
|
|
=== Security Flags ===
|
|
{{ security_flags.stdout | default('(not collected)') }}
|
|
|
|
==========================================
|
|
|
|
# ---------- Per-host JSON report ----------
|
|
|
|
- name: Write per-host JSON cron audit report
|
|
ansible.builtin.copy:
|
|
content: "{{ {
|
|
'timestamp': ansible_date_time.iso8601,
|
|
'hostname': inventory_hostname,
|
|
'etc_crontab': etc_crontab.stdout | default('') | trim,
|
|
'cron_d_entries': cron_d_entries.stdout | default('') | trim,
|
|
'cron_period_dirs': cron_period_dirs.stdout | default('') | trim,
|
|
'crontab_users': crontab_users.stdout | default('') | trim,
|
|
'crontab_contents': crontab_contents.stdout | default('') | trim,
|
|
'systemd_timers': systemd_timers.stdout | default('') | trim,
|
|
'security_flags': security_flags.stdout | default('') | trim
|
|
} | to_nice_json }}"
|
|
dest: "{{ report_dir }}/{{ inventory_hostname }}_{{ ansible_date_time.date }}.json"
|
|
delegate_to: localhost
|
|
changed_when: false
|