---
# Task file for CIS Controls
# This file is commented to help view what Ansible Automation is doing
#  and under what circumstances.

# Some blocks below have tasks with tags and some without. Blocks of tasks that
#  contain multiple controls have tasks with tags. Blocks that consist of a
#  single control and are just put together for convience sake, do not have
#  sub-block tasks with tags.

# Comments about how the modules are used will become more infrequent as
#  the file goes along to avoid repeating oneself.

# Let the user know what version of the controls file is running
# Use a variable so it prints out the correct version.
- name: Print Header
  ansible.builtin.debug:
    msg: "CIS Controls for {{ ansible_distribution }} {{ ansible_distribution_major_version }}"

# Collect the packages installed on the system so we can check agains them later
- name: Collect package list
  ansible.builtin.package_facts:
    manager: auto
  tags:
    - always

# Find the minimum UID of the machine for normal acocunts. This varies
#  between machines and environments, so we pull it from the file it
#  is supposed to exist in.
- name: Determine the Minimum UID for new, non-system, accounts
  ansible.builtin.command: "/usr/bin/awk '/^s*UID_MIN/{print $2}' /etc/login.defs"
  register: min_uid
  changed_when: min_uid.rc == "2"
  check_mode: false
  tags:
    - always

# Update the system with security packages using the system's package manager
#  Only update the system if the 'update_system' variable is set to true
- name: 1.9.0 - Ensure updated system
  ansible.builtin.dnf:
    name: "*"
    state: latest
    security: true
  when: update_system
  tags:
    - 1.9.0

- name: 1.1.1.1 - disable squashfs
  ansible.builtin.blockinfile:
    path: /etc/modprobe.d/squashfs.conf
    create: true
    owner: root
    group: root
    mode: 644
    setype: modules_conf_t
    block: |
      install squashfs /bin/false
      blacklist squashfs
  tags:
    - 1.1.1.1

- name: 1.1.1.2 - disable udf
  ansible.builtin.blockinfile:
    path: /etc/modprobe.d/udf.conf
    create: true
    owner: root
    group: root
    mode: 0644
    setype: modules_conf_t
    block: |
      install udf /bin/false
      blacklist udf
  tags:
    - 1.1.1.2


# Create and configure the local-fs systemd service file
- name: 1.1.2 - Ensure /tmp is configured
  tags:
    - 1.1.2
  block:
    # Create a file to hold the system specific local-fs service information
    #  be sure to set the selinux security context. Even if selinux is disabled,
    #  it's a good idea to make sure it is set on files
    - name: 1.1.2.[2-5] - Ensure the local-fs directory is created
      ansible.builtin.file:
        path: /etc/systemd/system/local-fs.target.wants
        state: directory
        owner: root
        group: root
        mode: 0755
        setype: etc_t
      tags:
        - 1.1.2.1
        - 1.1.2.2
        - 1.1.2.3
        - 1.1.2.4

    # Add content to the file we created using the blockinfile command.
    # Notify systemd to reload its daemons and start the local-fs service
    - name: 1.1.[2-5] - Configure config file for tmpfs
      ansible.builtin.blockinfile:
        path: /etc/systemd/system/local-fs.target.wants/tmp.mount
        block: |
          [Mount]
          What=tmpfs
          Where=/tmp
          Type=tmpfs
          Options=mode=1777,strictatime,noexec,nodev,nosuid
        create: true
        owner: root
        group: root
        mode: 0644
      notify: Restart tmpfs
      tags:
        - 1.1.2.1
        - 1.1.2.2
        - 1.1.2.3
        - 1.1.2.4

# Determine if a filesystem is on a separate partition, if so, then
#  check to see if various filesystem options exist for the filesystem
- name: 1.1.3 - Configure /var
  tags:
    - 1.1.3
  block:
    # Create a empty integer variable and set it as a fact on the managed
    #  machine.
    - name: 1.1.3 - Set/reset /var mount counter
      ansible.builtin.set_fact:
        mount_count: 0

    # Examine the ansible_mounts variable which includes all of the system mounts
    #  on machine. Search for the appropriate mount information. If it exists,
    #  increment the integer variable by '1' and save the filesystems options to a
    #  new variable called mount_options.
    - name: 1.1.3.1 - Determine if /var is on a separate partition
      ansible.builtin.set_fact:
        mount_count: "addition{{ mount_count + 1 }}"
      when: item.mount == "/var"
      with_items:
        - "{{ ansible_mounts }}"
    # If the number in mount_count variable is > 0, then we found the mount. If not,
    #  then report to the user that the given filesystem was not on a separate partition.
    - name: 1.1.3.1 - Report to user if /var is not on a separate partition
      ansible.builtin.debug:
        msg: "FAILED CONTROL: /var is not on a separate partition"
      when: mount_count == 0
      changed_when: true
      tags:
        - 1.1.3.1

    - name: 1.1.3.[2-3] - /var mount option controls
      tags:
        - 1.1.3.2
        - 1.1.3.3
      when: mount_count != 0
      block:
        - name: 1.1.3.2 - Report to user if /var does not have nodev set
          ansible.builtin.debug:
            msg: "FAILED CONTROL: /var does not have nodev set"
          when: mount_options is defined and "nodev" not in mount_options
          changed_when: true
          tags:
            - 1.1.3.2

        # Look through the mount_options variable for the given filesystem option. if it is
        #  not found, or if the filesystem is not on a separate partition (therefore has no mount options)
        #  let the user know.
        - name: 1.1.3.3 Report to user if /var does not have nosuid set
          ansible.builtin.debug:
            msg: "FAILED CONTROL: /var does not have nosuid set"
          when: mount_options is defined and "nodsuid" not in mount_options
          changed_when: true
          tags:
            - 1.1.3.3

# Determine if a filesystem is on a separate partition, if so, then
#  check to see if various filesystem options exist for the filesystem
- name: 1.1.4.1 - Configure /var/tmp
  tags:
    - 1.1.4
  block:
    # Create a empty integer variable and set it as a fact on the managed
    #  machine.
    - name: 1.1.4 - Set/reset /var/tmp mount counter
      ansible.builtin.set_fact:
        mount_count: 0

    # Examine the ansible_mounts variable which includes all of the system mounts
    #  on machine. Search for the appropriate mount information. If it exists,
    #  increment the integer variable by '1' and save the filesystems options to a
    #  new variable called mount_options.
    - name: 1.1.4.1 - Determine if /var/tmp is on a separate partition
      ansible.builtin.set_fact:
        mount_count: "addition{{ mount_count + 1 }}"
        mount_options: "{{ item.options }}"
      when: item.mount == "/var/tmp"
      with_items:
        - "{{ ansible_mounts }}"
      tags:
        - 1.1.4.1

    # If the number in mount_count variable is > 0, then we found the mount. If not,
    #  then report to the user that the given filesystem was not on a separate partition.
    - name: 1.1.4.1 - Report to user if /var/tmp not on separate partition
      ansible.builtin.debug:
        msg: "FAILED CONTROL: /var/tmp is not on a separate partition. Skipping mount option checks"
      when: mount_count == 0
      changed_when: true
      tags:
        - 1.1.4.1

    - name: 1.1.4.[2-4] - /var/tmp mount option controls
      tags:
        - 1.1.4.2
        - 1.1.4.3
        - 1.1.4.4
      when: mount_count != 0
      block:
        # Look through the mount_options variable for the given filesystem option. if it is
        #  not found, or if the filesystem is not on a separate partition (therefore has no mount options)
        #  let the user know.
        - name: 1.1.4.2 - Report to user if /var/tmp does not have noexec set
          ansible.builtin.debug:
            msg: "FAILED CONTROL: /var/tmp/ does not have noexec set"
          when: mount_options is defined and "noexec" not in mount_options
          changed_when: true
          tags:
            - 1.1.4.2

        # Look through the mount_options variable for the given filesystem option. if it is
        #  not found, or if the filesystem is not on a separate partition (therefore has no mount options)
        #  let the user know.
        - name: 1.1.4.3 - Report to user if /var/tmp does not have nosuid set
          ansible.builtin.debug:
            msg: "FAILED CONTROL: /var/tmp/ does not have nosuid set"
          when: mount_options is defined and "nodsuid" not in mount_options
          changed_when: true
          tags:
            - 1.1.4.3

        # Look through the mount_options variable for the given filesystem option. if it is
        #  not found, or if the filesystem is not on a separate partition (therefore has no mount options)
        #  let the user know.
        - name: 1.1.4.4 - Report to user if /var/tmp does not have nodev set
          ansible.builtin.debug:
            msg: "FAILED CONTROL: /var/tmp/ does not have nodev set"
          when: mount_options is defined and "nodev" not in mount_options
          changed_when: true
          tags:
            - 1.1.4.4

# Determine if a filesystem is on a separate partition, if so, then
#  check to see if various filesystem options exist for the filesystem
- name: 1.1.5 - Configure /var/log
  tags:
    - 1.1.5
  block:
    # Create a empty integer variable and set it as a fact on the managed
    #  machine.
    - name: 1.1.5 - Set/reset /var/log mount counter
      ansible.builtin.set_fact:
        mount_count: 0

    # Examine the ansible_mounts variable which includes all of the system mounts
    #  on machine. Search for the appropriate mount information. If it exists,
    #  increment the integer variable by '1' and save the filesystems options to a
    #  new variable called mount_options.
    - name: 1.1.5.1 - Determine if /var/log is on a separate partition
      ansible.builtin.set_fact:
        mount_count: "addition{{ mount_count + 1 }}"
      when: item.mount == "/var/log"
      with_items:
        - "{{ ansible_mounts }}"
      tags:
        - 1.1.5.1

    # If the number in mount_count variable is > 0, then we found the mount. If not,
    #  then report to the user that the given filesystem was not on a separate partition.
    - name: 1.1.5.1 - Report to user if /var/log is not on separate partition
      ansible.builtin.debug:
        msg: "FAILED CONTROL: /var/log is not on a separate partition"
      when: mount_count == 0
      changed_when: true
      tags:
        - 1.1.5.1

    - name: 1.1.5.[2-3] - /var/log mount option controls
      tags:
        - 1.1.5.2
        - 1.1.5.3
      when: mount_count != 0
      block:
        - name: 1.1.5.2 - Report to user if /var/log does not have nodev set
          ansible.builtin.debug:
            msg: "FAILED CONTROL: /var/log does not have nodev set"
          when: mount_options is defined and "nodev" not in mount_options
          changed_when: true
          tags:
            - 1.1.5.2

        # Look through the mount_options variable for the given filesystem option. if it is
        #  not found, or if the filesystem is not on a separate partition (therefore has no mount options)
        #  let the user know.
        - name: 1.1.5.3 Report to user if /var/log does not have nosuid set
          ansible.builtin.debug:
            msg: "FAILED CONTROL: /var/log does not have nosuid set"
          when: mount_options is defined and "nodsuid" not in mount_options
          changed_when: true
          tags:
            - 1.1.5.3

# Determine if a filesystem is on a separate partition, if so, then
#  check to see if various filesystem options exist for the filesystem
- name: 1.1.6 - Configure /var/log/audit
  tags:
    - 1.1.6
  block:
    # Create a empty integer variable and set it as a fact on the managed
    #  machine.
    - name: 1.1.6 - Set/reset /var/log/audit mount counter
      ansible.builtin.set_fact:
        mount_count: 0

    # Examine the ansible_mounts variable which includes all of the system mounts
    #  on machine. Search for the appropriate mount information. If it exists,
    #  increment the integer variable by '1' and save the filesystems options to a
    #  new variable called mount_options.
    - name: 1.1.6.1 - Determine if /var/log/audit is on a separate partition
      ansible.builtin.set_fact:
        mount_count: "addition{{ mount_count + 1 }}"
      when: item.mount == "/var/log/audit"
      with_items:
        - "{{ ansible_mounts }}"
      tags:
        - 1.1.6.1

    # If the number in mount_count variable is > 0, then we found the mount. If not,
    #  then report to the user that the given filesystem was not on a separate partition.
    - name: 1.1.6.1 - Report to user if /var/log/audit is not on a separate partition
      ansible.builtin.debug:
        msg: "FAILED CONTROL: /var/log/audit is not on a separate partition"
      when: mount_count == 0
      changed_when: true
      tags:
        - 1.1.6.1

    - name: 1.1.6.[2-4] - /var/log/audit mount option controls
      tags:
        - 1.1.5.2
        - 1.1.5.3
        - 1.1.5.4
      when: mount_count != 0
      block:
        # Look through the mount_options variable for the given filesystem option. if it is
        #  not found, or if the filesystem is not on a separate partition (therefore has no mount options)
        #  let the user know.
        - name: 1.1.6.2 - Report to user if /var/log/audit does not have noexec set
          ansible.builtin.debug:
            msg: "FAILED CONTROL: /var/log/audit/ does not have noexec set"
          when: mount_options is defined and "noexec" not in mount_options
          changed_when: true
          tags:
            - 1.1.6.2

        # Look through the mount_options variable for the given filesystem option. if it is
        #  not found, or if the filesystem is not on a separate partition (therefore has no mount options)
        #  let the user know.
        - name: 1.1.6.3 - Report to user if /var/log/audit does not have nodev set
          ansible.builtin.debug:
            msg: "FAILED CONTROL: /var/log/audit/ does not have nodev set"
          when: mount_options is defined and "nodev" not in mount_options
          changed_when: true
          tags:
            - 1.1.6.3

        # Look through the mount_options variable for the given filesystem option. if it is
        #  not found, or if the filesystem is not on a separate partition (therefore has no mount options)
        #  let the user know.
        - name: 1.1.6.4 - Report to user if /var/log/audit does not have nosuid set
          ansible.builtin.debug:
            msg: "FAILED CONTROL: /var/log/audit/ does not have nosuid set"
          when: mount_options is defined and "nodsuid" not in mount_options
          changed_when: true
          tags:
            - 1.1.6.4

# Determine if a filesystem is on a separate partition, if so, then
#  check to see if various filesystem options exist for the filesystem
- name: 1.1.7 - Configure /home
  tags:
    - 1.1.7
  block:
    # Create a empty integer variable and set it as a fact on the managed
    #  machine.
    - name: 1.1.7 - Set/reset /home mount counter
      ansible.builtin.set_fact:
        mount_count: 0

    # Examine the ansible_mounts variable which includes all of the system mounts
    #  on machine. Search for the appropriate mount information. If it exists,
    #  increment the integer variable by '1' and save the filesystems options to a
    #  new variable called mount_options.
    - name: 1.1.7.1 - Determine if /home is on a separate partition
      ansible.builtin.set_fact:
        mount_count: "addition{{ mount_count + 1 }}"
        mount_options: "{{ item.options }}"
      when: item.mount == "/home"
      with_items:
        - "{{ ansible_mounts }}"
      tags:
        - 1.1.7.1

    # If the number in mount_count variable is > 0, then we found the mount. If not,
    #  then report to the user that the given filesystem was not on a separate partition.
    - name: 1.1.7.1 - Report to user if /home is not on a separate partition
      ansible.builtin.debug:
        msg: "FAILED CONTROL: /home is not on a separate partition. Skipping mount option checks"
      when: mount_count == 0
      changed_when: true
      tags:
        - 1.1.7.1

    - name: 1.1.7.[2-3] - /home mount option controls
      tags:
        - 1.1.7.2
        - 1.1.7.3
      when: mount_count != 0
      block:
        # Look through the mount_options variable for the given filesystem option. if it is
        #  not found, or if the filesystem is not on a separate partition (therefore has no mount options)
        #  let the user know.
        - name: 1.1.7.2 - Report to user if /home does not have nodev set
          ansible.builtin.debug:
            msg: "FAILED CONTROL: /home does not have nodev set"
          when: mount_options is defined and "nodev" not in mount_options
          changed_when: true
          tags:
            - 1.1.7.2

        - name: 1.1.7.3 Report to user if /home does not have nosuid set
          ansible.builtin.debug:
            msg: "FAILED CONTROL: /home does not have nosuid set"
          when: mount_options is defined and "nodsuid" not in mount_options
          changed_when: true
          tags:
            - 1.1.7.3

# /dev/shm does not exist in ansible_mounts so we have to check the
#  mount command directly. This requires the use of the shell command which
#  is not ideal.
# Grep out /dev/shm and see if the given option is set.
- name: 1.1.8 - Configure /dev/shm
  tags:
    - 1.1.8
  block:
    - name: Determine if /dev/shm has nodev set
      ansible.builtin.shell: cat /proc/mounts | /usr/bin/grep /dev/shm | /usr/bin/grep -v nodev
      register: devshm_nodev_out
      failed_when: devshm_nodev_out == "2"
      changed_when: false
      check_mode: false

# Let the user know if we did not find the option set.
    - name: 1.1.8.2 - Report to user if /dev/shm does not have nodev set
      ansible.builtin.debug:
        msg: "FAILED CONTROL: /dev/shm does not have nodev set"
      when: devshm_nodev_out is defined and devshm_nodev_out.stdout
      changed_when: true
      tags:
        - 1.1.8.2

# Grep out /dev/shm and see if the given option is set.
- name: 1.1.8.4 - Report if /dev/shm does not have nosuid set
  tags:
    - 1.1.8.4
  block:
    - name: Determine if /dev/shm has nosuid set
      ansible.builtin.shell: cat /proc/mounts | /usr/bin/grep /dev/shm | /usr/bin/grep -v nosuid
      register: devshm_nosuid_out
      failed_when: devshm_nosuid_out == "2"
      changed_when: false
      check_mode: false

- name: 1.1.8.3 - Report if /dev/shm does not have noexec set
  tags:
    - 1.1.8.3
  block:
    - name: 1.1.8.3 - Determine if /dev/shm has noexec set
      ansible.builtin.shell: cat /proc/mounts | /usr/bin/grep /dev/shm | /usr/bin/grep -v noexec
      register: devshm_noexec_out
      failed_when: devshm_noexec_out == "2"
      changed_when: false
      check_mode: false
      tags:
        - 1.1.8.3

# Let the user know if we did not find the option set.
    - name: 1.1.8.3 - Report to user if /dev/shm does not have noexec set
      ansible.builtin.debug:
        msg: "FAILED CONTROL: /dev/shm does not have noexec set"
      when: devshm_noexec_out is defined and devshm_noexec_out.stdout
      changed_when: true
      tags:
        - 1.1.8.3
# Let the user know if we did not find the option set.
    - name: 1.1.8.2 - Report to user about /dev/shm
      ansible.builtin.debug:
        msg: "FAILED CONTROL: /dev/shm does not have nosuid set"
      when: devshm_nosuid_out is defined and devshm_nosuid_out.stdout
      changed_when: true

# Control 1.1.9 is for removable media

# Turn off and disable the autofs service using the service module.
# We check to see if the package that autofs belongs to (convienently called autofs)
#  exists in the ansible_facts.packages list we gathered early in the play
- name: 1.1.22 - disable automounting
  ansible.builtin.systemd:
    name: autofs
    enabled: false
    state: stopped
    masked: true
  when: "'autofs' in ansible_facts.packages"
  tags:
    - 1.1.22

- name: 1.1.23 - Disable USB storage module
  ansible.builtin.lineinfile:
    dest: /etc/modprobe.d/CIS.conf
    line: "install usb-storage /bin/true"
    state: present
    create: true
    owner: root
    group: root
    mode: 0644
  tags:
    - 1.1.23

# Control 1.2.1 is for checking GPG keys. The control just says that each repo should have the correct key

# GPGKeys are used to sign packages. enabling them will mean that all packages
#  from a given repo must be signed with the appropriate key
- name: 1.2.[3,4] - Ensure GPG keys are configured
  tags:
    - 1.2.3
  block:
    # Replace any instances of gpgcheck with a 1 after it to 'gpgcheck = 1'
    - name: 1.2.2 - set master dnf.conf gpgcheck to '1'
      ansible.builtin.replace:
        dest: /etc/dnf/dnf.conf
        regexp: '^gpgcheck\s*=\s*[^1]*$'
        replace: "gpgcheck=1"
      when: gpgcheck and ansible_distribution == "Fedora"

    # 1.2.4 - Ensure repo_gpgcheck is globally activated doesn't work on all repos so skipped. Environment dependant

    # Find all files in /etc/yum.repos.d and add them to a list variable
    - name: 1.2.4 - find all repo files in /etc/dnf.repos.d/
      ansible.builtin.find:
        paths: "/etc/dnf.repos.d"
        patterns: "*.repo"
      register: dnfrepos
      when: gpgcheck is defined and gpgcheck

    # parse the list variable and replace any instances of gpgcheck with a 1 after it to 'gpgcheck = 1'
    - name: 1.2.4 - Set all repos gpgchecks to '1'
      ansible.builtin.replace:
        dest: "{{ item.path }}"
        regexp: '^gpgcheck\s*=\s*[^1]*$'
        replace: gpgcheck = 1
      with_items: "{{ dnfrepos.files }}"
      when: gpgcheck is defined and gpgcheck

# This is out of order, but sudo is configured a lot through the other controls
- name: 5.3.1 - Ensure sudo is installed
  ansible.builtin.dnf:
    name: sudo
    state: present
  tags:
    - 5.3.1

# AIDE is a file system integrity checker which will document all
# filesystem changes. It's very noisy on busy systems and should be
# enabled when you have the sapce and need for it.
- name: 1.3 - Filesystem integrity checking w/AIDE
  tags:
    - 1.3.0
  block:
    # use the system package manager to install AIDE
    - name: 1.3.1 - Ensure aide is installed
      ansible.builtin.package:
        name: aide
        state: present
      tags:
        - 1.3.1

    # AIDE requires initialization the first time and it takes time on a large system.
    # DUse stat module on the file that should be there if it is set up.
    - name: 1.3.1 - Determine if AIDE has already been initialized
      ansible.builtin.stat:
        path: /var/lib/aide/aide.db.gz
      register: aide_path
      tags:
        - 1.3.1

    - name: 1.3.1 - Set up database file location
      ansible.builtin.replace:
        dest: /etc/aide.conf
        regexp: "^database=file:((?!{{ aide_db_name }}).)*$"
        replace: "database=file:{{ aide_db_name }}"
      tags:
        - 1.3.1

    - name: 1.3.1 - Set up database_out file location
      ansible.builtin.replace:
        dest: /etc/aide.conf
        regexp: "^database_out=file:((?!{{ aide_new_db_name }}).)*$"
        replace: "database_out=file:{{ aide_new_db_name }}"
      tags:
        - 1.3.1

    - name: 1.3.1 - enable gzip compression for database
      ansible.builtin.lineinfile:
        dest: /etc/aide.conf
        regexp: '^gzip_dbout\s*=\s*((?!{{ aide_gzip }}).)*$'
        line: "gzip_dbout={{ aide_gzip }}"
        state: present
      tags:
        - 1.3.1

    # stat returns a lot of information. 'exists' is true if the file exists and 'isreg'
    #  is true if the file is a regular file. If either of these are not true, then
    #  run the initializatoin again.
    - name: 1.3.1 - Initialize AIDE if it hasn't been already (/usr/sbin/aide)
      ansible.builtin.command: /usr/sbin/aide --init
      when: ( not aide_path.stat.exists or not aide_path.stat.isreg ) and ansible_distribution != "SLES"
      register: aide
      async: 1200 # 20 minutes until timeout
      poll: 0 # run concurrently
      tags:
        - 1.3.1

    - name: 1.3.1 - Wait for AIDE initialization to complete
      ansible.builtin.async_status:
        jid: "{{ aide.ansible_job_id }}"
      register: aide_status
      until: aide_status.finished
      when: ( not aide_path.stat.exists or not aide_path.stat.isreg ) and ansible_distribution != "SLES"
      retries: 300
      tags:
        - 1.3.1

    # AIDE creates the new database as a different name. Use the copy module with
    #  the remote_src argument to copy the file on the remote machine to another location
    #  on the remote machine.
    - name: 1.3.1 - Move the newly created database into place
      ansible.builtin.copy:
        src: /var/lib/aide/aide.db.new.gz
        remote_src: true
        dest: /var/lib/aide/aide.db.gz
        mode: preserve
      when: ( not aide_path.stat.exists or not aide_path.stat.isreg ) and ansible_distribution != "SLES"
      changed_when: false
      tags:
        - 1.3.1

    # Copy in the already configured systemd service file using the copy module.
    # Be sure to set the selinux context.
    # Notify systemd to reload its daemons and start the service
    - name: 1.3.2 - Ensure File integrity is regularly checked (aidecheck service)
      tags:
        - 1.3.2
      notify: Restart aidecheck
      block:
        - name: 1.3.2 - Template in aidecheck.service file
          ansible.builtin.template:
            src: aidecheck.service
            dest: /etc/systemd/system/aidecheck.service
            owner: root
            group: root
            mode: 0644
            setype: systemd_unit_file_t

        - name: 1.3.2 - Enable aidecheck.service
          ansible.builtin.systemd:
            name: aidecheck.service
            enabled: true

      # Copy in the already configured systemd timer file using the copy module.
      # Be sure to set the selinux context.
      # Notify systemd to reload its daemons and start the timer
        - name: 1.3.2 - Ensure File integrity is regulary checked (aidecheck timer)
          ansible.builtin.template:
            src: aidecheck.timer
            dest: /etc/systemd/system/aidecheck.timer
            owner: root
            group: root
            mode: 0644
            setype: systemd_unit_file_t

        - name: 1.3.2 - Enable aidecheck.timer
          ansible.builtin.systemd:
            name: aidecheck.timer
            enabled: true

    - name: 1.3.3 Create aidecheck config.d dir
      ansible.builtin.file:
        path: /etc/aide.conf.d/
        owner: root
        group: root
        mode: 0644
        state: directory
      tags:
        - 1.3.3

    - name: 1.3.3 Ensure cryptographic mechanisms are used to protect the integrity of audit tools
      ansible.builtin.blockinfile:
        path: /etc/aide.conf.d/crypt.conf
        owner: root
        group: root
        create: true
        mode: 0644
        block: |
          # Audit Tools
          /sbin/auditctl p+i+n+u+g+s+b+acl+xattrs+sha512
          /sbin/auditd p+i+n+u+g+s+b+acl+xattrs+sha512
          /sbin/ausearch p+i+n+u+g+s+b+acl+xattrs+sha512
          /sbin/aureport p+i+n+u+g+s+b+acl+xattrs+sha512
          /sbin/autrace p+i+n+u+g+s+b+acl+xattrs+sha512
          /sbin/augenrules p+i+n+u+g+s+b+acl+xattrs+sha512
      tags:
        - 1.3.3

# 1.4 Secure Boot settings

# EFI uses grub.cfg file in the LILO location now. So the extra checks have been removed
- name: 1.4.0 - Check if the LILO path exists
  ansible.builtin.stat:
    path: "/boot/grub2/grub.cfg"
  register: grubdir
  tags:
    1.4.1

- name: 1.4.0 - Check if the grub user.cfg exists
  ansible.builtin.stat:
    path: "/boot/grub2/user.cfg"
  register: usercfgdir
  tags:
    1.4.1

# Control 1.4.1, Grub bootloader password - skipped

# Use file module to set permissions on grub files
- name: 1.4.2 - Set permissions on grub.cfg, grubenv
  ansible.builtin.file:
    path: "{{ item }}"
    owner: root
    group: root
    mode: 0600
  loop:
    - "{{ grubdir.stat.path }}"
    - /boot/grub2/grubenv
  tags:
    - 1.4.2

- name: 1.4.3 - Require authorization to enter Rescue mode
  tags:
    - 1.4.3
  block:
    - name: 1.4.3 - Require authorization to enter Rescue mode - create dir
      ansible.builtin.file:
        dest: "/etc/systemd/system/rescue.service.d/"
        state: directory
        owner: root
        group: root
        mode: 0755

    - name: 1.4.3 - Require authorization to enter Rescue mode - set file
      ansible.builtin.copy:
        dest: "/etc/systemd/system/rescue.service.d/00-require-auth.conf"
        owner: root
        group: root
        mode: 0644
        setype: etc_t
        content: |
          [Service]
          ExecStart=-/usr/lib/systemd/systemd-sulogin-shell rescue

# 1.5 Additional Process Hardening

- name: 1.5.1 - Ensure core dump storage is disabled
  ansible.builtin.blockinfile:
    path: /etc/systemd/coredump.conf
    create: true
    owner: root
    group: root
    mode: 0644
    block: |
      Storage=none
    state: present
    marker: "# {mark} Ansible managed Storage setting"
  tags:
    - 1.5.1

- name: 1.5.2 - Ensure core dump backtraces are disabled
  ansible.builtin.blockinfile:
    path: /etc/systemd/coredump.conf
    create: true
    owner: root
    group: root
    mode: 0644
    block: |
      ProcessSizeMax=0
    state: present
    marker: "# {mark} Ansible managed ProcessSize setting"
  tags:
    - 1.5.2

- name: 1.5.3 - Ensure address space layout reandomization (ASLR) is enabled
    # The sysctl module will set variables in /etc/sysctl.conf and tell sysctl
    #  to reload them immediately if 'reload' is set to 'true'.
  ansible.posix.sysctl:
    name: kernel.randomize_va_space
    value: "2"
    reload: true
    state: present
    sysctl_set: true
  tags:
    - 1.5.3

# Use system package manager to remove
- name: 1.6.1.1 - Ensure SELinux is installed
  ansible.builtin.dnf:
    name:
      - libselinux
      - python3-libselinux
    state: present
  register: selinux_installed
  when: selinux is defined and selinux != "Disabled"
  tags:
    - 1.6.1.1

# re-gather system facts in case we installed selinux packages.
#  If selinux wasn't installed, it will not populate ansible_selinux fact correctly, regathering
#  will pull it with the right information
- name: 1.6.1.1 - Regather facts if installed selinux package
  ansible.builtin.setup:
  when: selinux_installed.changed
  tags:
    - 1.6.1.1

# Use the replace module to remove any disablment of selinux in grub if
#  it isn't expressly disabled from a variable
- name: 1.6.1.2 - Ensure SELinux is not disabled in bootloader configuration
  ansible.builtin.replace:
    dest: /etc/default/grub
    regexp: "{{ item }}"
    replace: ""
  with_items:
    - selinux=0
    - enforcing=0
  when: selinux is defined and selinux | lower != "disabled"
  notify: Rebuild grub
  tags:
    - 1.6.1.2

# Replace the current selinux policy with whatever the variable is set for
- name: 1.6.1.3 - Set SELinux policy to {{ selinux_policy }}
  ansible.builtin.replace:
    dest: /etc/selinux/config
    regexp: "^SELINUXTYPE=((?!{{ selinux_policy }}).)*$"
    replace: "SELINUXTYPE={{ selinux_policy }}"
  when: ( selinux is defined and selinux_policy is defined ) and selinux | lower != "disabled"
  tags:
    - 1.6.1.3

# If we are going to be enabling selinux in passive or enforcing mode,
#  set the autorelabel and notify the machine to reboot
- name: 1.6.1.3 - If disabled and we are enabling it, autorelabel
  ansible.builtin.file:
    path: /.autorelabel
    owner: root
    group: root
    mode: 0644
    state: touch
  when: ( ansible_selinux.status == "disabled" and selinux | lower != "disabled" ) or selinux_installed.changed
  notify: Reboot
  tags:
    - 1.6.1.3

# Replace the current selinux mode with what the variable is set to
- name: 1.6.1.[4-5] - Set SELinux to {{ selinux | lower }}
  ansible.builtin.replace:
    dest: /etc/selinux/config
    regexp: "^SELINUX=((?!{{ selinux }}).)*$"
    replace: "SELINUX={{ selinux | lower }}"
  when: selinux is defined and ( selinux | lower == "enforcing" or selinux | lower == "permissive" or selinux | lower == "disabled" )
  notify: Reboot
  tags:
    - 1.6.1.4
    - 1.6.1.5

# Let the user know if there are any processes that are not running under the
#  a selinux context
- name: 1.6.1.6 - Report on unconfined running services
  tags:
    - 1.6.1.6
  when: ansible_selinux.status != "disabled"
  block:
    # In RHEL8, all unconfined services run under their own context
    - name: 1.6.1.6 - Generate report on unconfined running services
      ansible.builtin.shell: /usr/bin/ps -eZ | /usr/bin/grep unconfined_service_t
      register: unconfined_services_out
      failed_when: unconfined_services_out.rc == "2"
      changed_when: false
      check_mode: false

    # Print any findings to the user
    - name: 1.6.1.6 - Report on unconfined running services to user
      ansible.builtin.debug:
        msg:
          - "Unconfined processes found:"
          - "{{ unconfined_services_out.stdout_lines }}"
      changed_when: true
      when: unconfined_services_out.stdout

# Use system package manager to remove package
- name: 1.6.1.7 - Remove setroubleshoot
  ansible.builtin.dnf:
    name: setroubleshoot
    state: absent
  tags:
    - 1.6.1.7

- name: 1.6.1.8 - Remove MCS Translation Service
  ansible.builtin.dnf:
    name: mcstrans
    state: absent
  tags:
    - 1.6.1.8

# 1.7 Warning Banners

# Use copy module to copy in the appropriate files based on variable and set permissions
- name: 1.7.1 - Install motd banners
  ansible.builtin.copy:
    src: "{{ motd_file }}"
    dest: /etc/motd
    owner: root
    group: root
    mode: 0644
  when: motd_use is defined and motd_use
  tags:
    - 1.7.1
    - 1.7.4

# Use copy module to copy in the appropriate files based on variable and set permissions
- name: 1.7.2 - Install issue banners
  ansible.builtin.copy:
    src: "{{ issue_file }}"
    dest: /etc/issue
    owner: root
    group: root
    mode: 0644
  when: issue_use is defined and issue_use
  tags:
    - 1.7.2
    - 1.7.5

# Use copy module to copy in the appropriate files based on variable and set permissions
- name: 1.7.3 - Install issue.net banners
  ansible.builtin.copy:
    src: "{{ issue_net_file }}"
    dest: /etc/issue.net
    owner: root
    group: root
    mode: 0644
  when: issue_net_use is defined and issue_net_use
  tags:
    - 1.7.3
    - 1.7.6

# 1.8 GDM

# Disable GDM
- name: 1.8.1 - disable display manager if graphical desktop not needed
  tags:
    - 1.8.1
  block:
    # Find the current default run level. The systemctl module does not handle the
    #  get-default routine, so we are looking at the target of the symlink at /etc/systemd/system/default.target
    - name: 1.8.1 - get default runlevel
      ansible.builtin.stat:
        path: /etc/systemd/system/default.target
      register: default_runlevel_out
      tags:
        - 1.8.1

    # Set the current run level. The systemctl module does not handle the
    #  get-default routine, so we are looking at the target of the symlink at /etc/systemd/system/default.target
    - name: 1.8.1 - Set current runlevel (non graphical)
      ansible.builtin.command: /usr/bin/systemctl isolate multi-user.target
      register: isolate_out
      changed_when: isolate_out.changed
      when: default_runlevel_out.stat.lnk_target is search("graphical.target") and not graphical_interface
      tags:
        - 1.8.1

    - name: 1.8.1 - Set current runlevel (graphical)
      ansible.builtin.command: /usr/bin/systemctl isolate graphical.target
      register: isolate_out
      changed_when: isolate_out.changed
      when: default_runlevel_out.stat.lnk_target is search("multi-user.target") and graphical_interface
      tags:
        - 1.8.1

    # Set the default run level. We are doing it the hard way since systemctl doesn't handle set-default
    - name: 1.8.1 - Set default runlevel (non graphical)
      ansible.builtin.file:
        src: /lib/systemd/system/multi-user.target
        dest: /etc/systemd/system/default.target
        owner: root
        group: root
      when: not graphical_interface

    - name: 1.8.1 - Set default runlevel (graphical)
      ansible.builtin.file:
        src: /lib/systemd/system/graphical.target
        dest: /etc/systemd/system/default.target
        owner: root
        group: root
      when: graphical_interface

    - name: 1.8.1 - Remove the GNOME display manager
      ansible.builtin.dnf:
        name: gdm
        state: absent
      when: "'gdm' in ansible_facts.packages and not graphical_interface"

# add a banner to the login screen if the graphical_interface variable is set to true
- name: 1.8.[2-3] Ensure GDM banner set up
  when: graphical_inteface is defined and graphical_interface
  tags:
    - 1.8.2
    - 1.8.3
    - 1.8.4
  block:
    - name: 1.8.[2-4] - Set up dconf profile for gdm
      ansible.builtin.blockinfile:
        path: /etc/dconf/profile/gdm
        owner: root
        group: root
        mode: 0644
        setype: etc_t
        create: true
        block: |
          user-db:user
          system-db:gdm
          file-db:/usr/share/gdm/greeter-dconf-defaults
      tags:
        - 1.8.2
        - 1.8.3
        - 1.8.4

    - name: 1.8.[2-3] - Create the defaults file and populate group
      ansible.builtin.blockinfile:
        path: /etc/dconf/db/gdm.d/01-banner-message
        owner: root
        group: root
        mode: 0644
        setype: etc_t
        create: true
        block: |
          [org/gnome/login-screen]
      tags:
        - 1.8.2
        - 1.8.3

    - name: 1.8.2 - Enable login screen for gdm
      ansible.builtin.blockinfile:
        # Add our required pieces to the greeter defaults file
        path: /etc/dconf/db/gdm.d/01-banner-message
        owner: root
        group: root
        mode: 0644
        setype: etc_t
        insertafter: "[org/gnome/login-screen]"
        block: |
          banner-message-enable=true
          banner-message-text='Authorized users only. All activity may be monitored and reported.'
      tags:
        - 1.8.2

    - name: 1.8.3 Ensure GDM disable-user list is enabled
      ansible.builtin.blockinfile:
        path: /etc/dconf/db/gdm.d/01-banner-message
        owner: root
        group: root
        mode: 0644
        setype: etc_t
        insertafter: "[org/gnome/login-screen]"
        block: |
          disable-user-list=true
      tags:
        - 1.8.3

    - name: 1.8.4 Set gdm timeouts
      ansible.builtin.blockinfile:
        path: /etc/dconf/db/distro.d/00-screensaver
        owner: root
        group: root
        mode: 0644
        setype: etc_t
        block: |
          # Specify the dconf path
          [org/gnome/desktop/session]

          # Number of seconds of inactivity before the screen goes blank
          # Set to 0 seconds if you want to deactivate the screensaver.
          idle-delay=uint32 {{ idle_delay }}
          # Specify the dconf path
          [org/gnome/desktop/screensaver]
          # Number of seconds after the screen is blank before locking the screen
          lock-delay=uint32 {{ lock_delay }}
      tags:
        - 1.8.4

# 1.8.5 TODO
# 1.8.6 TODO
# 1.8.7 TODO
# 1.8.8 TODO
# 1.8.9 TODO
# 1.8.10 - Ensure XDCMP is not enabled, skipping

# 1.10 Configure crypto policy
- name: 1.10.0 - Configure crypto-policy
  tags:
    - 1.10.0
  block:
    - name: 1.10.0 - Display error if crypto variable violates policy
      ansible.builtin.debug:
        msg:
          - "crypto_policy is set to: {{ crypto_policy }}. Which is not a valid selection."
          - "Valid choices are DEFAULT, FUTURE, and FIPS."
          - "LEGACY selection does not satisfy the control requirement"
          - "Refusing to update crypto_policy information"
      when: crypto_policy is defined and ( crypto_policy != "DEFAULT" and crypto_policy != "FUTURE" and crypto_policy != "FIPS" )

    - name: 1.10.0 - Set crypto-policy to {{ crypto_policy | upper | default('DEFAULT', true) }}
      ansible.builtin.lineinfile:
        path: /etc/crypto-policies/config
        regexp: "^(LEGACY|FUTURE|FIPS|DEFAULT)"
        line: "{{ crypto_policy | upper | default('DEFAULT', true) }}"
      notify: Update crypto_policy

    - name: 1.10.0 - Check to see if FIPS mode is already set up if crypto_policy == "FIPS"
      ansible.builtin.command: /usr/sbin/fips-mode-setup --is-enabled
      register: fips_mode
      when: crypto_policy is defined and crypto_policy == "FIPS"
      failed_when: false
      changed_when: false

    - name: 1.10.0 - Enabling FIPS mode if crypt_policy set to FIPS
      ansible.builtin.command: /usr/bin/fips-mode-setup --enable
      when: ( crypto_policy is defined and crypto_policy == "FIPS") and fips_mode.rc == "2"

# 2 Services

- name: 2.1.1 - Verify chrony is installed
  ansible.builtin.dnf:
    name: "chrony"
    state: present
  tags:
    - 2.1.1

# Use the template module to deploy the config file for the time sync program
#  The default file does not have any template variables, but it's there so
#  they can be added in the future.
# Control also sets the user to chrony, but it is already default in RHEL9
- name: 2.1.2 - Configure chrony
  ansible.builtin.template:
    src: "chrony.conf"
    dest: /etc/chrony.conf
    owner: root
    group: root
    mode: 0644
  notify: Restart chronyd
  tags:
    - 2.1.2

# 2.2 Special Purpose Services
# This collection of tasks creates a empty list and save it as a fact.
#  For every item that is encountered (without the tag being skipped),
#  add a string to the list.
- name: 2.2.1 - Create empty list for unneeded packages
  ansible.builtin.set_fact:
    unneeded_packages: []

- name: 2.2.1 - Remove xorg-x11-server-common
  ansible.builtin.set_fact:
    unneeded_packages: "{{ unneeded_packages + [ 'xorg-x11-server-common' ] }}"
  tags:
    - 2.2.1

- name: 2.2.2 - Remove avahi; add to removal list
  ansible.builtin.set_fact:
    unneeded_packages: "{{ unneeded_packages + [ 'avahi' ] }}"
  tags:
    - 2.2.2

- name: 2.2.4 - Remove dhcp; add to removal list
  ansible.builtin.set_fact:
    unneeded_packages: "{{ unneeded_packages + [ 'dhcp' ] }}"
  when: dhcp_server is defined and not dhcp_server
  tags:
    - 2.2.4

- name: 2.2.5 - Remove bind; add to removal list
  ansible.builtin.set_fact:
    unneeded_packages: "{{ unneeded_packages + [ 'bind' ] + [ 'unbound' ] }}"
  when: dns_server is defined and not dns_server
  tags:
    - 2.2.5

- name: 2.2.6 - Remove vsftpd; add to removal list
  ansible.builtin.set_fact:
    unneeded_packages: "{{ unneeded_packages + [ 'vsftpd' ] }}"
  when: ftp_server is defined and not ftp_server
  tags:
    - 2.2.6

- name: 2.2.7 - Remove tftp-server; add to removal list
  ansible.builtin.set_fact:
    unneeded_packages: "{{ unneeded_packages + [ 'tftp-server' ] }}"
  when: tftp_server is defined and not tftp_server
  tags:
    - 2.2.7

- name: 2.2.8 - Remove httpd; add to removal list
  ansible.builtin.set_fact:
    unneeded_packages: "{{ unneeded_packages + [ 'httpd' ] + [ 'httpd-tools' ] + [ 'mod_ssl' ] }}"
  when: http_server is defined and not http_server
  tags:
    - 2.2.8

- name: 2.2.9 - Remove dovecot and cyrus-imapd; add to removal list
  ansible.builtin.set_fact:
    unneeded_packages: "{{ unneeded_packages + [ 'dovecot' ] + [ 'cyrus-imapd'] }}"
  when: email_server is defined and not email_server
  tags:
    - 2.2.9

- name: 2.2.10 - Remove samba; add to removal list
  ansible.builtin.set_fact:
    unneeded_packages: "{{ unneeded_packages + [ 'samba' ] }}"
  when: smb_server is defined and not smb_server
  tags:
    - 2.2.10

- name: 2.2.11 - Remove squid; add to removal list
  ansible.builtin.set_fact:
    unneeded_packages: "{{ unneeded_packages + [ 'squid' ] }}"
  tags:
    - 2.2.11

- name: 2.2.12 - Remove snmp; add to removal list
  ansible.builtin.set_fact:
    unneeded_packages: "{{ unneeded_packages + [ 'net-snmp' ] + [ 'net-snmp-libs'] }}"
  tags:
    - 2.2.12

- name: 2.2.13 - Remove telnet-server; add to removal list
  ansible.builtin.set_fact:
    unneeded_packages: "{{ unneeded_packages + [ 'telnet-server' ] }}"
  tags:
    - 2.2.13

- name: 2.2.14 - Remove dnsmasq; add to removal list
  ansible.builtin.set_fact:
    unneeded_packages: "{{ unneeded_packages + [ 'dnsmasq' ] }}"
  tags:
    - 2.2.14

- name: 2.2.16 - Remove nfs server; add to removal list
  ansible.builtin.set_fact:
    unneeded_packages: "{{ unneeded_packages + [ 'nfs-utils' ] }}"
  when: nfs_server is defined and not nfs_server
  tags:
    - 2.2.16

- name: 2.2.17 - Remove rpcbind; add to removal list
  ansible.builtin.set_fact:
    unneeded_packages: "{{ unneeded_packages + [ 'rpcbind' ] }}"
  tags:
    - 2.2.17

- name: 2.2.18 - Remove rsync-daemon; add to removal list
  ansible.builtin.set_fact:
    unneeded_packages: "{{ unneeded_packages + [ 'rsync-daemon' ] }}"
  tags:
    - 2.2.18

- name: 2.3.1 - Remove telnet; add to removal list
  ansible.builtin.set_fact:
    unneeded_packages: "{{ unneeded_packages + [ 'telnet' ] }}"
  tags:
    - 2.3.1

- name: 2.3.2 - Remove openldap-clients; add to removal list
  ansible.builtin.set_fact:
    unneeded_packages: "{{ unneeded_packages + [ 'openldap-clients' ] }}"
  tags:
    - 2.3.2

- name: 2.3.3 - Remove tftp; add to removal list
  ansible.builtin.set_fact:
    unneeded_packages: "{{ unneeded_packages + [ 'tftp' ] }}"
  tags:
    - 2.3.3

- name: 2.3.4 - Remove ftp; add to removal list
  ansible.builtin.set_fact:
    unneeded_packages: "{{ unneeded_packages + [ 'ftp' ] }}"
  tags:
    - 2.3.4

- name: 2.3 - list of packages to remove
  ansible.builtin.debug:
    var: unneeded_packages
  tags:
    - 2.3.0

# With the list complete, use it with the system's package manager
#  to remove packages from the system that are not needed.
- name: 2.3 - Process removal list
  ansible.builtin.dnf:
    name: "{{ unneeded_packages }}"
    state: absent
  tags:
    - 2.2.0
    - 2.3.0

# Cups should be remove per control 2.2.16, but it may not be able to due to
#  dependencies, so disable the service instead
- name: 2.2.3 - Disable cups as we my not be able to uninstall it
  ansible.builtin.service:
    name: "{{ item }}"
    enabled: false
    state: stopped
  when: "'cups' in ansible_facts.packages"
  loop:
    - cups.service
    - cups.socket
    - cups-browsed.service
  tags:
    - 2.2.3

# Use the stat module to determine if the mail server config file exists.
#  If it does and we are to be a mail server, then modify it per the control.
- name: 2.2.15 - Configure email for local-only mode if mail software is installed and not intending to be an external email relay (mail_server=false)
  tags:
    - 2.2.15
  block:
    - name: 2.2.15 - Find if we have a mail agent config file
      ansible.builtin.stat:
        path: /etc/postfix/main.cf
      register: postfix_out
      changed_when: false

    - name: 2.2.15 - If the file exists and not a mail server, then set loopback only
      ansible.builtin.replace:
        dest: /etc/postfix/main.cf
        regexp: "^inet_interfaces = ((?!localhost).)*$"
        replace: "inet_interfaces = loopback-only"
      when: postfix_out.stat.exists and not email_server
      notify: Restart postfix

# Control 2.4 is a manual control, skipping

# Section 3, Network parameters
#
# Control 3.1.1 Report on IPv6 status skipped
# Control 3.1.2 Ensure wireless interfaces are disabled is interface dependent
#   skipping

#  IPv4 network parameters
- name: 3.2.0 - Create empty dictionary for unneeded IPv4 network parameters
  ansible.builtin.set_fact:
    unneeded_ipv4_network: {}

- name: 3.2.0 - Create empty dictionary for unneeded IPv6 network parameters
  ansible.builtin.set_fact:
    unneeded_ipv6_network: {}

- name: 3.2.1 - Ensure IP forwarding is disabled
  ansible.builtin.set_fact:
    unneeded_ipv4_network: "{{ unneeded_ipv4_network | combine({ 'net.ipv4.ip_forward' : '0'}) }}"
  tags:
    - 3.2.1

- name: 3.2.1 - Ensure IP forwarding is disabled
  ansible.builtin.set_fact:
    unneeded_ipv6_network: "{{ unneeded_ipv6_network | combine({ 'net.ipv6.conf.all.forwarding' : '0'}) }}"
  when: not ipv6_disable
  tags:
    - 3.2.1

- name: 3.2.2 - Ensure packet redirect sending is disabled
  ansible.builtin.set_fact:
    unneeded_ipv4_network: "{{ unneeded_ipv4_network | combine({ item : '0' }) }}"
  loop:
    - net.ipv4.conf.all.send_redirects
    - net.ipv4.conf.default.send_redirects
  tags:
    - 3.2.2

- name: 3.3.1 - Ensure source routed packets are not accepted
  ansible.builtin.set_fact:
    unneeded_ipv4_network: "{{ unneeded_ipv4_network | combine({ item : '0' }) }}"
  loop:
    - net.ipv4.conf.all.accept_source_route
    - net.ipv4.conf.default.accept_source_route
  tags:
    - 3.3.1

- name: 3.3.1 - Ensure source routed packets are not accepted
  ansible.builtin.set_fact:
    unneeded_ipv6_network: "{{ unneeded_ipv6_network | combine({ item : '0' }) }}"
  loop:
    - net.ipv6.conf.all.accept_source_route
    - net.ipv6.conf.default.accept_source_route
  when: not ipv6_disable
  tags:
    - 3.3.1

- name: 3.3.2 - Ensure ICMP redirects are not accepted
  ansible.builtin.set_fact:
    unneeded_ipv4_network: "{{ unneeded_ipv4_network | combine({ 'net.ipv4.conf.all.accept_redirects' : '0' }) }}"
  tags:
    - 3.3.2

- name: 3.3.2 - Ensure ICMP redirects are not accepted
  ansible.builtin.set_fact:
    unneeded_ipv6_network: "{{ unneeded_ipv6_network | combine({ 'net.ipv6.conf.all.accept_redirects' : '0' }) }}"
  when: not ipv6_disable
  tags:
    - 3.3.2

- name: 3.3.3 - Ensure secure ICMP redirects are not accepted
  ansible.builtin.set_fact:
    unneeded_ipv4_network: "{{ unneeded_ipv4_network | combine({ item : '0' }) }}"
  loop:
    - net.ipv4.conf.all.secure_redirects
    - net.ipv4.conf.default.secure_redirects
  tags:
    - 3.3.3

- name: 3.3.4 - Ensure suspicious packets are logged
  ansible.builtin.set_fact:
    unneeded_ipv4_network: "{{ unneeded_ipv4_network | combine({ item : '1' }) }}"
  loop:
    - net.ipv4.conf.all.log_martians
    - net.ipv4.conf.default.log_martians
  tags:
    - 3.3.4

- name: 3.3.5 - Ensure broadcast ICMP requests are ignored
  ansible.builtin.set_fact:
    unneeded_ipv4_network: "{{ unneeded_ipv4_network | combine({ 'net.ipv4.icmp_echo_ignore_broadcasts' : '1' }) }}"
  tags:
    - 3.3.5

- name: 3.3.6 - Ensure bogus ICMP responses are ignored
  ansible.builtin.set_fact:
    unneeded_ipv4_network: "{{ unneeded_ipv4_network | combine({ 'net.ipv4.icmp_ignore_bogus_error_responses' : '1' }) }}"
  tags:
    - 3.3.6

- name: 3.3.7 - Ensure reverse path filtering is enabled
  ansible.builtin.set_fact:
    unneeded_ipv4_network: "{{ unneeded_ipv4_network | combine({ item : '1' }) }}"
  loop:
    - net.ipv4.conf.all.rp_filter
    - net.ipv4.conf.default.rp_filter
  tags:
    - 3.3.7

- name: 3.3.8 - Ensure TCP SYN Cookies is enabled
  ansible.builtin.set_fact:
    unneeded_ipv4_network: "{{ unneeded_ipv4_network | combine({ 'net.ipv4.tcp_syncookies' : '1' }) }}"
  tags:
    - 3.3.8

- name: 3.3.9 - Ensure IPv6 router advertisements are not accepted
  ansible.builtin.set_fact:
    unneeded_ipv6_network: "{{ unneeded_ipv6_network | combine({ item : '0' }) }}"
  when: not ipv6_disable
  loop:
    - net.ipv6.conf.all.accept_ra
    - net.ipv6.conf.default.accept_ra
  tags:
    - 3.3.9

- name: 3.3 - list of IPv4 network settings
  ansible.builtin.debug:
    var: unneeded_ipv4_network

- name: 3.3 - list of IPv6 network settings
  ansible.builtin.debug:
    var: unneeded_ipv6_network

# The sysctl module will configure certain sysctl parameters. They are
#  collected into a loop here to speed the implementation
# Once complete, notify the system to flush the network routes
- name: 3.3 - Process unneeded network settings for IPv4
  tags:
    - 3.3.0
  block:
    - name: 3.3 - Set networking parameters
      ansible.posix.sysctl:
        name: "{{ item.key }}"
        value: "{{ item.value }}"
        reload: true
        state: present
        sysctl_set: true
      loop: "{{ lookup('ansible.builtin.dict' , unneeded_ipv4_network) }}"
      notify: Flush network routes

- name: 3.3 - Process unneeded network settings for IPv6
  tags:
    - 3.3.0
  block:
    - name: 3.3 - Set networking parameters
      ansible.posix.sysctl:
        name: "{{ item.key }}"
        value: "{{ item.value }}"
        reload: true
        state: present
        sysctl_set: true
      loop: "{{ lookup('ansible.builtin.dict' , unneeded_ipv6_network) }}"
      notify: Flush network routes

- name: 3.1 - Disable uncommon network protocols
  tags:
    - 3.1.1
  block:
    # This collection of tasks creates a empty list and save it as a fact.
    #  For every item that is encountered (without the tag being skipped),
    #  add a string to the list.
    - name: 3.1.0 - Create empty list of uncommon network protocols to disable
      ansible.builtin.set_fact:
        uncommon_network: []

    - name: 3.1.1 - Disable TIPC
      ansible.builtin.set_fact:
        uncommon_network: "{{ uncommon_network + [ 'tipc' ] }}"

    - name: 3.1.3 - Process uncommon network list
      ansible.builtin.lineinfile:
        dest: /etc/modprobe.d/CIS.conf
        line: "install {{ item }} /bin/false"
        state: present
        create: true
        owner: root
        group: root
        mode: 0644
      with_items:
        - "{{ uncommon_network }}"

    - name: 3.1.3 - Add to blacklist
      ansible.builtin.lineinfile:
        dest: /etc/modprobe.d/CIS.conf
        line: "blacklist {{ item }}"
        state: present
        create: true
        owner: root
        group: root
        mode: 0644
      with_items:
        - "{{ uncommon_network }}"

# Section 3 - Firewall
#   iptables is deprecated and not covered under the CIS controls for RHEL 9

- name: 3.4.1.1 - ensure nftables is installed
  ansible.builtin.dnf:
    name: nftables
    state: present
  tags:
    - 3.4.1

- name: 3.4.1.1 - Install firewalld
  ansible.builtin.dnf:
    name: "firewalld"
    state: present
  when: enable_firewall is defined and enable_firewall == "firewalld"
  notify: Start firewalld # 3.4.2.1


- name: 3.4.1.1 - Disable netfilters service
  ansible.builtin.systemd:
    name: nftables
    state: stopped
    enabled: false
    masked: true
  when: enable_firewall is defined and enable_firewall == "firewalld"

- name: 3.4.1.1 - Disable firewalld service
  ansible.builtin.systemd:
    name: firewalld
    state: stopped
    enabled: false
    masked: true
  when: enable_firewall is defined and enable_firewall == "firewalld"

- name: 3.4.2.1 - Set default zone
  ansible.builtin.lineinfile:
    path: "/etc/firewalld/firewalld.conf"
    regexp: '^DefaultZone\s*((?!{{ firewalld_default_zone }}).)*$'
    line: "DefaultZone={{ firewalld_default_zone }}"
  when: enable_firewall is defined and enable_firewall == "firewalld" and firewalld_default_zone is defined
  notify: Restart firewalld

- name: 3.4.2.2 - Ensure netfilters has at least one table
  when: enable_firewall is defined and enable_firewall == "nftables"
  block:
    - name: 3.4.2.2 - Find any current netfilter tables
      ansible.builtin.command: nft list tables
      register: tables_list
      changed_when: false

    - name: 3.4.2.2 - Create a basic table if none exist
      ansible.builtin.command: nft create table inet firewalld NFTables
      when: not tables_list
      tags:
        - 3.4.2.2

# Control 3.4.2.[3-7] is not set as it is very machine dependant

- name: Notify users to configure the firewall
  ansible.builtin.debug:
    msg:
      - "3.4.2 - Firewall must be configured locally"
  tags:
    - 3.4.2

# Section 4 - Logging and Auditing

- name: 4.1 Install and configure system auditing
  when: enable_audit is defined and enable_audit
  tags:
    - 4.1.0
  block:
    - name: 4.1.1.1 - Install Auditd
      ansible.builtin.dnf:
        name:
          - audit
          - audit-libs
        state: present
      tags:
        - 4.1.1.1

    - name: 4.1.1.2 - Ensure auditing for processes that start prior to auditd
    # We check here because we don't know what position the audit=1 is in
    # order to simply do the replace, so we are instead looking for the match in the file first.
    # If it doesn't exist, then we can just insert it
      ansible.builtin.lineinfile:
        path: /etc/default/grub
        regexp: '^\s*GRUB_CMDLINE_LINUX.*audit=1'
        state: absent
      check_mode: true
      changed_when: false
      register: audit_exist
      failed_when: false
      tags:
        - 4.1.1.2

    # use the replace module to add it to grub bootloader and then notify
    #  grub to rebuild
    - name: 4.1.1.2 - enable audit service in grub
      ansible.builtin.replace:
        path: /etc/default/grub
        regexp: '^GRUB_CMDLINE_LINUX="'
        replace: 'GRUB_CMDLINE_LINUX="audit=1 '
      notify: Rebuild grub
      when: not audit_exist.found
      tags:
        - 4.1.1.2

    - name: 4.1.1.3 - Ensure audit_backlog_limit is sufficient, check if limit exists
      ansible.builtin.lineinfile:
        path: /etc/default/grub
        regexp: '^\s*GRUB_CMDLINE_LINUX.*audit_backlog_limit'
        state: absent
      check_mode: true
      changed_when: false
      register: audit_backlog_exist
      failed_when: false
      tags:
        - 4.1.1.3

    - name: 4.1.1.3 - Ensure audit_backlog_limit is sufficient, add audit_backlog_limit to grub
      ansible.builtin.replace:
        dest: /etc/default/grub
        regexp: '^\s*GRUB_CMDLINE_LINUX="'
        replace: 'GRUB_CMDLINE_LINUX="audit_backlog_limit={{ audit_backlog_limit }} '
      notify: Rebuild grub
      when: not audit_backlog_exist.found
      tags:
        - 4.1.1.3

    - name: 4.1.1.3 - Ensure audit_backlog_limit is sufficient, update limit if it already exists (check)
      ansible.builtin.lineinfile:
        dest: /etc/default/grub
        regexp: '^\s*GRUB_CMDLINE_LINUX=.*{{ audit_backlog_limit }}'
        state: absent
      check_mode: true
      changed_when: false
      register: our_limit
      when: audit_backlog_exist.found
      tags:
        - 4.1.1.3

    - name: 4.1.1.3 - Ensure audit_backlog_limit is sufficient, update limit if it already exists (fix)
      ansible.builtin.replace:
        dest: /etc/default/grub
        regexp: 'audit_backlog_limit=[\S]*'
        replace: 'audit_backlog_limit={{ audit_backlog_limit }}'
      notify: Rebuild grub
      when: audit_backlog_exist.found and not our_limit.found
      tags:
        - 4.1.1.3

    # Control is out of order to allow configuration before startup
    - name: 4.1.1.4 - Enable auditd service
      ansible.builtin.service:
        name: auditd
        enabled: true
        state: started
      tags:
        - 4.1.1.4

    # The replace module here is looking through file and make replacements of partial lines

    - name: 4.1.2.[1-3] - Configure audit log storage size
      ansible.builtin.replace:
        path: /etc/audit/auditd.conf
        regexp: "{{ item.find }}"
        replace: "{{ item.replace }}"
      loop:
        - {find: '^max_log_file\s+=\s+[^{{ log_file_size }}]', replace: 'max_log_file = {{ log_file_size }}'} # 4.1.2.1
        - {find: '^max_log_file_action\s+=\s+((?!keep_logs).)*$', replace: 'max_log_file_action = keep_logs'} # 4.1.2.2
        - {find: '^space_left_action\s+=\s+((?!email).)*$', replace: 'space_left_action = email'} # 4.1.2.2
        - {find: '^action_mail_acct\s+=\s+((?!root).)*$', replace: 'action_mail_acct = root'} # 4.1.2.2
        - {find: '^admin_space_left_action\s+=\s+((?!suspend).)*$', replace: 'admin_space_left_action = suspend'} # 4.1.2.2
        - {find: '^log_file\s+=\s+[^{{ log_file }}]', replace: 'log_file = {{ log_file }}'} # Supports 4.1.4.1
      notify: Restart auditd
      tags:
        - 4.1.2.1
        - 4.1.2.2
        - 4.1.2.3

    # For the next several checks, each one is in their own file, so we are using
    #  the copy module to place each file independently and then motifying
    #  a restart of auditd if anything changes.

    # cis-security versions before 1.5.0 did not enumerate the files, so the old files
    #  need to be removed to make way for the new versions
    - name: 4.1.3 - Remove old rules files that were not in correct order (pre v1.5.0)
      ansible.builtin.file:
        path: "/etc/audit/rules.d/{{ item }}"
        state: absent
      tags:
        - 4.1.3
      loop:
        - sudolog.rules
        - user_emulation.rules
        - datetime.rules
        - network.rules
        - file-system-mounts.rules
        - bad-file-access.rules
        - user-group-info.rules
        - dac.rules
        - sessions.rules
        - delete.rules
        - login.rules
        - MAC-policy.rules
        - chcon.rules
        - setfacl.rules
        - chacl.rules
        - usermod.rules
        - modules.rules

    - name: 4.1.3.1 - Ensure changes to system administration scope is collected
      ansible.builtin.template:
        dest: /etc/audit/rules.d/00-sudolog.rules
        src: audit_rules/sudolog.rules
        owner: root
        group: root
        mode: 0600
      notify: Restart auditd
      tags:
        - 4.1.3.1
        - 4.1.3.3

    - name: 4.1.3.2 - Ensure actions as another user are always logged
      ansible.builtin.template:
        dest: /etc/audit/rules.d/00-user_emulation.rules
        src: audit_rules/user_emulation.rules
        owner: root
        group: root
        mode: 0600
      notify: Restart auditd
      tags:
        - 4.1.3.2

    - name: 4.1.3.4 - Ensure to collect events that modify date/time
      ansible.builtin.template:
        dest: /etc/audit/rules.d/00-datetime.rules
        src: audit_rules/datetime.rules
        owner: root
        group: root
        mode: 0600
      notify: Restart auditd
      tags:
        - 4.1.3.4

    # TODO, determine if we need a separate RHEL9 version of network.rules
    - name: 4.1.3.5 - Ensure modifications to network environment are collected
      ansible.builtin.template:
        dest: /etc/audit/rules.d/00-network.rules
        src: audit_rules/network.rules
        owner: root
        group: root
        mode: 0600
      notify: Restart auditd
      tags:
        - 4.1.3.5

    # Control 4.1.3.6 requires a system scan, skipping

    - name: 4.1.3.[7,10] - Ensure [un]successful file system mounts are collected
      ansible.builtin.template:
        dest: /etc/audit/rules.d/00-file-system-mounts.rules
        src: audit_rules/file-system-mounts.rules
        owner: root
        group: root
        mode: 0600
      notify: Restart auditd
      tags:
        4.1.3.7
        4.1.3.10

    - name: 4.1.3.7 - Ensure unsuccessful file access attempts are collected
      ansible.builtin.template:
        dest: /etc/audit/rules.d/00-bad-file-access.rules
        src: audit_rules/bad-file-access.rules
        owner: root
        group: root
        mode: 0600
      notify: Restart auditd
      tags:
        4.1.3.7

    - name: 4.1.3.8 - Ensure events that modify user/group information are collected
      ansible.builtin.template:
        dest: /etc/audit/rules.d/00-user-group-info.rules
        src: audit_rules/user-group-info.rules
        owner: root
        group: root
        mode: 0600
      notify: Restart auditd
      tags:
        - 4.1.3.8

    - name: 4.1.3.9 - Ensure modifications to discretionary access controls are collected
      ansible.builtin.template:
        dest: /etc/audit/rules.d/00-dac.rules
        src: audit_rules/dac.rules
        owner: root
        group: root
        mode: 0600
      notify: Restart auditd
      tags:
        - 4.1.3.9

    - name: 4.1.3.11 - Ensure session initiation information is collected
      ansible.builtin.template:
        dest: /etc/audit/rules.d/00-sessions.rules
        src: audit_rules/sessions.rules
        owner: root
        group: root
        mode: 0600
      notify: Restart auditd
      tags:
        - 4.1.3.11

    - name: 4.1.3.12 - Ensure system logins are collected
      ansible.builtin.template:
        dest: /etc/audit/rules.d/00-login.rules
        src: audit_rules/login.rules
        owner: root
        group: root
        mode: 0600
      notify: Restart auditd
      tags:
        - 4.1.3.12

    - name: 4.1.3.13 - Ensure file deletion events by users are collected
      ansible.builtin.template:
        dest: /etc/audit/rules.d/00-delete.rules
        src: audit_rules/delete.rules
        owner: root
        group: root
        mode: 0600
      notify: Restart auditd
      tags:
        - 4.1.3.13

    - name: 4.1.3.14 - Ensure modifications to Mandatory Access Controls are collected
      ansible.builtin.template:
        dest: /etc/audit/rules.d/00-MAC-policy.rules
        src: audit_rules/MAC-policy.rules
        owner: root
        group: root
        mode: 0600
      notify: Restart auditd
      tags:
        - 4.1.3.14

    - name: 4.1.3.15 - Ensure successful and unsuccessful attempts to use the chcon command are recorded
      ansible.builtin.template:
        dest: /etc/audit/rules.d/00-chcon.rules
        src: audit_rules/chcon.rules
        owner: root
        group: root
        mode: 0600
      notify: Restart auditd
      tags:
        - 4.1.3.15

    - name: 4.1.3.16 - Ensure successful and unsuccessful attempts to use the setfacl command are recorded
      ansible.builtin.template:
        dest: /etc/audit/rules.d/00-setfacl.rules
        src: audit_rules/setfacl.rules
        owner: root
        group: root
        mode: 0600
      notify: Restart auditd
      tags:
        - 4.1.3.16

    - name: 4.1.3.17 - Ensure successful and unsuccessful attempts to use the chacl command are recorded
      ansible.builtin.template:
        dest: /etc/audit/rules.d/00-chacl.rules
        src: audit_rules/chacl.rules
        owner: root
        group: root
        mode: 0600
      notify: Restart auditd
      tags:
        - 4.1.3.17

    - name: 4.1.3.18 - Ensure successful and unsuccessful attempts to use the usermod command are recorded
      ansible.builtin.template:
        dest: /etc/audit/rules.d/00-usermod.rules
        src: audit_rules/usermod.rules
        owner: root
        group: root
        mode: 0600
      notify: Restart auditd
      tags:
        - 4.1.3.18

    - name: 4.1.3.19 - Ensure kernel module loading and unloading is collected
      ansible.builtin.template:
        dest: /etc/audit/rules.d/00-modules.rules
        src: audit_rules/modules.rules
        owner: root
        group: root
        mode: 0600
      notify: Restart auditd
      tags:
        - 4.1.3.19

    - name: 4.1.3.20 - Ensure audit configuration is immutable
      ansible.builtin.copy:
        dest: /etc/audit/rules.d/99-finalize.rules
        content: |
          -e 2
        owner: root
        group: root
        mode: 0600
      notify: Restart auditd
      tags:
        - 4.1.3.20

    # 4.1.3.21 requires manual verification and ansible won't be able to check until after handlers are run; skipping

- name: 4.1.4.[1-2,5-7] - Set auditd files to mode 600, user root, group root
  ansible.builtin.file:
    path: "{{ item }}"
    owner: root
    group: root
    mode: 0600
  with_fileglob:
    - "/etc/audit/auditd.conf"
    - '/etc/audit/rules.d/*'
    - "{{ log_file }}"
  tags:
    - 4.1.4.1
    - 4.1.4.2
    - 4.1.4.3
    - 4.1.4.5
    - 4.1.4.6
    - 4.1.4.7

# TODO 4.1.4.4

- name: 4.1.4.[8-10] - Ensure audit tools are 0755 or less permissive
  ansible.builtin.file:
    path: "{{ item }}"
    owner: root
    group: root
    mode: 'go-w'
  loop:
    - "/sbin/auditctl"
    - "/sbin/aureport"
    - "/sbin/ausearch"
    - "/sbin/autrace"
    - "/sbin/auditd"
    - "/sbin/augenrules"
  tags:
    - 4.1.4.8
    - 4.1.4.9
    - 4.1.4.10

# Section 4, Logging
- name: 4.2.1 - Configuring Rsyslog
  when: log_service and log_service == "rsyslog"
  block:
    - name: 4.2.1.1 - Ensure rsyslog is installed
      ansible.builtin.dnf:
        name: rsyslog
        state: present
      tags:
        - 4.2.1.1

    - name: 4.2.1.2 - Ensure Rsyslog service is running
      ansible.builtin.service:
        name: rsyslog
        enabled: true
        state: started
      tags:
        - 4.2.1.2

    - name: 4.2.1.3 - Configure journald to forward logs to rsyslog
      tags:
        - 4.2.1.3
      block:
        - name: 4.2.1.3 - Find any rsyslog files where all logs are being forwarded to a loghost
          ansible.builtin.shell: /usr/bin/grep -l -s "^*.*[^I][^I]*@" /etc/rsyslog.conf /etc/rsyslog.d/*.conf
          register: rsyslog_forward_out
          changed_when: false
          failed_when: rsyslog_forward_out.rc == "2"
          check_mode: false

        - name: 4.2.1.3 - Forward journald logs to rsyslog IF rsyslog is sending logs to a log host
          ansible.builtin.lineinfile:
            dest: /etc/systemd/journald.conf
            regexp: "^ForwardToSyslog=((?!yes).)*$"
            line: "ForwardToSyslog=yes"
            insertafter: "#ForwardToSyslog=no"
          when: rsyslog_forward_out.stdout

    - name: 4.2.1.4 - Ensure rsyslog default file permissions are configured
      ansible.builtin.lineinfile:
        path: /etc/rsyslog.conf
        regexp: '^\$FileCreateMode\s+0640'
        line: "$FileCreateMode 0640"
        create: true
        owner: root
        group: root
        mode: 0644
        state: present
      notify: Restart rsyslog
      tags:
        - 4.2.1.4

    - name: 4.2.1.5 - Ensure logging is configured in rsyslog
      ansible.builtin.copy:
        src: "{{ rsyslog_file }}"
        dest: "/etc/rsyslog.d/{{ rsyslog_file }}"
        owner: root
        group: root
        mode: 0640
      when: rsyslog_file is defined and rsyslog_file | length > 0
      tags:
        - 4.2.1.5

    # Control 4.2.1.6 - Ensure rsyslog is configured to send logs to a remote log host is machine dependent
    # skipping

    - name: 4.2.1.7 - Ensure remote rsyslog messages are only acepted on designated log hosts
      tags:
        - 4.2.1.7
      block:
        - name: 4.2.1.7 - Find all rsyslog conf files in /etc/rsyslog.d
          ansible.builtin.find:
            paths: "/etc/rsyslog.d"
            patterns: "*.conf"
          register: rsyslog_module_found

        - name: 4.2.1.7 - Disable imtcp loading module on non log hosts (rsyslog.d conf files)
          ansible.builtin.lineinfile:
            dest: "{{ item.path }}"
            regexp: '^\$ModLoad\s+imtcp'
            state: absent
          loop: "{{ rsyslog_module_found.files }}"
          when: log_host is defined and not log_host

        - name: 4.2.1.7 - Disable imtcp loading module on non log hosts (main rsyslog conf file)
          ansible.builtin.lineinfile:
            dest: "/etc/rsyslog.conf"
            regexp: '^\$ModLoad\s+imtcp'
            state: absent
          when: log_host is defined and not log_host

        - name: 4.2.1.7 - Disable TCP port listening on non log hosts (rsylog.d conf files)
          ansible.builtin.lineinfile:
            dest: "{{ item.path }}"
            regexp: '^\$InputTCPServerRun'
            state: absent
          loop: "{{ rsyslog_module_found.files }}"
          when: log_host is defined and not log_host

        - name: 4.2.1.7 - Disable TCP port listening on non log hosts (main rsyslog conf file)
          ansible.builtin.lineinfile:
            dest: "/etc/rsyslog.conf"
            regexp: '^\$InputTCPServerRun'
            state: absent
          when: log_host is defined and not log_host

        - name: 4.2.1.7 - Enable loading of imtcp module on log hosts
          ansible.builtin.lineinfile:
            dest: /etc/rsyslog.d/CIS.conf
            regexp: '^\$ModLoad\s+imtcp'
            line: "$ModLoad imtcp"
            create: true
            owner: root
            group: root
            mode: 0644
          when: log_host is defined and log_host

        - name: 4.2.1.7 - Enable TCP Port listening on port {{ log_port }}
          ansible.builtin.lineinfile:
            dest: /etc/rsyslog.d/CIS.conf
            regexp: '^\$InputTCPServerRun {{ log_port }}'
            line: "$InputTCPServerRun {{ log_port }}"
            create: true
            owner: root
            group: root
            mode: 0644
          when: log_host is defined and log_host

# 4.2.2 Configure journald
- name: 4.2.2.1.1 - configure journald
  when: log_service and log_service == "journald"
  tags:
    - 4.2.2.1
  block:
    - name: 4.2.2.1.1 - Ensure systemd-journald-remote is installed
      ansible.builtin.dnf:
        name: systemd-journal-remote
        state: present
      tags:
        - 4.2.2.1.1

    # Control 4.2.2.1.2 is machine dependent, skipping
    # Control 4.2.2.1.3 required 4.2.2.1.2 be configured prior. skipping

    - name: 4.2.2.1.4 Ensure systemd-journal-remote.socket is masked
      ansible.builtin.systemd:
        name: systemd-journal-remote.socket
        enabled: false
        masked: true
      tags:
        - 4.2.2.1.4

    - name: 4.2.2.1.4 Ensure systemd-jorunal-remote.service is masked
      ansible.builtin.systemd:
        name: systemd-journal-remote.service
        enabled: false
        masked: true
      tags:
        - 4.2.2.1.4

    - name: 4.2.2.3 - Ensure journald compresses large files
      ansible.builtin.lineinfile:
        dest: /etc/systemd/journald.conf
        regexp: "^Compress=((?!yes).)*$"
        line: "Compress=yes"
        insertafter: "^#Compress="
      notify: Restart journald
      tags:
        - 4.2.2.3

    - name: 4.2.2.4 - Ensure journald writes to peristent disk
      ansible.builtin.lineinfile:
        dest: /etc/systemd/journald.conf
        regexp: "^Storage=((?!persistent).)*$"
        line: "Storage=persistent"
        insertafter: "^#Storage="
      notify: Restart journald
      tags:
        - 4.2.2.4

    - name: 4.2.2.5 - Ensure journald is not configured to send logs to rsyslog IF rsyslog is sending logs to a log host
      ansible.builtin.lineinfile:
        dest: /etc/systemd/journald.conf
        regexp: "^ForwardToSyslog=((?!yes).)*$"
        state: absent
      tags:
        - 4.2.2.5

    # Control 4.2.2.6, configure log rotation is machine specific, skipping
    #   TODO
    # Control 4.2.2.7, Ensure journald default file permissions configured, is machine dependant, skipping

# Control 4.2.3 is machine specific, skipping

    - name: 4.2.2.2 - Ensure journald service is enabled
      ansible.builtin.systemd:
        name: systemd-journald
        state: started
        masked: false
        enabled: true
      tags:
        - 4.2.2.2

- name: 4.3 - Ensure logrotate is installed and configured
  tags:
    - 4.3.0
  block:
    - name: 4.3.0 - Ensure logrotate is installed
      ansible.builtin.package:
        name: logrotate
        state: present

    - name: 4.3.0 - Copy source logrotate file
      ansible.builtin.copy:
        src: "{{ logrotate_file }}"
        dest: /etc/logrotate.conf
        owner: root
        group: root
        mode: 0644
        setype: etc_t
      when: logrotate_file and logrotate_file | length > 0

# Section 5 - Access and Authorization
#

# This control is early in order to create the files. This will
# make sure they are available when cron starts
- name: 5.1.0 - Configure cron/at
  when: "'cronie' in ansible_facts.packages"
  tags:
    - 5.1.0
  block:
    - name: 5.1.8 - Ensure cron is restricted to authorized users - Create file
      ansible.builtin.file:
        path: /etc/cron.allow
        owner: root
        group: root
        mode: 0600
        state: file
      when: cron_allow and cron_allow | length > 0
      tags:
        - 5.1.8

    - name: 5.1.8 - Ensure cron is restricted to authorized users
      ansible.builtin.lineinfile:
        path: /etc/cron.allow
        regexp: ^{{ item.0 }}
        line: "{{ item.0 }}"
        owner: root
        group: root
        mode: 0600
        create: true
      loop:
        - "{{ cron_allow }}"
      when: cron_allow and cron_allow | length > 0
      tags:
        - 5.1.8

    - name: 5.1.1 - Ensure cron is enabled
      ansible.builtin.service:
        name: crond
        enabled: true
        state: started
      tags:
        - 5.1.1

    - name: 5.1.2 - Ensure permissions on /etc/crontab
      ansible.builtin.file:
        path: /etc/crontab
        owner: root
        group: root
        mode: 0600
      tags:
        - 5.1.2

    - name: 5.1.[3-7] - Ensure permissions on crontab directories
      ansible.builtin.file:
        path: "{{ item }}"
        owner: root
        group: root
        mode: 0700
      loop:
        - /etc/cron.hourly
        - /etc/cron.daily
        - /etc/cron.weekly
        - /etc/cron.monthly
        - /etc/cron.d
      tags:
        - 5.1.3
        - 5.1.4
        - 5.1.5
        - 5.1.6
        - 5.1.7

    - name: 5.1.9 - Ensure at is restricted to authorized users - Create file
      ansible.builtin.file:
        path: /etc/at.allow
        owner: root
        group: root
        mode: 0600
        state: file
      when: at_allow and at_allow | length > 0
      tags:
        - 5.1.9

    - name: 5.1.9 - Ensure at is restricted to authorized users
      ansible.builtin.lineinfile:
        path: /etc/at.allow
        regexp: ^{{ item.0 }}
        line: "{{ item.0 }}"
        owner: root
        group: root
        mode: 0600
        create: true
      loop:
        - "{{ at_allow }}"
      when: at_allow and at_allow | length > 0
      tags:
        - 5.1.9

- name: 5.2 - SSH File configurations
  tags:
    - 5.2.0
  block:
    - name: 5.2.1 - Set permissions on SSH file
      ansible.builtin.file:
        dest: /etc/ssh/sshd_config
        owner: root
        group: root
        mode: 0600
      tags:
        - 5.2.1

    - name: 5.2.2 - Set Permissions on ssh private host keys
      tags:
        - 5.2.2
      block:
        - name: 5.2.2 - Find all ssh private host keys
          ansible.builtin.find:
            paths: /etc/ssh
            file_type: file
            patterns: ssh_host_*_key
          register: ssh_host_out
          changed_when: false

        - name: 5.2.2 - Set permissions on all ssh private host keys (Red Hat set the group to ssh_keys and mode to 640)
          ansible.builtin.file:
            dest: "{{ item.path }}"
            owner: root
            group: root
            mode: 0600
          loop: "{{ ssh_host_out.files }}"

    - name: 5.2.3 - Set Permissions on ssh public host keys
      tags:
        - 5.2.3
      block:
        - name: 5.2.3 - Find all ssh public host keys
          ansible.builtin.find:
            paths: /etc/ssh
            file_type: file
            patterns: ssh_host_*_key.pub
          register: ssh_hostpub_out
          changed_when: false

        - name: 5.2.3 - Set permissions on all ssh public host keys
          ansible.builtin.file:
            dest: "{{ item.path }}"
            owner: root
            group: root
            mode: 0644
          loop: "{{ ssh_hostpub_out.files }}"

    - name: 5.2.4 - Ensure SSH access is limited
      tags:
        - 5.2.4
      block:
        - name: 5.2.4 - Ensure SSH access is limited (AllowedUsers)
          ansible.builtin.lineinfile:
            path: "/etc/ssh/sshd_config"
            regexp: ^{{ item.key }}\ *{{ item.value }}
            line: "{{ item.key }}  {{ item.value }}"
          notify: Restart sshd
          loop:
            - { key: 'AllowUsers', value: "{{ ssh_allowed_users }}" }
          when: ssh_allowed_users is defined and ssh_allowed_users

        - name: 5.2.4 - Ensure SSH access is limited (AllowGroups)
          ansible.builtin.lineinfile:
            path: "/etc/ssh/sshd_config"
            regexp: ^{{ item.key }}\ *{{ item.value }}
            line: "{{ item.key }}  {{ item.value }}"
          notify: Restart sshd
          loop:
            - { key: 'AllowGroups', value: "{{ ssh_allowed_groups }}" }
          when: ssh_allowed_groups is defined and ssh_allowed_groups

        - name: 5.2.4 - Ensure SSH access is limited (DenyUsers)
          ansible.builtin.lineinfile:
            path: "/etc/ssh/sshd_config"
            regexp: "^{{ item.key }}\ *{{ item.value }}"
            line: "{{ item.key }}  {{ item.value }}"
          loop:
            - { key: 'DenyUsers', value: "{{ ssh_denied_users }}" }
          notify: Restart sshd
          when: ssh_denied_users is defined and ssh_denied_users

        - name: 5.2.4 - Ensure SSH access is limited (DenyGroups)
          ansible.builtin.lineinfile:
            path: "/etc/ssh/sshd_config"
            regexp: ^{{ item.key }}\ *{{ item.value }}
            line: "{{ item.key }}  {{ item.value }}"
          notify: Restart sshd
          loop:
            - { key: 'DenyGroups', value: "{{ ssh_denied_groups }}" }
          when: ssh_denied_groups is defined and ssh_denied_groups

    - name: 5.2.5 - Set LogLevel to {{ ssh_log_level }}
      ansible.builtin.replace:
        path: /etc/ssh/sshd_config
        replace: "LogLevel {{ ssh_log_level | upper }}"
        regexp: '^LogLevel\s*(QUIET|FATAL|ERROR|DEBUG)*$'
      notify: Restart sshd
      when: ssh_log_level == "INFO" or ssh_log_level == "WARN"
      tags:
        - 5.2.5

    - name: 5.2.6 - Ensure SSH is configured to use PAM
      ansible.builtin.lineinfile:
        path: "/etc/ssh/sshd_config"
        line: "UsePAM yes"
        regexp: '^UsePAM\s+[yes|no]'
        insertafter: "#UsePAM"
      notify: Restart sshd
      tags:
        - 5.2.6

    - name: 5.2.7 - Ensure PermitRootLogin is disbled
      ansible.builtin.lineinfile:
        path: /etc/ssh/sshd_config
        line: "PermitRootLogin no"
        regexp: '^PermitRootLogin\s*[^n]'
        insertafter: '^#PermitRootLogin\s*[^n]'
      notify: Restart sshd
      tags:
        - 5.2.7

    - name: 5.2.8 - Ensure HostbasedAuthentication is disabled
      ansible.builtin.lineinfile:
        path: /etc/ssh/sshd_config
        line: "HostbasedAuthentication no"
        regexp: '^HostbasedAuthentication\s*[^n]'
        insertafter: '^#HostbasedAuthentication\s*[^n]'
      notify: Restart sshd
      tags:
        - 5.2.8

    - name: 5.2.9 - Ensure SSH PermitEmptyPasswords is disabled
      ansible.builtin.lineinfile:
        path: /etc/ssh/sshd_config
        state: absent
        regexp: '^PermitEmptyPasswords\s*[^n]'
      notify: Restart sshd
      tags:
        - 5.2.9

    - name: 5.2.10 - Ensure PermitUserEnvironment is disabled
      ansible.builtin.lineinfile:
        path: /etc/ssh/sshd_config
        state: absent
        regexp: '^PermitUserEnvironment\s*[^n]'
      notify: Restart sshd
      tags:
        - 5.2.10

    - name: 5.2.11 - Ensure IgnoreRhosts is set
      ansible.builtin.lineinfile:
        path: /etc/ssh/sshd_config
        line: "IgnoreRhosts yes"
        regexp: '^IgnoreRhosts\s*[^y]'
        insertafter: '^#IgnoreRhosts\s*[^y]'
      notify: Restart sshd
      tags:
        - 5.2.11

    - name: 5.2.12 - Disable X11 forwarding
      ansible.builtin.lineinfile:
        path: /etc/ssh/sshd_config
        regexp: '^X11Forwarding\s*yes'
        state: absent
      notify: Restart sshd
      tags:
        - 5.2.12

    - name: 5.2.13 - Disable TCP Forwarding
      ansible.builtin.lineinfile:
        path: "/etc/ssh/sshd_config"
        line: "AllowTcpForwarding no"
        regexp: '^AllowTcpForwarding\s+(yes|no)'
        insertafter: "^#AllowTcpForwarding"
      notify: Restart sshd
      tags:
        - 5.2.13

    - name: 5.2.14 - Ensure system crypto policy isn't overriden in SSH
      ansible.builtin.lineinfile:
        path: "/etc/ssh/sshd_config"
        state: absent
        regexp: '^\s*(CRYPTO_POLICY\s*=.*)$'
      notify: Restart sshd
      tags:
        - 5.2.14

    - name: 5.2.15 - Ensure SSH Banner is configured
      ansible.builtin.lineinfile:
        path: "/etc/ssh/sshd_config"
        line: "Banner /etc/{{ ssh_login_banner }}"
        regexp: "^Banner /etc/{{ ssh_login_banner }}"
        insertafter: "^#Banner none"
      notify: Restart sshd
      tags:
        - 5.2.15

    - name: 5.2.16 - Ensure SSH MaxAuthTires is set to {{ ssh_max_auth_tries }}
      ansible.builtin.lineinfile:
        path: /etc/ssh/sshd_config
        line: "MaxAuthTries {{ ssh_max_auth_tries }}"
        regexp: '^MaxAuthTries\s*[^1-{{ ssh_max_auth_tries | int + 1 }}]'
        insertafter: "^#MaxAuthTries"
      notify: Restart sshd
      tags:
        - 5.2.16

    - name: 5.2.17 - Limit max unauthenticated startups
      ansible.builtin.lineinfile:
        path: "/etc/ssh/sshd_config"
        line: "MaxStartups 10:30:60"
        regexp: '^MaxStartups\s+10:30:60'
        insertafter: '^#MaxStartups\s+10:30:100'
      notify: Restart sshd
      tags:
        - 5.2.17

    - name: 5.2.18 - Limit max sessions to {{ ssh_max_sessions }}
      ansible.builtin.lineinfile:
        path: "/etc/ssh/sshd_config"
        line: "MaxSessions {{ ssh_max_sessions }}"
        regexp: '^MaxSessions\s+[{{ ssh_max_sessions }}]'
        insertafter: '^#MaxSessions\s+10'
      notify: Restart sshd
      tags:
        - 5.2.18

    - name: 5.2.19 - Ensure SSH LoginGraceTime is set to {{ ssh_grace_time }}
      ansible.builtin.lineinfile:
        path: /etc/ssh/sshd_config
        line: "LoginGraceTime {{ ssh_grace_time }}"
        regexp: "^LoginGraceTime {{ ssh_grace_time }}"
        insertafter: "^#LoginGraceTime"
      notify: Restart sshd
      tags:
        - 5.2.19

    - name: 5.2.20 - Ensure SSH Idle Timeout is configured ClientAliveInterval
      ansible.builtin.lineinfile:
        path: /etc/ssh/sshd_config
        line: "ClientAliveInterval {{ ssh_alive_interval }}"
        regexp: "^ClientAliveInterval {{ ssh_alive_interval }}"
        insertafter: "^#ClientAliveInterval"
      notify: Restart sshd
      tags:
        - 5.2.20

    - name: 5.2.20 - Ensure SSH Idle Timeout is configured ClientAliveCountMax
      ansible.builtin.lineinfile:
        path: /etc/ssh/sshd_config
        line: "ClientAliveCountMax {{ ssh_alive_count_max }}"
        regexp: "^ClientAliveCountMax {{ ssh_alive_count_max }}"
        insertafter: "^#ClientAliveCountMax"
      notify: Restart sshd
      tags:
        - 5.2.20

# Make sure the sudoers file includes the requirement to use pty
- name: 5.3.2 - Ensure sudo commands use pty
  ansible.builtin.lineinfile:
    path: /etc/sudoers
    regexp: '^Defaults\s*use_pty'
    line: "Defaults use_pty"
    insertafter: "^# Defaults specification"
    validate: /usr/sbin/visudo -cf %s
  tags:
    - 5.3.2

# Make sure the sudoers file includes the requirement to log to a file
- name: 5.3.3 - Ensure sudo log file exists
  ansible.builtin.lineinfile:
    path: /etc/sudoers
    regexp: '^Defaults\s*logfile="{{ sudo_log }}"'
    line: 'Defaults logfile="{{ sudo_log }}"'
    insertafter: "^# Defaults specification"
    validate: /usr/sbin/visudo -cf %s
  tags:
    - 5.3.3

- name: 5.3.4 - Require password for priviledge escalation
  tags:
    - 5.3.4
  block:
    - name: 5.3.4 - Find any instances of 'NOPASSWD' in /etc/sudoers or /etc/sudoers.d/*
      ansible.builtin.find:
        paths: "{{ item }}"
        file_type: file
        contains: "NOPASSWD"
      register: sudo_nopasswd
      with_fileglob:
        - "/etc/sudoers"
        - "/etc/sudoers.d/*"

    - name: 5.3.4 - Inform the user that instances were found
      ansible.builtin.debug:
        msg: "NOPASSWD was found in a sudoers file, please check and remove if not needed!"
      when: sudo_nopasswd

- name: 5.3.5 - Ensure re-authentication for privilege escalation is not disabled globally
  tags:
    - 5.3.5
  block:
    - name: 5.3.5 - Find any instances of '!authenticate' in /etc/sudoers or /etc/sudoers.d/*
      ansible.builtin.find:
        paths: "{{ item }}"
        file_type: file
        contains: '^[^#].*\!authenticate'
      register: sudo_reauthenticate
      with_fileglob:
        - "/etc/sudoers"
        - "/etc/sudoers.d/*"

    - name: 5.3.5 - Inform the user that instances were found
      ansible.builtin.debug:
        msg: "!authenticate was found in a sudoers file, please check and remove if not needed!"
      when: sudo_reauthenticate

# Control 5.3.6 TODO

- name: 5.3.7 - Restrict su to wheel group
  tags:
    - 5.3.7
  block:
    - name: 5.3.7 - Configure PAM to only allow su from wheel group
      ansible.builtin.replace:
        path: /etc/pam.d/su
        regexp: '^#auth\s+required\s+pam_wheel.so\s+use_uid'
        replace: "auth         required        pam_wheel.so use_uid group=wheel"

    - name: 5.3.7 - Add root to the wheel group
      ansible.builtin.user:
        name: root
        groups: wheel
        append: true


# Control section 5.4, authselect, cannot be used with Red Hat IPA or Microsoft AD
# Skipping until can assure we know how to test against this.

# Control 5.4.3, Set password retention, requries file replacement
# skipping

- name: 5.5.0 - Configure PAM files and password requirements
  tags:
    - 5.5.0
  block:
    - name: 5.5.1 - require at least one digit in passwords
      ansible.builtin.lineinfile:
        path: /etc/security/pwquality.conf
        line: dcredit = -1
        regexp: "^dcredit = -1"
        insertafter: "# dcredit = 0"
      when: password_req_digit

    - name: 5.5.1 - require at least one uppercase letter in passwords
      ansible.builtin.lineinfile:
        path: /etc/security/pwquality.conf
        line: ucredit = -1
        regexp: "^ucredit = -1"
        insertafter: "# ucredit = 0"
      when: password_req_upper

    - name: 5.5.1 - require at least one lowercase letter in passwords
      ansible.builtin.lineinfile:
        path: /etc/security/pwquality.conf
        line: lcredit = -1
        regexp: "^lcredit = -1"
        insertafter: "^# lcredit = 0"
      when: password_req_lower

    - name: 5.5.1 - Require at least one special character in passwords
      ansible.builtin.lineinfile:
        path: /etc/security/pwquality.conf
        line: ocredit = -1
        regexp: "^ocredit = -1"
        insertafter: "^# ocredit = 0"
      when: password_req_digit

    - name: 5.5.1 - Require at least {{ password_min_length }} characters in passwords
      ansible.builtin.lineinfile:
        path: /etc/security/pwquality.conf
        line: minlen = {{ password_min_length }}
        regexp: "^minlen = {{ password_min_length }}"
        insertafter: "^# minlen = 8"
      when: password_req_digit

- name: 5.5.2 - Ensure lockout attempts for failed password attempts is configured
  ansible.builtin.lineinfile:
    path: /etc/security/faillock.conf
    regexp: "^\ *deny\ *=\ *{{ password_failed_attempts }}*$"
    line: "deny = {{ password_failed_attempts }}"
    insertafter: "#\ *deny"
    owner: root
    group: root
    mode: 0600
  tags:
    - 5.5.2

- name: 5.5.2 - Ensure lockout time for failed password attempts is configured
  ansible.builtin.lineinfile:
    path: /etc/security/faillock.conf
    regexp: "^\ *unlock_time\ *=\ *{{ password_failed_time }}*$"
    line: "unlock_time={{ password_failed_time }}"
    insertafter: "#\ *deny"
    owner: root
    group: root
    mode: 0600
  tags:
    - 5.5.2

# 5.5.3 - Password retention involves configuring pam.conf, skipping

- name: 5.5.4 - Ensure password hashing algorithm is SHA-512 or yescrypt
  ansible.builtin.replace:
    path: /etc/libuser.conf
    regexp: "^crypt_style\ *=\ *(sha512|yescrypt)"
    replace: "crypt_style = {{ password_hash_alg | lower }}"
    after: "[defaults]"
    owner: root
    group: root
    mode: 0644
  tags:
    - 5.5.4

- name: 5.5.4 - Ensure password hashing algorithm is SHA-512 or yescrypt
  ansible.builtin.replace:
    path: /etc/login.defs
    regexp: "^ENCRYPT_METHOD\ *(SHA512|YESCRYPT)"
    replace: "ENCRYPT_METHOD {{ password_hash_alg | upper }}"
  tags:
    - 5.5.4

- name: 5.6.1.1 - Ensure password expiration is {{ password_expire_days }} days or less
  ansible.builtin.lineinfile:
    dest: /etc/login.defs
    regexp: '^PASS_MAX_DAYS\s*((?!{{ password_expire_days }}).)*$'
    line: "PASS_MAX_DAYS  {{ password_expire_days }}"
    state: present
  tags:
    - 5.6.1.1

- name: 5.6.1.2 - Ensure password change days is set to {{ password_min_days }}
  ansible.builtin.lineinfile:
    dest: /etc/login.defs
    regexp: '^PASS_MIN_DAYS\s*((?!{{ password_min_days }}).)*$'
    line: "PASS_MIN_DAYS  {{ password_min_days }}"
    state: present
  tags:
    - 5.6.1.2

- name: 5.6.1.3 - Ensure password warning days is set to {{ password_warning_days }}
  ansible.builtin.lineinfile:
    dest: /etc/login.defs
    regexp: '^PASS_WARN_AGE\s*((?!{{ password_warning_days }}).)*$'
    line: "PASS_WARN_AGE  {{ password_warning_days }}"
    state: present
  tags:
    - 5.6.1.3

# We need to do this the hard way because the user module that calls /usr/sbin/useradd does not support setting inactive days
# The defaults perms are 0644 on the file, but after useradd is run against it, it changes to 0600, so we'll change it as well
- name: 5.6.1.4 - Disable accounts that are inactive for {{ password_inactive_lock_days }} days after password expiration
  ansible.builtin.replace:
    path: /etc/default/useradd
    regexp: "^INACTIVE=((?!{{ password_inactive_lock_days }}).)*$"
    replace: "INACTIVE={{ password_inactive_lock_days }}"
    owner: root
    group: root
    mode: 0600
  tags:
    - 5.6.1.4

# 5.6.1.5, Ensure all users last password change date is in the past,
# is not easily automated. Will revisit later

# 5.6.2, Ensure system accounts are secured, requires manual intervention, skipping

- name: 5.6.3 - Ensure default shell timeout is {{ shell_timeout }} seconds or less
  ansible.builtin.blockinfile:
    path: "{{ item }}"
    block: |
      TMOUT={{ shell_timeout }}
      export TMOUT
    marker: "# {mark} Ansible Managed CIS Timeout"
  loop:
    - /etc/bashrc
    - /etc/profile
  tags:
    - 5.6.3

# Control is actually setting to GID of 0 and the user module takes a group name, not a GID, so have to use usermod
- name: 5.6.4 - Ensure default group for root is GID 0
  ansible.builtin.command: /usr/sbin/usermod -g 0 root
  changed_when: false
  tags:
    - 5.6.4

- name: 5.6.5 - Ensure default user umask is set
  ansible.builtin.replace:
    path: "{{ item }}"
    replace: "     umask {{ default_umask }}"
    regexp: '^\s*umask\s*022'
  loop:
    - /etc/bashrc
    - /etc/profile
    - /etc/login.defs
  tags:
    - 5.6.5

# The user module here uses a known salt to idompotently set the password for multiple runs
#  see https://docs.ansible.com/ansible/latest/user_guide/playbooks_filters.html#hash-filters
- name: 5.6.6 - Set root password
  tags:
    - 5.6.6
  block:
    - name: 5.6.6 - Check if root has a password
      ansible.builtin.lineinfile:
        path: /etc/shadow
        regexp: '^root:[*\!|*\*]*:'
        state: absent
      check_mode: true
      changed_when: false
      register: root_pw_check
      failed_when: false

    - name: 5.6.6 - Set root password
      ansible.builtin.user:
        name: root
        password: "{{ 'root_password' | password_hash('sha512', 65534 | random(seed=inventory_hostname) | string) }}"
      when: root_pw_check.found != "0" and root_password is defined

# Section 6 - System Maintenance

- name: 6.1.[1-4] - Ensure permissions on /etc/passwd[-], /etc/group[-]
  ansible.builtin.file:
    path: /etc/{{ item }}
    owner: root
    group: root
    mode: 0644
  loop:
    - passwd
    - passwd-
    - group
    - group-
  tags:
    - 6.1.1
    - 6.1.2
    - 6.1.3
    - 6.1.4

- name: 6.1.[5-8] - Ensure permissions on /etc/shadow[-], /etc/gshadow
  ansible.builtin.file:
    path: /etc/{{ item }}
    owner: root
    group: root
    mode: 0000
  loop:
    - shadow
    - shadow-
    - gshadow
    - gshadow-
  tags:
    - 6.1.5
    - 6.1.6
    - 6.1.7
    - 6.1.8

# Control 6.1.10, Ensure no world writable files exist, is system dependent so we are only
# providing a list to the user here.
- name: 6.1.9 - Ensure no world writable files exist
  tags:
    - 6.1.9
  block:
    - name: 6.1.9 - Find any world writiable files
      ansible.builtin.shell: "/usr/bin/df --local -P | /usr/bin/awk {' if (NR!=1) print $6'} | /usr/bin/xargs -I '{}' find '{}' -xdev -type f -perm -0002"
      register: ww_files
      changed_when: false
      check_mode: false

    - name: 6.1.9 - Print any world writable files found
      ansible.builtin.debug:
        msg: "World writiable files found: {{ ww_files.stdout }}"
      changed_when: true
      when: ww_files.stdout

# Control 6.1.10 Ensure no unowned files exist, is system dependent so we are only
# providing a list to the user here.
- name: 6.1.10 - Ensure no unowned files exist
  tags:
    - 6.1.10
  block:
    - name: 6.1.10 - Find any unowned files
      ansible.builtin.shell: "/usr/bin/df --local -P | /usr/bin/awk {' if (NR!=1) print $6'} | /usr/bin/xargs -I '{}' find '{}' -xdev -nouser"
      register: uo_files
      changed_when: false
      check_mode: false

    - name: 6.1.10 - Print any unowned files found
      ansible.builtin.debug:
        msg: "unowned files found: {{ uo_files.stdout }}"
      changed_when: true
      when: uo_files.stdout

- name: 6.1.11 - Ensure no ungrouped files exist
  tags:
    - 6.1.11
  block:
    - name: 6.1.11 - Find any ungrouped files
      ansible.builtin.shell: "/usr/bin/df --local -P | /usr/bin/awk {' if (NR!=1) print $6'} | /usr/bin/xargs -I '{}' find '{}' -xdev -nogroup"
      register: ug_files
      changed_when: false
      check_mode: false

    - name: 6.1.11 - Print any ungrouped files found
      ansible.builtin.debug:
        msg: "ungrouped files found: {{ uo_files.stdout }}"
      changed_when: true
      when: ug_files.stdout


  # Find all local filesystem directories and set the sticky bit on world writable ones
- name: 6.1.12 - Ensure sticky bit is set on world-writeable directories
  ansible.builtin.shell: >
    set -o pipefail ;
    /usr/bin/df --local -P | awk '{if (NR!=1) print $6}' | xargs -I '{}' find '{}' -xdev -type d \( -perm -0002 -a ! -perm -1000 \) 2>/dev/null |
    xargs -I '{}' chmod a+t '{}'
  changed_when: false
  tags:
    - 6.1.12


# Control 6.1.13, Audit SUID executables, is a verification and is system dependent.
# Not implementing because it will always return some SUID files
# Manually review the control

# Control 6.1.14, Audit SGID executables, is a verification and is system dependent.
# Not implementing because it will always return some SUID files
# Manually review the control

# Contorl 6.1.15, Audit system file permissions requires manual intervention, skipping

- name: 6.2.1 - Ensure accounts in /etc/passwd use shadowed passwords
  tags:
    - 6.2.1
  block:
    - name: 6.2.1 - Check to see if there are any accounts with empty passwords
      ansible.builtin.shell: "/usr/bin/cat /etc/shadow | awk -F: '($2 == \"\" ) { print $1 }'"
      changed_when: false
      register: empty_passwords
      check_mode: false

    - name: 6.2.1 - Report the named users to the report
      ansible.builtin.debug:
        msg: "The user {{ item }} has an empty password"
      when: empty_passwords.stdout
      changed_when: true
      loop: "{{ empty_passwords.stdout_lines }}"

- name: 6.2.2 - /etc/shadow password fields are not empty
  ansible.builtin.command: "awk -F: '($2 == \"\")' {{ item }}"
  loop:
    - /etc/shadow
  changed_when: false
  tags:
    - 6.2.2

- name: 6.2.3 - Report on groups in /etc/passwd  with a GID not in /etc/group
  tags:
    - 6.2.3
  block:
    - name: 6.2.3 - Use script to pull the list of groups
      ansible.builtin.script:
        cmd: files/undefined_groups.sh
      register: undefined_groups
      changed_when: false
      check_mode: false

    - name: 6.2.3 - Report to user any unreferenced groups
      ansible.builtin.debug:
        msg: "{{ undefined_groups.stdout_lines }}"
      changed_when: true
      when: undefined_groups.stdout

- name: 6.2.4 - Report if shadow group exists in /etc/group
  block:
    - name: 6.2.4 - Determine if the shadow group exists in /etc/group
      ansible.builtin.command: /bin/grep "^shadow:" /etc/group
      register: shadow_out
      changed_when: false
      failed_when: shadow_out.rc == "2"

    - name: 6.2.4 - Print report of shadow group in /etc/group to user
      ansible.builtin.debug:
        msg: "Shadow group exists in /etc/group. Remove"
      changed_when: true
      when: shadow_out.stdout


- name: 6.2.4 - Ensure no duplicate UIDs exist
  tags:
    - 6.2.4
  block:
    - name: 6.2.3 - find accounts with UID of 0
      ansible.builtin.shell: "/usr/bin/cat /etc/passwd | awk -F: '($3 == 0) { print $1 }'"
      register: rootuid
      changed_when: rootuid.rc >= 2
      check_mode: false

    - name: 6.2.4 - Use script to pull the list of duplicate UIDs
      ansible.builtin.script:
        cmd: files/duplicate_uids.sh
      register: duplicate_uids
      changed_when: false
      check_mode: false

    - name: 6.2.4 - Print report of duplicated UIDs to user
      ansible.builtin.debug:
        msg: "{{ duplicate_uids.stdout_lines }}"
      changed_when: true
      when: duplicate_uids.stdout

- name: 6.2.5 - Report on duplicate GIDs in /etc/group
  tags:
    - 6.2.5
  block:
    - name: 6.2.5 - Use script to pull the list of duplicate GIDs
      ansible.builtin.script:
        cmd: files/duplicate_guids.sh
      register: duplicate_guids
      changed_when: false
      check_mode: false

    - name: 6.2.5 - Print report of duplcate GIDs to user
      ansible.builtin.debug:
        msg: "{{ duplicate_guids.stdout_lines }}"
      changed_when: true
      when: duplicate_guids.stdout

- name: 6.2.6 - Report on duplicate users in /etc/passwd
  tags:
    - 6.2.6
  block:
    - name: 6.2.6 - Use script to pull the list of users
      ansible.builtin.script:
        cmd: files/duplicate_users.sh
      register: duplicate_users
      changed_when: false
      check_mode: false

    - name: 6.2.6 - Print report of duplicate users to user
      ansible.builtin.debug:
        msg: "{{ duplicate_users.stdout_lines }}"
      changed_when: true
      when: duplicate_users.stdout

- name: 6.2.7 - Report on duplicate groups in /etc/group
  tags:
    - 6.2.7
  block:
    - name: 6.2.7 - Use script to pull the list of groups
      ansible.builtin.script:
        cmd: files/duplicate_groups.sh
      register: duplicate_groups
      changed_when: false
      check_mode: false

    - name: 6.2.7 - Print report of duplicate groups to user
      ansible.builtin.debug:
        msg: "{{ duplicate_groups.stdout_lines }}"
      changed_when: true
      when: duplicate_groups.stdout

- name: 6.2.8 - Ensure root PATH integrity
  tags:
    - 6.2.8
  block:
    - name: 6.2.8 - Run script on path variable
      ansible.builtin.script: files/path_check.sh
      changed_when: false
      register: path_check
      check_mode: false

    - name: 6.2.8 - Print report to user
      ansible.builtin.debug:
        msg:
          - "Note, Ansible runs this as SUDO with the ansible user's PATH variable. The script may not print issues"
          - "that exist in root's path because of this. It should be run as root on the target machine manually."
          - " {{ path_check.stdout }}"
      when: path_check.stdout and not ansible_check_mode

- name: 6.2.9 - Ensure root is the only UID 0 account
  tags:
    - 6.2.9
  block:
    - name: 6.2.9 - find accounts with UID of 0
      ansible.builtin.shell: "/usr/bin/cat /etc/passwd | awk -F: '($3 == 0) { print $1 }'"
      register: rootuid
      changed_when: rootuid.rc == 2
      check_mode: false

    - name: 6.2.9 - Report on mulitple accounts with UID of 0
      ansible.builtin.debug:
        msg:
          - "Accounts with UID zero in addition to root"
          - " {{ rootuid.stdout_lines }}"
      changed_when: true
      when: rootuid.stdout != 'root'

- name: 6.2.10 - Report on users that do not have a home directory
  tags:
    - 6.2.10
  block:
    - name: 6.2.10 - Use script to find the users
      ansible.builtin.script:
        cmd: files/non_existant_homedirs.sh
      register: nohomedir
      changed_when: false

    - name: 6.2.10 - Print report of users that do not have a home directory
      ansible.builtin.debug:
        msg: "{{ nohomedir.stdout_lines }}"
      changed_when: true
      when: nohomedir.stdout

# Control 6.2.11 - Ensure users own their home directories is environment dependent, skipping
# Control 6.2.12 - Ensure users home dir permissions are set is environment dependent, skipping
# Control 6.2.13 - Ensure users .netrc files are removed is environment dependent, skipping
# Control 6.2.14 - Ensure users .forward files are removed is environment dependent, skipping
# Control 6.2.15 - Ensure users .rhosts files are removed is environment dependent, skipping
# Control 6.2.16 - Ensure users dot files are not gorup or world writable is environment dependent, skipping
