---
# 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 }} type systems"

  # 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.8 - Ensure updated system
    ansible.builtin.package:
      name: "*"
      state: latest
      security: true
    when: update_system
    tags:
      - 1.8.0

  # 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: 1.1 Disable unused filesystems
    ansible.builtin.set_fact:
      unused_filesystems: []

  - name: 1.1.1.1 - Add cramfs to list of unused filesystems
    ansible.builtin.set_fact:
      unused_filesystems: "{{ unused_filesystems + [ 'cramfs' ] }}"
    tags:
      - 1.1.1.1

  - name: 1.1.1.2 - Add freevxfs to list of unused filesystems
    ansible.builtin.set_fact:
      unused_filesystems: "{{ unused_filesystems + [ 'freevxfs' ] }}"
    tags:
      - 1.1.1.2

  - name: 1.1.1.3 - Add jffs2 to list of unused filesystems
    ansible.builtin.set_fact:
      unused_filesystems: "{{ unused_filesystems + [ 'jffs2' ] }}"
    tags:
      - 1.1.1.3

  - name: 1.1.1.4 - Add hfs to list of unused filesystems
    ansible.builtin.set_fact:
      unused_filesystems: "{{ unused_filesystems + [ 'hfs' ] }}"
    tags:
      - 1.1.1.4

  - name: 1.1.1.5 - Add hfsplus to list of unused filesystems
    ansible.builtin.set_fact:
      unused_filesystems: "{{ unused_filesystems + [ 'hfsplus' ] }}"
    tags:
      - 1.1.1.5

  - name: 1.1.1.6 - Add squashfs to list of unused filesystems
    ansible.builtin.set_fact:
      unused_filesystems: "{{ unused_filesystems + [ 'squashfs' ] }}"
    tags:
      - 1.1.1.6

  - name: 1.1.1.7 - Add udf to list of unused filesystems
    ansible.builtin.set_fact:
      unused_filesystems: "{{ unused_filesystems + [ 'udf' ] }}"
    tags:
      - 1.1.1.7

  - name: 1.1.1.8 - Add vfat to list of unused filesystems
    ansible.builtin.set_fact:
      unused_filesystems: "{{ unused_filesystems + [ 'vfat' ] }}"
    tags:
      - 1.1.1.8

  # With the list complete, use it with the system's package manager
  #  to remove packages from the system that are not needed.
  - name: Remove unused_filesystem list
    ansible.builtin.package:
      name: unused_filesystems
      state: absent

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

  # 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.2 - /tmp partition and mount options
    block:
      # Create a empty integer variable and set it as a fact on the managed
      #  machine.
      - name: 1.1.2 - Set/reset mount counter
        ansible.builtin.set_fact:
          mount_count: 0
        tags:
          - 1.1.2
          - 1.1.3
          - 1.1.4

      # 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.2 - Determine if /tmp is on a separate partition
        ansible.builtin.set_fact:
          mount_count: "addition{{ mount_count + 1 }}"
          mount_options: "{{ item.options }}"
        when: item.mount == "/tmp"
        with_items:
          - "{{ ansible_mounts }}"
        tags:
          - 1.1.2

      # 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.2 - Report to user if not on separate partition
        ansible.builtin.debug:
          msg: "FAILED CONTROL: /tmp is not on a separate partition. Skipping mount option checks"
        when: mount_count == 0
        changed_when: true
        tags:
          - 1.1.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 - Report to user if /tmp does not have noexec set
        ansible.builtin.debug:
          msg: "FAILED CONTROL: /tmp/ does not have noexec set"
        when: mount_options is defined and "noexec" not in mount_options and mount_count == 0
        changed_when: true
        tags:
          - 1.1.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 - Report to user if /tmp does not have nodev set
        ansible.builtin.debug:
          msg: "FAILED CONTROL: /tmp/ does not have nodev set"
        when: mount_options is defined and "nodev" not in mount_options and mount_count == 0
        changed_when: true
        tags:
          - 1.1.4

      # 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 - Report to user if tmp does not have nosuid set
        ansible.builtin.debug:
          msg: "FAILED CONTROL: /tmp does not have nosuid set"
        when: mount_options is defined and "nodsuid" not in mount_options and mount_count == 0
        changed_when: true
        tags:
          - 1.1.5

    # This whole block can be turned off by excluding the following tag(s)
    tags:
      1.1.2

  # 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 - Report if /var is not on a separate partition
    block:
      # Create a empty integer variable and set it as a fact on the managed
      #  machine.
      - name: 1.1.6 - Set/reset 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 - 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.6 - Report to user
        ansible.builtin.debug:
          msg: "FAILED CONTROL: /var is not on a separate partition"
        when: mount_count == 0
        changed_when: true
    # This whole block can be turned off by excluding the following tag(s)
    tags:
      - 1.1.6

  # 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 - /var/tmp partition and mount options
    block:
      # Create a empty integer variable and set it as a fact on the managed
      #  machine.
      - name: 1.1.7 - Set/reset mount counter
        ansible.builtin.set_fact:
          mount_count: 0
        tags:
          - 1.1.7
          - 1.1.8
          - 1.1.9
          - 1.1.10

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

      # 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 - Report to user if 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.7

      # 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.8 - 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 and mount_count == 0
        changed_when: true
        tags:
          - 1.1.8

      # 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.9 - 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 and mount_count == 0
        changed_when: true
        tags:
          - 1.1.9

      # 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.10 - 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 and mount_count == 0
        changed_when: true
        tags:
          - 1.1.10
    # This whole block can be turned off by excluding the following tag(s)
    tags:
      - 1.1.7

  # 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.11 - Report if /var/log is not on a separate partition
    block:
      # Create a empty integer variable and set it as a fact on the managed
      #  machine.
      - name: 1.1.11 - Set/reset 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.11 - 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 }}"

      # 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.11 - Report to user
        ansible.builtin.debug:
          msg: "FAILED CONTROL: /var/log is not on a separate partition"
        when: mount_count == 0
        changed_when: true
    # This whole block can be turned off by excluding the following tag(s)
    tags:
      - 1.1.11

  # 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.12 - Report if /var/log/audit is not on a separate partition
    block:
      # Create a empty integer variable and set it as a fact on the managed
      #  machine.
      - name: 1.1.12 - Set/reset 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.12 - 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 }}"

      # 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.12 - Report to user
        ansible.builtin.debug:
          msg: "FAILED CONTROL: /var/log/audit is not on a separate partition"
        when: mount_count == 0
        changed_when: true
    # This whole block can be turned off by excluding the following tag(s)
    tags:
      - 1.1.12

  # 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.13 - Report if /home is not on a separate partition
    block:
      # Create a empty integer variable and set it as a fact on the managed
      #  machine.
      - name: 1.1.13 - Set/reset 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.13 - 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 }}"

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

      # 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: 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 and mount_count == 0
        changed_when: true
    tags:
    # This whole block can be turned off by excluding the following tag(s)
      - 1.1.13

  # /dev/shm does not exist in ansible_mounts so we have to check the
  #  mount ansible.builtin.command directly. This requires the use of the shell ansible.builtin.command which
  #  is not ideal.
  # Grep out /dev/shm and see if the given option is set.
  - name: 1.1.15 - Report if /dev/shm does not have nodev set
    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.15 - Report to user
        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:
    # This whole block can be turned off by excluding the following tag(s)
      - 1.1.15

  # Grep out /dev/shm and see if the given option is set.
  - name: 1.1.16 - Report if /dev/shm does not have nosuid set
    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

  # Let the user know if we did not find the option set.
      - name: 1.1.16 - Report to user
        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
    tags:
    # This whole block can be turned off by excluding the following tag(s)
      - 1.1.16

  # Grep out /dev/shm and see if the given option is set.
  - name: 1.1.17 - Report if /dev/shm does not have noexec set
    block:
      - name: 1.1.17 - 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

  # Let the user know if we did not find the option set.
      - name: 1.1.17 - Report to user
        ansible.builtin.debug:
          msg: "FAILED CONTROL: /dev/shm does not have noexec set"
        when: devexec_nosuid_out is defined and devshm_noexec_out.stdout
        changed_when: true
    tags:
    # This whole block can be turned off by excluding the following tag(s)
      - 1.1.17

# Control 1.1.18, 1.1.19, 1.1.20 are for removable media

# REDO TO REPORT AND ACTION
  # Finda all local filesystem directories and set the sticky bit on world writable ones
#  - name: 1.1.21 - 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:
#      - 1.1.21

  # 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.service:
      name: autofs
      enabled: false
      state: stopped
    when: "'autofs' in ansible_facts.packages"
    tags:
      - 1.1.22


# Control 1.2.1 and 1.2.4 are system updating. Make sure system is set for some kind of system software update

  # Use the service module to disable the rhnsd service. If you want the machine
  #  to respond to queued services from Satellite, do not disable this.
  - name: 1.2.5 Disable rhnsd
    ansible.builtin.service:
      name: rhnsd
      enabled: false
      state: stopped
    ignore_errors: true # Remove for RHEL
    when: ansible_distribution == "RedHat"
    tags:
      - 1.2.5

  # 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.[2,3] - Ensure GPG keys are configured
    block:
      # Replace any instances of gpgcheck with a 1 after it to 'gpgcheck = 1'
      - name: 1.2.2 - set master yum.conf gpgcheck to '1'
        ansible.builtin.replace:
          dest: /etc/yum.conf
          regexp: '^gpgcheck\s*=\s*[^1]*$'
          replace: "gpgcheck = 1"
        when: gpgcheck is defined and gpgcheck

      # Find all files in /etc/yum.repos.d and add them to a list variable
      - name: 1.2.3 - find all repo files in /etc/yum.repos.d/
        ansible.builtin.find:
          paths: "/etc/yum.repos.d"
          patterns: "*.repo"
        register: yumrepos
        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.3 - Set all repos gpgchecks to '1'
        ansible.builtin.replace:
          dest: "{{ item.path }}"
          regexp: '^gpgcheck\s*=\s*[^1]*$'
          replace: gpgcheck = 1
        with_items: "{{ yumrepos.files }}"
        when: gpgcheck is defined and gpgcheck
    when: ansible_distribution != "SLES"
    tags:
      - 1.2.2
      - 1.2.3

  # - name: 1.2.1 - Ensure package manager repositories are configured, site dependent


  # 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
    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=ansible.builtin.file:((?!{{ aide_db_name }}).)*$"
          replace: "database=ansible.builtin.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=ansible.builtin.file:((?!{{ aide_new_db_name }}).)*$"
          replace: "database_out=ansible.builtin.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: 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

      # 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)
        ansible.builtin.template:
          src: aidecheck.service
          dest: /etc/systemd/system/aidecheck.service
          owner: root
          group: root
          mode: 0644
          setype: systemd_unit_file_t
        notify: Restart aidecheck
        tags:
          - 1.3.2

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

      # 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
        notify: Restart aidecheck
        tags:
          - 1.3.2
    tags:
      - 1.3.0

  # 1.4 Secure Boot settings
  # Determine if we are using LILO or EFI
  - name: 1.4.0 - Check if the EFI directory exists
    ansible.builtin.stat:
      path: "/boot/efi/EFI/{{ ansible_distribution | lower }}/grub.cfg"
    register: efidir
    tags:
      1.4.1

  - name: 1.4.1 - set variable for grub.cfg in EFI location
    ansible.builtin.set_fact:
      grub_cfg_path: "{{ efidir.stat.path }}"
    when: efidir.stat.path is defined
    tags:
      1.4.1

  - 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.1 - set variable for grub.cfg in LILO location
    ansible.builtin.set_fact:
      grub_cfg_path: "{{ grubdir.stat.path }}"
    when: grubdir.stat.path is defined
    tags:
      1.4.1

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

  # Control 1.4.2, Grub bootloader password - skipped

  # Use replace module to add the requirement to enter password on single user startup
  - name: 1.4.3 - Set single user password
    ansible.builtin.replace:
      dest: /usr/lib/systemd/system/{{ item }}
      regexp: '^ExecStart=-((?!/bin/sh\s+-c\s+\"\s+/sbin/sulogin).)*'
      replace: "ExecStart=-/bin/sh -c \"/sbin/sulogin; /usr/bin/systemctl --fail --no-block default\""
    with_items:
      - rescue.service
      - emergency.service
    tags:
      - 1.4.3

  # 1.5 Additional Process Hardening

  - name: 1.5.1 - Ensure core dumps are restricted
    block:
      # The sysctl module will set variables in /etc/sysctl.conf and tell sysctl
      #  to reload them immediately if 'reload' is set to 'true'.
      - name: 1.5.1 - Ensure core dumps are restricted
        ansible.builtin.sysctl:
          name: fs.suid_dumpable
          value: "0"
          state: present
          reload: true

      # The pam_limits module will configure the lines in the limits files.
      - name: 1.5.1 - Ensure core limits are set
        community.general.pam_limits:
          dest: /etc/security/limits.d/CIS.conf
          domain: "*"
          limit_type: hard
          limit_item: core
          value: "0"
    tags:
      - 1.5.1

  # Enable the No Execute / Execute Disable functionality on processors
  - name: 1.5.2 Ensure XD/NX support is enabled
    block:
      # To see if it is set already, we poll the joural with a grep and register a variable
      - name: search journal to see if protection was active at boot
        ansible.builtin.shell: "/usr/bin/journalctl | /usr/bin/grep 'protection: active' "
        register: nx_protection
        changed_when: false
        check_mode: false

      # If we can't verify that it is active, we notify the user. This has to be set
      #  in the BIOS or with a special kernel (32 bit kernels only)
      - name: Verify XD/NX support is active
        ansible.builtin.debug:
          msg: "XD/NX support is active"
        when: nx_protection.stdout is search("active")
    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.builtin.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 the prelink package
  - name: 1.5.4 - Remove prelink package
    ansible.builtin.package:
      name: prelink
      state: absent
    tags:
      - 1.5.4

  # 1.6 Mandatory Access Control

  # SLES does not provide a policy for selinux, so enabling it will freeze a system. For SLES skip
  #  This section
  - name: 1.6.0 - Mandatory Access Control
    block:
      # Use system package manager to install libselinux (RHEL CLones)
      - name: 1.6.2 - Ensure SELinux is installed
        ansible.builtin.package:
          name: libselinux
          state: present
        when: (selinux is defined and selinux != "Disabled") and ansible_distribution == "RedHat"
        tags:
          - 1.6.2

      # 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.1 - 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 != "Disabled"
        notify: Rebuild grub
        tags:
          - 1.6.1.1

      # re-gather system facts if 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: Regather facts
        ansible.builtin.setup:
        tags:
          - 1.6.1.1

      # Replace the current selinux policy with whatever the variable is set for
      - name: 1.6.1.2 - 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 != "Disabled"
        tags:
          - 1.6.1.2

      # 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.2 - 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"
        notify: Reboot
        tags:
          - 1.6.1.2

      # Replace the current selinux mode with what the variable is set to
      - name: 1.6.1.3 - 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" )
        tags:
          - 1.6.1.3

      # Use system package manager to remove package
      - name: 1.6.1.4 - Remove setroubleshoot
        ansible.builtin.package:
          name: "{{ item }}"
          state: absent
        loop:
          - setroubleshoot
          - setroubleshoot-server
          - setroubleshoot-plugins
        tags:
          - 1.6.1.4

      # Use system package manager to remove package
      - name: 1.6.1.5 - Remove MCS Translation Service
        ansible.builtin.package:
          name: mcstrans
          state: absent
        tags:
          - 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
        block:
          # use ps and grep to find services running under initrc context
          - name: 1.6.1.5 - Generate report on unconfined running services
            ansible.builtin.shell: /usr/bin/ps -eZ | egrep "initrc"
            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.5 - 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
        tags:
          - 1.6.1.5
    when: ansible_distribution != "SLES"
    tags:
      - 1.6.0

  # 1.7 Warning Banners

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

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

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

  # add a banner to the login screen if the graphical_interface variable is set to true
  - name: 1.7.2 Ensure GDM banner set up
    block:
      # Add our required pieces to the dconf file for GDM
      - name: 1.7.2 - Set up the dconf profile for GDM
        ansible.builtin.file:
          path: /etc/dconf/profile/gdm
          owner: root
          group: root
          mode: 0644
          block: |
            user-db:user
            system-db:gdm
            file-db:/usr/share/gdm/greeter-dconf-defaults

      # Set the greeter message
      - name: 1.7.2 - Set GDM to use banner message
        ansible.builtin.file:
          path: /etc/dconf/db/gdm.d/01-banner-message
          owern: root
          group: root
          mode: 0644
          block: |
            [org/gnome/login-screen]
            banner-message-enable=true
            banner-message-text='Authorized uses only. All activity may be monitored and reported.'
    when: graphical_inteface is defined and graphical_interface
    tags:
      - 1.7.2

 ### Part 2, Services ###
  # Remove old, unused, insecure services
  - name: 2.1.7 - Remove xinetd  service  [controlled by host variable tftp_server]
    ansible.builtin.package:
      name: xinetd
      state: absent
    when: tftp_server is defined and not tftp_server
    tags:
      - 2.1.7

  # use replace to make sure that listed services are disabled
  - name: Disable non-tftp services (if we are a tftp server)
    ansible.builtin.replace: regexp="disable\s+=\s+[^yes]" replace="  disable = yes" dest=/etc/xinetd.d/{{ item }}
    with_items:
      - chargen-dgram
      - chargen-stream
      - chargen-dgram
      - daytime-dgram
      - daytime-stream
      - discard-dgram
      - discard-stream
      - echo-dgram
      - tcpmux-server
      - time-dgram
      - time-stream
    when: tftp_server is defined and tftp_server
    ignore_errors: true
    tags:
      - 2.1.2

  # Set up the time server listed in the time_service variable
  - name: 2.2.1.1 - Verify {{ time_service }} is installed
    ansible.builtin.package:
      name: "{{ time_service }}"
      state: present
    when: time_service == "chrony" or time_service == "ntp"
    tags:
      - 2.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.
  - name: 2.2.1.2 - Configure {{ time_service }}
    ansible.builtin.template:
      src: "{{ time_service }}.conf"
      dest: /etc/{{ time_service }}.conf
      owner: root
      group: root
      mode: 0644
    when: time_service == "chrony" or time_service == "ntp"
    notify: Restart {{ time_service }}d
    tags:
      - 2.2.1.2
      - 2.2.1.3

  - name: 2.2.1.3 - configure sysconfig time_server options
    ansible.builtin.template:
      src: "{{ time_service }}d"
      dest: /etc/sysconfig/{{ time_service }}d
      owner: root
      group: root
      mode: 0644
    when: time_service == "chrony" or time_service == "ntp"
    notify: Restart {{ time_service }}d
    tags:
      - 2.2.1.3

  # Chrony and ntp argue if they are both active at the same time. Disable
  #  the other service.
  - name: 2.2.1.2 - disable chronyd if time_service is set to ntp
    ansible.builtin.systemd:
      name: chronyd
      state: stopped
      enabled: false
    when: time_server is defined and time_service == "ntp"
    tags:
      - 2.2.1.2
      - 2.2.1.3

  - name: 2.2.1.2 - disable ntpd if time_service is set to chrony
    ansible.builtin.systemd:
      name: ntpd
      state: stopped
      enabled: false
    when: time_service is defined and time_service == "chrony"
    ignore_errors: true
    tags:
      - 2.2.1.2
      - 2.2.1.3

  # Disable the display manager by changing the default boot target to multi-user
  - name: 2.2.2 - disable display manager if graphical desktop not needed
    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: 2.2.2 - get default runlevel
        ansible.builtin.stat:
          path: /etc/systemd/system/default.target
        register: default_runlevel_out
        tags:
          - 2.2.2

      # Use systemd module to stop the GDM service
      - name: 2.2.2 - Disable the gdm display manager
        ansible.builtin.systemd:
          name: gdm
          enabled: false
          masked: true
          state: stopped
          daemon-reload: true
        when: "'gdm' in ansible_facts.packages and not graphical_interface"
        tags:
          - 2.2.2

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

      - name: 2.2.2 - 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:
          - 2.2.2

      # Set the default run level. We are doing it the hard way since systemctl doesn't handle set-default
      - name: 2.2.2 - 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 and ansible_distribution != "SLES"

      - name: 2.2.2 - 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 and ansible_distribution != "SLES"
    tags:
      - 2.2.2

  # 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: create empty list for unneeded packages
    ansible.builtin.set_fact:
      unneeded_packages: []
    tags:
      - always

  - name: 2.2.3 - add avahi to unneeded package list
    ansible.builtin.set_fact:
      unneeded_packages: "{{ unneeded_packages + [ 'avahi' ] }}"
    tags:
      - 2.2.3

  - name: 2.2.5 - Disable dhcpd server [controlled by host variable dhcp_server]
    ansible.builtin.set_fact:
      unneeded_packages: "{{ unneeded_packages + [ 'dhcp' ] }}"
    when: dhcp_server is defined and not dhcp_server
    tags:
      - 2.2.5

  - name: 2.2.6 - add openldap-servers to removal list
    ansible.builtin.set_fact:
      unneeded_packages: "{{ unneeded_packages + [ 'openldap-servers' ] }}"
    tags:
      - 2.2.6

  - name: 2.2.7 - 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.7

  - name: 2.2.8 - 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.8

  - name: 2.2.9 - 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.9

  - name: 2.2.10 - 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.10

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

  - name: 2.2.12 - 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.12

  - name: 2.2.13 - add squid to removal list
    ansible.builtin.set_fact:
      unneeded_packages: "{{ unneeded_packages + [ 'squid' ] }}"
    when: not proxy_server
    tags:
      - 2.2.13

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

  - name: 2.2.16 - add ypserv to removal list
    ansible.builtin.set_fact:
      unneeded_packages: "{{ unneeded_packages + [ 'ypserv' ] }}"
    tags:
      - 2.2.16

  - name: 2.3.1 - add ypbind to removal list
    ansible.builtin.set_fact:
      unneeded_packages: "{{ unneeded_packages + [ 'ypbind' ] }}"
    when: not ypbind
    tags:
      - 2.3.1

  - name: 2.2.17 - add rsh to unneeded package list
    ansible.builtin.set_fact:
      unneeded_packages: "{{ unneeded_packages + [ 'rsh' ] }}"
    tags:
      - 2.2.17

  - name: 2.2.18 - add talk to unneeded package list
    ansible.builtin.set_fact:
      unneeded_packages: "{{ unneeded_packages + [ 'talk-server' ] + [ 'talk' ] }}"
    tags:
      - 2.2.18
      - 2.3.3

  - name: 2.2.19 - add telnet to unneeded package list
    ansible.builtin.set_fact:
      unneeded_packages: "{{ unneeded_packages + [ 'telnet-server' ] + [ 'telnet'] }}"
    tags:
      - 2.2.19
      - 2.3.4

  - name: 2.2.20 - add tftp to unneeded package list
    ansible.builtin.set_fact:
      unneeded_packages: "{{ unneeded_packages + [ 'tftp' ] + [ 'tftp-server'] }}"
    when: not tftp_server
    tags:
      - 2.2.20

  - name: 2.2.21 - add rsync to unneeded package list
    ansible.builtin.set_fact:
      unneeded_packages: "{{ unneeded_packages + [ 'rsync' ] }}"
    tags:
      - 2.2.21

  - name: 2.2.21 - list of packages to remove
    ansible.builtin.debug:
      var: unneeded_packages

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

  # Cups should be remove per control 2.2.4, but it may not be able to due to
  #  dependencies, so disable the service instead
  - name: 2.2.4 - 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.4

  # 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)
    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
    tags:
      - 2.2.15

    # openldap clients skipped (2.3.5)

  # Section 3, Network parameters

  # 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.1 - Set networking parameters for host only communications
    block:
      - name: 3.1 - Set ipv4 networking parameters (OFF)
        ansible.builtin.sysctl:
          name: "{{ item }}"
          value: "0"
          reload: true
          state: present
          sysctl_set: true
        loop:
          - net.ipv4.ip_forward # (3.1.1)
          - net.ipv4.conf.all.send_redirects # (3.1.2)
          - net.ipv4.conf.default.send_redirects # (3.1.2)
        notify: Flush network routes

      - name: 3.1 - Set ipv6 networking parameters (OFF)
        ansible.builtin.sysctl:
          name: "{{ item }}"
          value: "0"
          reload: true
          state: present
          sysctl_set: true
        loop:
          - net.ipv6.conf.all.forwarding # (3.1.1)
        when: not ipv6_disable
        notify: Flush network routes
    tags:
      - 3.1.0

  - name: 3.2 - Set networking parameters for host as router communications
    block:
      - name: 3.2 - Set ipv4 network parameters (OFF)
        ansible.builtin.sysctl:
          name: "{{ item }}"
          value: "0"
          reload: true
          state: present
          sysctl_set: true
        loop:
          - net.ipv4.conf.all.accept_source_route # (3.2.1)
          - net.ipv4.conf.default.accept_source_route # (3.2.1)
          - net.ipv4.conf.all.accept_redirects # (3.2.2)
          - net.ipv4.conf.default.accept_redirects # (3.2.2)
          - net.ipv4.conf.all.secure_redirects # (3.2.3)
          - net.ipv4.conf.default.secure_redirects # (3.2.3)
        notify: Flush network routes

      - name: 3.2.[4-8] - Set ipv4 networking parameters (ON)
        ansible.builtin.sysctl:
          name: "{{ item }}"
          value: "1"
          reload: true
          state: present
          sysctl_set: true
        loop:
          - net.ipv4.conf.all.log_martians # (3.2.4)
          - net.ipv4.conf.default.log_martians # (3.2.4)
          - net.ipv4.icmp_echo_ignore_broadcasts # (3.2.5)
          - net.ipv4.icmp_ignore_bogus_error_responses # (3.2.6)
          - net.ipv4.conf.all.rp_filter # (3.2.7)
          - net.ipv4.conf.default.rp_filter # (3.2.7)
          - net.ipv4.tcp_syncookies # ( 3.2.8)
        notify: Flush network routes

      - name: 3.2 - Set ipv6 networking parameters (OFF)
        ansible.builtin.sysctl:
          name: "{{ item }}"
          value: "0"
          reload: true
          state: present
          sysctl_set: true
        loop:
          - net.ipv6.conf.all.accept_source_route # (3.2.1)
          - net.ipv6.conf.default.accept_source_route # (3.2.1)
          - net.ipv6.conf.all.accept_redirects # (3.2.2)
          - net.ipv6.conf.default.accept_redirects # (3.2.2)
          - net.ipv6.conf.all.accept_ra # (3.2.9)
          - net.ipv6.conf.default.accept_ra # (3.2.9)
        notify: Flush network routes
        when: not ipv6_disable
    tags:
      - 3.2.0

  - name: 3.3 - IPv6 configuration and/or disablement
    block:
      - name: 3.3 - Set ipv6 networking parameters (OFF)
        ansible.builtin.sysctl:
          name: "{{ item }}"
          value: "0"
          reload: true
          state: present
          sysctl_set: true
        loop:
          - net.ipv6.conf.all.accept_redirects # (3.3.2)
          - net.ipv6.conf.default.accept_redirects # (3.3.2)
          - net.ipv6.conf.all.accept_ra # (3.3.1)
          - net.ipv6.conf.default.accept_ra # (3.3.1)
        notify: Flush network routes
        when: not ipv6_disable
        tags:
          - 3.3.1
          - 3.3.2

      # We check here because we don't know what position the ipv6.disable 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
      - name: 3.3 - Find if IPv6 is currently in the grub file, shows changed when it is in the file
        ansible.builtin.lineinfile:
          path: /etc/default/grub
          regexp: '^\s*GRUB_CMDLINE_LINUX.*ipv6.disable=1'
          state: absent
        check_mode: true
        changed_when: false
        register: ipv6_disable_grub
        failed_when: false
        tags:
          - 3.3.3

      # use the replace module to add it to grub bootloader and then notify
      #  grub to rebuild
      - name: 3.3 - Disable IPv6 in grub
        ansible.builtin.replace:
          path: /etc/default/grub
          regexp: '^GRUB_CMDLINE_LINUX="'
          replace: 'GRUB_CMDLINE_LINUX="ipv6.disable=1 '
        notify: Rebuild grub
        when: not ipv6_disable_grub.found and ipv6_disable
        tags:
          - 3.3.3
    when: ipv6_disable
    tags:
      3.3.0

  # TCP Wrappers is not distributed by RHEL, but was distributed via EPEL until the end
  #  of 2018. It is being left in the RHEL7 controls because it may still be in some security
  #  requirements. It is not included in the RHEL8 controls
  - name: 3.4 - TCP Wrappers
    block:
      - name: 3.4.1 - Install tcpwrappers
        ansible.builtin.package:
          name: "{{ tcpwrappers_pkg }}"
          state: present

      # use lineinfile to make sure everyone has access to tcpwrappers
      #  from the local machine
      - name: 3.4.2 - create basic hosts.allow
        ansible.builtin.lineinfile:
          dest: /etc/hosts.allow
          owner: root
          group: root
          mode: 0644
          line: "ALL: 127.0.0.1"

      # 3.4.3 is heavy handed so we aren't implementing it
      - name: Refuse to set all on hosts.deny (3.4.3) because it's too heavy handed
        ansible.builtin.debug:
          msg: "not setting all on hosts.deny (3.4.3)"

      - name: 3.4.[4-5] - Set permissions on tcpwrappers files
        ansible.builtin.file:
          path: "{{ item }}"
          owner: root
          group: root
          mode: 0644
        loop:
          - /etc/hosts.allow # (3.4.4)
          - /etc/hosts.deny # (3.4.5)
    when: tcpwrappers
    tags:
      - 3.4.0

  - name: 3.5 - Disable uncommon network protocols
    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.5.0 - Create empty list of uncommon network protocols to disable
        ansible.builtin.set_fact:
          uncommon_network: []

      - name: 3.5.1 - Add dccp to list of uncommon network protocols to disable
        ansible.builtin.set_fact:
          uncommon_network: "{{ uncommon_network + [ 'dccp' ] }}"
        tags:
          - 3.5.1

      - name: 3.5.2 - Add sctp to list of uncommon network protocols to disable
        ansible.builtin.set_fact:
          uncommon_network: "{{ uncommon_network + [ 'sctp' ] }}"
        tags:
          - 3.5.2

      - name: 3.5.3 - Add rds to list of uncommon network protocols to disable
        ansible.builtin.set_fact:
          uncommon_network: "{{ uncommon_network + [ 'rds' ] }}"
        tags:
          - 3.5.3

      - name: 3.5.4 - Add tipc to list of uncommon network protocols to disable
        ansible.builtin.set_fact:
          uncommon_network: "{{ uncommon_network + [ 'tipc' ] }}"
        tags:
          - 3.5.4

  # With the list complete, use it with the system's package manager
  #  to remove packages from the system that are not needed.
      - name: 3.5.0 - Process uncommon network list
        ansible.builtin.lineinfile:
          dest: /etc/modprobe.d/CIS.conf
          line: "install {{ item }} /bin/true"
          state: present
          create: true
        with_items:
          - "{{ uncommon_network  }}"
    tags:
      - 3.5.0

  # Section 3 - Firewall

  - name: 3.6 - Configure firewalld
    tags:
      - 3.6.0
    block:
      - name: 3.6.1 - Install firewalld
        ansible.builtin.package:
          name: "firewalld"
          state: present
        notify: Start firewalld

      - name: 3.6.1 - Disable iptables
        ansible.builtin.service:
          name: iptables
          state: stopped
          enabled: false
          masked: true
        ignore_errors: true
        failed_when: false

      - name: 3.6.1 - Remove iptables-services
        package:
          name: "iptables-services"
          state: absent

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

    when: enable_firewall is defined and enable_firewall == "firewalld"

  - name: 3.6.1 - Configure iptables
    tags:
      - 3.6.0
    block:
      - name: 3.6.1 Install iptables
        ansible.builtin.package:
          name:
            - "iptables"
            - "iptables-services"
          state: present
        notify: Start iptables
        tags:
          - 3.6.1

      - name: 3.6.1 - Disable firewalld
        ansible.builtin.service:
          name: firewalld
          state: stopped
          enabled: false
          ignore_errors: true
    when: enable_firewall is defined and enable_firewall == "iptables"

  - ansible.builtin.debug:
      msg: " Ensure default firewall policy (3.6.[2-5]) must be handled locally"


# Control 3.7 Ensure wireless interfaces are disabled is interface dependent
# skipping

  # Section 4 - Logging and Auditing

  - name: 4.1 Install and configure system auditing
    block:
      - name: 4.1.1 - Install Audit
        ansible.builtin.package:
          name:
            - audit
            - audit-libs
          state: present
        tags:
          - 4.1.1.1
      # The replace module here is looking through file and make replacements of partial lines
      - name: 4.1.1-[2-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.1.1
          - {find: '^max_log_file_action\s+=\s+((?!keep_logs).)*$', replace: 'max_log_file_action = keep_logs'} # 4.1.1.2
          - {find: '^space_left_action\s+=\s+((?!email).)*$', replace: 'space_left_action = email'} # 4.1.1.2
          - {find: '^action_mail_acct\s+=\s+((?!root).)*$', replace: 'action_mail_acct = root'} # 4.1.1.2
          - {find: '^admin_space_left_action\s+=\s+((?!suspend).)*$', replace: 'admin_space_left_action = suspend'} # 4.1.1.2
        notify: Restart auditd
        tags:
          - 4.1.1.2
          - 4.1.1.3

      - name: 4.1.2 - Enable auditd service
        ansible.builtin.service:
          name: auditd
          enabled: true
          state: started
        tags:
          - 4.1.2

      - name: 4.1.1.3 - 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.3

      # use the replace module to add it to grub bootloader and then notify
      #  grub to rebuild
      - name: 4.1.1.3 - 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.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.
      - name: 4.1.4 - Ensure to collect events that modify date/time
        ansible.builtin.template:
          dest: /etc/audit/rules.d/datetime.rules
          src: audit_rules/datetime.rules
          owner: root
          group: root
          mode: 0600
        notify: Restart auditd
        tags:
          - 4.1.4

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

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

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

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

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

      # This is the first control that we use the min_uid variable that we determined earlier
      - name: 4.1.10 - Ensure modifications to discretionary access controls are collected
        ansible.builtin.template:
          dest: /etc/audit/rules.d/dac.rules
          src: audit_rules/dac.rules
          owner: root
          group: root
          mode: 0600
        notify: Restart auditd
        tags:
          4.1.10

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

      # Control 4.1.12 - Ensure use of privileged ansible.builtin.commands is collected, is machine dependent
      # skipping

      - name: 4.1.13 - Ensure successful file system mounts are collected
        ansible.builtin.template:
          dest: /etc/audit/rules.d/file-system-mounts.rules
          src: audit_rules/file-system-mounts.rules
          owner: root
          group: root
          mode: 0600
        notify: Restart auditd
        tags:
          4.1.13

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

      - name: 4.1.15 - Ensure sysadmin actions (sudolog) are collected
        ansible.builtin.template:
          dest: /etc/audit/rules.d/sudolog.rules
          src: audit_rules/sudolog.rules
          owner: root
          group: root
          mode: 0600
        notify: Restart auditd
        tags:
          4.1.15
          4.1.16

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

      - name: 4.1.18 - 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.18
    when: enable_audit is defined and enable_audit


  - name: 4.2.1.1 - Ensure rsyslog is installed
    ansible.builtin.package:
      name: rsyslog
      state: present
    tags:
      - 4.2.3

  - name: 4.2.1.1 - Enable Rsyslog
    ansible.builtin.service:
      name: rsyslog
      enabled: true
    tags:
      - 4.2.1.1

  - name: 4.2.1.2 - Ensure logging is configured
    ansible.builtin.copy:
      src: "{{ rsyslog_file }}"
      dest: "/etc/rsyslog.d/{{ rsyslog_file }}"
      owner: root
      group: root
      mode: 0640
    when: rsylog_file is defined
    tags:
      - 4.2.1.2

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

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

  - name: 4.2.1.5 - Ensure remote rsyslog messages are only acepted on designated log hosts
    block:
      - name: 4.2.1.5 - 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.5 - 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.5 - 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.5 - 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.5 - 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.5 - 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.5 - 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
    tags:
      - 4.2.1.5

  # 4.2.2 - Configure syslog-ng skipped as it is not a supported package from RHEL

  # Ensure rsyslog package is installed (4.2.3) moved to before 4.2.1.1 which is the package install

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

  # 4.3 - Ensure logrotate is configured skipped as machine and environment dependent

  # 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: Create the cron/at allow files (5.1.8)
    ansible.builtin.copy:
      dest: "{{ item }}"
      content: ""
      force: false
      owner: root
      group: root
      mode: 0644
    with_items:
      - /etc/cron.allow
      - /etc/at.allow
    tags:
      - 5.1.8

  - name: 5.1.1 - Ensure cron is enabled (RHEL Clones)
    ansible.builtin.service:
      name: crond
      enabled: true
      state: started
    when: ansible_distribution != "SLES"
    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

  # Restrict at/cron skipped (5.1.8) as is rarely used and environment dependent

  # If you want to deploy your own SSH config file, exclude the entire 5.2.0 tag
  - name: 5.2 - SSH File configurations
    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

    # Control 5.2.2, Ensure SSH Protocol is set to 2, is not supported in OpenSSH
    # supplied by Red Hat (or SUSE 15) any longer. Omitting

    - name: 5.2.3 - Set LogLevel to {{ ssh_log_level }} or more verbose, but not ansible.builtin.debug
      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.3

    # Using replace with a replace argument of "" removes the selected
    #  text.
    - name: 5.2.4 - Disable X11 forwarding
      ansible.builtin.lineinfile:
        path: /etc/ssh/sshd_config
        state: absent
        regexp: '^X11Forwarding\s*yes'
      notify: Restart sshd
      tags:
        - 5.2.4

    - name: 5.2.5 - Ensure SSH MaxAuthTires is set to {{ ssh_max_auth_tries }} or less
      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.5

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

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

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

    - name: 5.2.19 - 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: Set approved MAC algorithms (5.2.11)
      ansible.builtin.lineinfile:
        path: /etc/ssh/sshd_config
        line: "MACs {{ ssh_mac_list }}"
        insertafter: EOF
        regexp: '^MACs {{ ssh_mac_list }}'
      notify: Restart sshd
      tags:
        - 5.2.11

    - name: 5.2.12 - 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.12

    - name: 5.2.12 - 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.12

    - name: 5.2.13 - Ensure SSH LoginGraceTime is set to {{ ssh_grace_time }} or less
      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.13

    # Control 5.2.14 is system dependent
    # skipping

    - 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 }}"
      notify: Restart sshd
      tags:
        - 5.2.15

    # 5.2.16 - Configure PAM - skipped
    # 5.2.17 - Disable SSH AllowTCPForwarding
    - name: 5.2.17 - Ensure SSH AllowTcpForwarding is disabled
      ansible.builtin.lineinfile:
        path: "/etc/ssh/sshd_config"
        line: "AllowTcpForwarding no"
        regexp: '^AllowTcpForwarding\s+(yes|no)'
        insertafter: "^#AllowTcpForwarding"
      notify: Restart sshd
      tags:
        - 5.2.17

    tags:
      5.2.0


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

    - name: 5.3.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 ="
      when: password_req_upper

    - name: 5.3.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 ="
      when: password_req_lower

    - name: 5.3.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 ="
      when: password_req_digit

    - name: 5.3.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 ="
      when: password_req_digit
    tags:
      - 5.3.1

  # Control 5.3.2, Ensure lockout for failed password attempts, really requires a file replacement
  # skipping

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

  - name: 5.3.4 - Ensure password hashing algorithm is set to sha-512
    block:
    - name: 5.3.4 - Determine if we are currently using sha512 password
      ansible.builtin.command: authconfig --test
      register: sha512_exist
      changed_when: false

    - name: 5.3.4 - SHA-512 Password setting
      ansible.builtin.command: /usr/sbin/authconfig --enableshadow --passalgo sha512 --update
      when: not sha512_exist.stdout is search("sha512")
      failed_when: false
    when: not ansible_check_mode
    tags:
      5.3.4

  - name: 5.4.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.4.1.1

  - name: 5.4.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.4.1.2

  - name: 5.4.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.4.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.4.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.4.1.4

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

  # 5.4.2, Ensure all system accounts do not have an active shell,
  # is not easily automated. Will revisit later

  # 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.4.3 - Ensure default group for root is GID 0
    ansible.builtin.command: /usr/sbin/usermod -g 0 root
    changed_when: false
    tags:
      - 5.4.3

  - name: 5.4.4 - Ensure umask is set
    ansible.builtin.replace:
      path: "{{ item }}"
      replace: "     umask {{ default_umask }}"
      regexp: '^\s*umask\s*022'
    loop:
      - /etc/bashrc
      - /etc/profile
    when: ansible_distribution != "SLES"
    tags:
      - 5.4.4

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


  # 5.5.5,  Ensure root login is restricted to system console
  # not easily automatable because of the various TTYs on a machine
  # Manually verify that only physically secure TTYs are listed  in
  # /etc/securetty

  - name: 5.6 - Restrict su to wheel group
    block:
      - name: Configure PAM to only allow su from wheel group (5.6)
        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"

      - name: Add root to the wheel group (5.6)
        ansible.builtin.user:
          name: root
          groups: wheel
          append: true
    tags:
      - 5.6.0

  # Section 6 - System Maintenance
  #

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

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

  - name: 6.1.[3,5] - Ensure permissions on /etc/gshadow
    ansible.builtin.file:
      path: /etc/{{ item }}
      owner: root
      group: root
      mode: 0000
    loop:
      - gshadow
    when: ansible_distribution != "SLES"
    tags:
      - 6.1.5

  - name: 6.1.[6-8] - Ensure permissions on /etc/passwd- /etc/shadow- /etc/group-
    ansible.builtin.file:
      path: /etc/{{ item }}
      owner: root
      group: root
      mode: 0000
    with_items:
      - passwd-
      - shadow-
      - group-
    tags:
      - 6.1.6
      - 6.1.7
      - 6.1.8

  - name: 6.1.9 - Ensure permissions on /etc/passwd- /etc/gshadow-
    ansible.builtin.file:
      path: /etc/{{ item }}
      owner: root
      group: root
      mode: 0000
    with_items:
      - gshadow-
    when: ansible_distribution != "SLES"
    tags:
      - 6.1.9

  # 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.10 - Ensure no world writable files exist
    block:
      - name: 6.1.10 - 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.10 - Print any world writable files found
        ansible.builtin.debug:
          msg: "World writiable files found: {{ ww_files.stdout }}"
        changed_when: true
        when: ww_files.stdout
    tags:
      - 6.1.10

  # Control 6.1.11, Ensure no unowned files exist, is system dependent so we are only
  # providing a list to the user here.
  - name: 6.1.11 - Ensure no unowned files exist
    block:
      - name: 6.1.11 - 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.11 - Print any unowned files found
        ansible.builtin.debug:
          msg: "unowned files found: {{ uo_files.stdout }}"
        changed_when: true
        when: uo_files.stdout
    tags:
      - 6.1.11

  # Control 6.1.12, Enscure no ungrouped files exist, is system dependent so we are only
  # providing a list to the user here.
  - name: 6.1.12 - Ensure no ungrouped files exist
    block:
      - name: 6.1.12 - 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.12 - Print any ungrouped files found
        ansible.builtin.debug:
          msg: "ungrouped files found: {{ uo_files.stdout }}"
        changed_when: true
        when: ug_files.stdout
    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

  - name: 6.2.1 - Ensure password fields are not empty
    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 }}"
    tags:
      - 6.2.1

  - name: 6.2.[2,4-5] - Ensure no legacy "+" entries exist in password files
    ansible.builtin.lineinfile:
      regexp: '^\+:.*'
      state: absent
      path: "{{ item }}"
    when: ypbind is defined and not ypbind
    loop:
      - /etc/passwd
      - /etc/shadow
      - /etc/group
    tags:
      - 6.2.2
      - 6.2.3
      - 6.2.4

  - name: Report on multiple accounts with UID of 0 (6.2.5)
    block:
      - name: 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: Report on mulitple accounts with UID of 0
        ansible.builtin.debug:
          msg: "Accounts with UID zero in addition to root"
          var: rootuid.stdout
        when: rootuid.stdout != 'root'
    tags:
      - 6.2.5

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

      - name: 6.2.6 - 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
    tags:
      - 6.2.6

  # Control 6.2.7 is environment dependent, skipping
  # Control 6.2.8 is environment dependent, skipping
  # Control 6.2.9 is environment dependent, skipping
  # Controls 6.2.[10-14] are recommended to be handled by monitoring software

  - name: 6.2.15 - Report on groups in /etc/passwd  with a GID not in /etc/group
    block:
      - name: 6.2.15 - 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.15 - Report to user any unreferenced groups
        ansible.builtin.debug:
          msg: "{{ undefined_groups.stdout_lines }}"
        changed_when: true
        when: undefined_groups.stdout
    tags:
      - 6.2.15

  - name: 6.2.16 - Report on duplicate UIDs in /etc/passwd
    block:
      - name: 6.2.16 - 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.16 - Print report of duplicated UIDs to user
        ansible.builtin.debug:
          msg: "{{ duplicate_uids.stdout_lines }}"
        changed_when: true
        when: duplicate_uids.stdout
    tags:
      - 6.2.16

  - name: 6.2.17 - Report on duplicate GIDs in /etc/group
    block:
      - name: 6.2.17 - 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.17 - Print report of duplcate GIDs to user
        ansible.builtin.debug:
          msg: "{{ duplicate_guids.stdout_lines }}"
        changed_when: true
        when: duplicate_guids.stdout
    tags:
      - 6.2.17

  - name: 6.2.18 - Report on duplicate users in /etc/passwd
    block:
      - name: 6.2.18 - 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.18 - Print report of duplicate users to user
        ansible.builtin.debug:
          msg: "{{ duplicate_users.stdout_lines }}"
        changed_when: true
        when: duplicate_users.stdout
    tags:
      - 6.2.18

  - name: 6.2.19 - Report on duplicate groups in /etc/group
    block:
      - name: 6.2.19 - 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.19 - Print report of duplicate groups to user
        ansible.builtin.debug:
          msg: "{{ duplicate_groups.stdout_lines }}"
        changed_when: true
        when: duplicate_groups.stdout
    tags:
      - 6.2.19
