Sanitized mirror from private repository - 2026-04-06 09:21:56 UTC
This commit is contained in:
276
ansible/automation/playbooks/cron_audit.yml
Normal file
276
ansible/automation/playbooks/cron_audit.yml
Normal file
@@ -0,0 +1,276 @@
|
||||
---
|
||||
# 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
|
||||
Reference in New Issue
Block a user