---
# 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
