From bf018e9f8327b231b967db8ec74fabf01802b6a8 Mon Sep 17 00:00:00 2001 From: Watson Sato Date: Wed, 11 Aug 2021 09:45:04 +0200 Subject: [PATCH 1/3] Add test for ansible files removed and readded Check if any playbook removes a file and then add it back again. The file removal is based on the 'file' module with 'state: absent', and the reintroduction of the file is based on 'lineinfile', 'blockinfile' and 'copy' modules. --- CMakeLists.txt | 2 + tests/CMakeLists.txt | 8 ++ tests/test_ansible_file_removed_and_added.py | 97 ++++++++++++++++++++ 3 files changed, 107 insertions(+) create mode 100644 tests/test_ansible_file_removed_and_added.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 330b869d0f9..e41f2caa630 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -129,6 +129,7 @@ find_python_module(jinja2 REQUIRED) find_python_module(pytest) find_python_module(pytest_cov) find_python_module(json2html) +find_python_module(yamlpath) # sphinx documentation requirements find_python_module(sphinx) @@ -231,6 +232,7 @@ message(STATUS "python pytest module (optional): ${PY_PYTEST}") message(STATUS "ansible-playbook module (optional): ${ANSIBLE_PLAYBOOK_EXECUTABLE}") message(STATUS "ansible-lint module (optional): ${ANSIBLE_LINT_EXECUTABLE}") message(STATUS "yamllint module (optional): ${YAMLLINT_EXECUTABLE}") +message(STATUS "yamlpath module (optional): ${PY_YAMLPATH}") message(STATUS "BATS framework (optional): ${BATS_EXECUTABLE}") message(STATUS "python sphinx module (optional): ${PY_SPHINX}") message(STATUS "python sphinxcontrib.autojinja module (optional): ${PY_SPHINXCONTRIB.AUTOJINJA}") diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 3e2d8a4ec31..739cc124035 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -121,3 +121,11 @@ add_test( ) set_tests_properties("fix_rules-sort_subkeys" PROPERTIES LABELS quick) set_tests_properties("fix_rules-sort_subkeys" PROPERTIES DEPENDS "test-rule-dir-json") + +if (PY_YAMLPATH) + add_test( + NAME "ansible-file-removed-and-added" + COMMAND env "PYTHONPATH=$ENV{PYTHONPATH}" "${PYTHON_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/test_ansible_file_removed_and_added.py" --ansible_dir "${CMAKE_BINARY_DIR}/ansible" + ) + set_tests_properties("fix_rules-sort_subkeys" PROPERTIES LABELS quick) +endif() diff --git a/tests/test_ansible_file_removed_and_added.py b/tests/test_ansible_file_removed_and_added.py new file mode 100644 index 00000000000..23f6f888bda --- /dev/null +++ b/tests/test_ansible_file_removed_and_added.py @@ -0,0 +1,97 @@ +#!/usr/bin/python3 + +import argparse +import os +import sys +from types import SimpleNamespace +from yamlpath import Processor +from yamlpath import YAMLPath +from yamlpath.common import Parsers +from yamlpath.exceptions import YAMLPathException +from yamlpath.wrappers import ConsolePrinter + + +def parse_command_line_args(): + parser = argparse.ArgumentParser( + description="Checks if an Ansible Playbook removes a file and then adds it again.") + parser.add_argument("--ansible_dir", required=True, + help="Directory containing Ansible Playbooks") + args = parser.parse_args() + return args + + +def check_playbook_file_removed_and_added(playbook_path): + playbook_ok = True + + yaml_parser = Parsers.get_yaml_editor() + + logging_args = SimpleNamespace(quiet=False, verbose=False, debug=False) + log = ConsolePrinter(logging_args) + + # Find every path removed by a file Task (also matches tasks within blocks) + files_absent_string = "tasks.**.file[state=absent][parent()].path" + files_absent_yamlpath = YAMLPath(files_absent_string) + path_editing_tasks_yamlpath = "" + + log.info("Info: Evaluating playbook '{}'".format(playbook_path)) + (yaml_data, doc_loaded) = Parsers.get_yaml_data(yaml_parser, log, playbook_path) + if not doc_loaded: + # There was an issue loading the file; an error message has already been + # printed via ConsolePrinter. + return False + + processor = Processor(log, yaml_data) + try: + for node in processor.get_nodes(files_absent_yamlpath, mustexist=False): + path = str(node) + # 'node' is a NodeCoords. + if path == 'None': + continue + elif "{{" in path: + # Identified path is a Jinja expression, unfortunately there is no easy way to get + # the actual path without making this test very complicated + continue + + # Check if this paths is used in any of the following ansible modules + ansible_modules = ["lineinfile", "blockinfile", "copy"] + path_editing_tasks_string = "tasks.**.[.=~/{modules}/][*='{path}'][parent()].name" + path_editing_tasks_yamlpath = YAMLPath(path_editing_tasks_string.format( + modules="|".join(ansible_modules), + path=node) + ) + for task in processor.get_nodes(path_editing_tasks_yamlpath, mustexist=False): + log.info("Error: Task '{}' manipulates a file that is removed by another task" + .format(task)) + playbook_ok = False + except YAMLPathException as ex: + no_file_msg = ("Cannot add PathSegmentTypes.TRAVERSE subreference to lists at 'None' " + "in '{}'.") + if str(ex) == no_file_msg.format(files_absent_string): + log.info("Info: Playbook {} has no 'file' tasks.".format(playbook_path)) + elif path_editing_tasks_yamlpath and str(ex) == no_file_msg.format( + path_editing_tasks_yamlpath): + log.info("Info: Playbook {} has no '{}' tasks.".format( + playbook_path, " ".join(ansible_modules))) + else: + log.info("Error: {}.".format(ex)) + + return playbook_ok + + +def main(): + args = parse_command_line_args() + + all_playbooks_ok = True + for dir_item in os.listdir(args.ansible_dir): + if dir_item.endswith(".yml"): + playbook_path = os.path.join(args.ansible_dir, dir_item) + + if not check_playbook_file_removed_and_added(playbook_path): + all_playbooks_ok = False + + if not all_playbooks_ok: + sys.exit(1) + + +if __name__ == "__main__": + main() From e6d727762ba446cad94f1e002fa7a7fef0f1a4cb Mon Sep 17 00:00:00 2001 From: Watson Sato Date: Wed, 11 Aug 2021 09:48:14 +0200 Subject: [PATCH 2/3] Unit tests the function for file removed and added Add a unit test for the core function that checks if any playbook removes a file and then reintroduces it back. --- tests/CMakeLists.txt | 6 ++ .../file_block_removed_and_added.yml | 69 +++++++++++++++++++ .../file_not_removed_and_added.yml | 49 +++++++++++++ .../file_removed_and_added.yml | 62 +++++++++++++++++ .../file_removed_and_not_added.yml | 46 +++++++++++++ ...t_check_playbook_file_removed_and_added.py | 39 +++++++++++ 6 files changed, 271 insertions(+) create mode 100644 tests/ansible_file_removed_and_added/file_block_removed_and_added.yml create mode 100644 tests/ansible_file_removed_and_added/file_not_removed_and_added.yml create mode 100644 tests/ansible_file_removed_and_added/file_removed_and_added.yml create mode 100644 tests/ansible_file_removed_and_added/file_removed_and_not_added.yml create mode 100644 tests/test_check_playbook_file_removed_and_added.py diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 739cc124035..000a1b1385d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -123,6 +123,12 @@ set_tests_properties("fix_rules-sort_subkeys" PROPERTIES LABELS quick) set_tests_properties("fix_rules-sort_subkeys" PROPERTIES DEPENDS "test-rule-dir-json") if (PY_YAMLPATH) + if (PY_PYTEST) + add_test( + NAME "test-function-check_playbook_file_removed_and_added" + COMMAND "${PYTHON_EXECUTABLE}" -m pytest ${PYTEST_COVERAGE_OPTIONS} "${CMAKE_CURRENT_SOURCE_DIR}/test_check_playbook_file_removed_and_added.py" + ) + endif() add_test( NAME "ansible-file-removed-and-added" COMMAND env "PYTHONPATH=$ENV{PYTHONPATH}" "${PYTHON_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/test_ansible_file_removed_and_added.py" --ansible_dir "${CMAKE_BINARY_DIR}/ansible" diff --git a/tests/ansible_file_removed_and_added/file_block_removed_and_added.yml b/tests/ansible_file_removed_and_added/file_block_removed_and_added.yml new file mode 100644 index 00000000000..8863b333129 --- /dev/null +++ b/tests/ansible_file_removed_and_added/file_block_removed_and_added.yml @@ -0,0 +1,69 @@ +--- + +- hosts: all + vars: + var_system_crypto_policy: !!str FUTURE + var_sudo_logfile: !!str /var/log/sudo.log + + tasks: + - name: Modify the System Login Banner - add correct banner + lineinfile: + dest: /etc/issue + line: '{{ login_banner_text | regex_replace("^\^(.*)\$$", "\1") | regex_replace("^\((.*)\|.*\)$", + "\1") | regex_replace("\[\\s\\n\]\+"," ") | regex_replace("\(\?:\[\\n\]\+\|\(\?:\\\\n\)\+\)", + "\n") | regex_replace("\\", "") | wordwrap() }}' + create: true + when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"] + tags: + - banner_etc_issue + - low_complexity + - medium_disruption + - medium_severity + - no_reboot_needed + - unknown_strategy + + - name: Test for existence /etc/issue + stat: + path: /etc/issue + register: file_exists + tags: + - configure_strategy + - file_permissions_etc_issue + - low_complexity + - low_disruption + - medium_severity + - no_reboot_needed + + - name: Ensure permission 0644 on /etc/issue + file: + path: /etc/issue + mode: '0644' + when: file_exists.stat is defined and file_exists.stat.exists + tags: + - configure_strategy + - file_permissions_etc_issue + - low_complexity + - low_disruption + - medium_severity + - no_reboot_needed + + - block: + + - name: Remove Rsh Trust Files + file: + path: /root/shosts.equiv + state: absent + + - name: Add line to /root/shosts.equiv + lineinfile: + dest: /root/shosts.equiv + line: 'test host' + create: true + tags: + - high_severity + - low_complexity + - low_disruption + - no_reboot_needed + - no_rsh_trust_files + - restrict_strategy + diff --git a/tests/ansible_file_removed_and_added/file_not_removed_and_added.yml b/tests/ansible_file_removed_and_added/file_not_removed_and_added.yml new file mode 100644 index 00000000000..3d3e53b958f --- /dev/null +++ b/tests/ansible_file_removed_and_added/file_not_removed_and_added.yml @@ -0,0 +1,49 @@ +--- + +- hosts: all + vars: + var_system_crypto_policy: !!str FUTURE + var_sudo_logfile: !!str /var/log/sudo.log + + tasks: + - name: Modify the System Login Banner - add correct banner + lineinfile: + dest: /etc/issue + line: '{{ login_banner_text | regex_replace("^\^(.*)\$$", "\1") | regex_replace("^\((.*)\|.*\)$", + "\1") | regex_replace("\[\\s\\n\]\+"," ") | regex_replace("\(\?:\[\\n\]\+\|\(\?:\\\\n\)\+\)", + "\n") | regex_replace("\\", "") | wordwrap() }}' + create: true + when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"] + tags: + - banner_etc_issue + - low_complexity + - medium_disruption + - medium_severity + - no_reboot_needed + - unknown_strategy + + - name: Test for existence /etc/issue + stat: + path: /etc/issue + register: file_exists + tags: + - configure_strategy + - file_permissions_etc_issue + - low_complexity + - low_disruption + - medium_severity + - no_reboot_needed + + - name: Ensure permission 0644 on /etc/issue + file: + path: /etc/issue + mode: '0644' + when: file_exists.stat is defined and file_exists.stat.exists + tags: + - configure_strategy + - file_permissions_etc_issue + - low_complexity + - low_disruption + - medium_severity + - no_reboot_needed + diff --git a/tests/ansible_file_removed_and_added/file_removed_and_added.yml b/tests/ansible_file_removed_and_added/file_removed_and_added.yml new file mode 100644 index 00000000000..a44c39a9db2 --- /dev/null +++ b/tests/ansible_file_removed_and_added/file_removed_and_added.yml @@ -0,0 +1,62 @@ +--- + +- hosts: all + vars: + var_system_crypto_policy: !!str FUTURE + var_sudo_logfile: !!str /var/log/sudo.log + + tasks: + - name: Modify the System Login Banner - remove incorrect banner + file: + state: absent + path: /etc/issue + when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"] + tags: + - banner_etc_issue + - low_complexity + - medium_disruption + - medium_severity + - no_reboot_needed + - unknown_strategy + + - name: Modify the System Login Banner - add correct banner + lineinfile: + dest: /etc/issue + line: '{{ login_banner_text | regex_replace("^\^(.*)\$$", "\1") | regex_replace("^\((.*)\|.*\)$", + "\1") | regex_replace("\[\\s\\n\]\+"," ") | regex_replace("\(\?:\[\\n\]\+\|\(\?:\\\\n\)\+\)", + "\n") | regex_replace("\\", "") | wordwrap() }}' + create: true + when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"] + tags: + - banner_etc_issue + - low_complexity + - medium_disruption + - medium_severity + - no_reboot_needed + - unknown_strategy + + - name: Test for existence /etc/issue + stat: + path: /etc/issue + register: file_exists + tags: + - configure_strategy + - file_permissions_etc_issue + - low_complexity + - low_disruption + - medium_severity + - no_reboot_needed + + - name: Ensure permission 0644 on /etc/issue + file: + path: /etc/issue + mode: '0644' + when: file_exists.stat is defined and file_exists.stat.exists + tags: + - configure_strategy + - file_permissions_etc_issue + - low_complexity + - low_disruption + - medium_severity + - no_reboot_needed + diff --git a/tests/ansible_file_removed_and_added/file_removed_and_not_added.yml b/tests/ansible_file_removed_and_added/file_removed_and_not_added.yml new file mode 100644 index 00000000000..08cda7e5063 --- /dev/null +++ b/tests/ansible_file_removed_and_added/file_removed_and_not_added.yml @@ -0,0 +1,46 @@ +--- + +- hosts: all + vars: + var_system_crypto_policy: !!str FUTURE + var_sudo_logfile: !!str /var/log/sudo.log + + tasks: + - name: Modify the System Login Banner - remove incorrect banner + file: + state: absent + path: /etc/issue + when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"] + tags: + - banner_etc_issue + - low_complexity + - medium_disruption + - medium_severity + - no_reboot_needed + - unknown_strategy + + - name: Test for existence /etc/issue + stat: + path: /etc/issue + register: file_exists + tags: + - configure_strategy + - file_permissions_etc_issue + - low_complexity + - low_disruption + - medium_severity + - no_reboot_needed + + - name: Ensure permission 0644 on /etc/issue + file: + path: /etc/issue + mode: '0644' + when: file_exists.stat is defined and file_exists.stat.exists + tags: + - configure_strategy + - file_permissions_etc_issue + - low_complexity + - low_disruption + - medium_severity + - no_reboot_needed + diff --git a/tests/test_check_playbook_file_removed_and_added.py b/tests/test_check_playbook_file_removed_and_added.py new file mode 100644 index 00000000000..181bb14ed46 --- /dev/null +++ b/tests/test_check_playbook_file_removed_and_added.py @@ -0,0 +1,39 @@ +import os +import pytest + +from .test_ansible_file_removed_and_added import check_playbook_file_removed_and_added + + +def test_file_removed_and_added(): + playbook_path = os.path.join(os.path.dirname(__file__), + "ansible_file_removed_and_added", + "file_removed_and_added.yml") + assert not check_playbook_file_removed_and_added(playbook_path) + + +def test_file_removed_and_not_added(): + playbook_path = os.path.join(os.path.dirname(__file__), + "ansible_file_removed_and_added", + "file_removed_and_not_added.yml") + assert check_playbook_file_removed_and_added(playbook_path) + + +def test_file_not_removed_and_added(): + playbook_path = os.path.join(os.path.dirname(__file__), + "ansible_file_removed_and_added", + "file_not_removed_and_added.yml") + assert check_playbook_file_removed_and_added(playbook_path) + + +def test_file_block_removed_and_added(): + playbook_path = os.path.join(os.path.dirname(__file__), + "ansible_file_removed_and_added", + "file_block_removed_and_added.yml") + assert not check_playbook_file_removed_and_added(playbook_path) + + +def test_file_block_removed_and_not_added(): + playbook_path = os.path.join(os.path.dirname(__file__), + "ansible_file_removed_and_added", + "file_block_removed_and_not_added.yml") + assert check_playbook_file_removed_and_added(playbook_path) From 741ec823ac39341f8aa0649031b72d2ac36e8a64 Mon Sep 17 00:00:00 2001 From: Watson Sato Date: Thu, 12 Aug 2021 10:36:47 +0200 Subject: [PATCH 3/3] Mention Ansible static yamlpath test in docs --- .../developer/02_building_complianceascode.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/manual/developer/02_building_complianceascode.md b/docs/manual/developer/02_building_complianceascode.md index d536df0a259..87469bf5f9b 100644 --- a/docs/manual/developer/02_building_complianceascode.md +++ b/docs/manual/developer/02_building_complianceascode.md @@ -64,6 +64,20 @@ yum install yamllint ansible-lint apt-get install yamllint ansible-lint ``` +### Static Ansible Playbooks tests + +Install `yamlpath` and `pytest` to run tests cases that analyse the Ansible +Playbooks' yaml nodes. +```bash +pip3 install yamlpath + +# Fedora/RHEL +yum install python3-pytest + +# Ubuntu/Debian +apt-get install python-pytest +``` + ### Ninja (Faster Builds) Install the `ninja` build system if you want to use it instead of